顯示具有 LINQ 標籤的文章。 顯示所有文章
顯示具有 LINQ 標籤的文章。 顯示所有文章

2011年5月26日 星期四

LINQ 的延遲執行(Deferred execution) 造成的奇特現象

LINQ 的一大強項,就是 deferred execution。所謂的 deferred execution,是指當下宣告時只是宣告未來要執行的命令,而實際的執行要等到列舉(enumerate) 時才真的開始。

範例

class Program
{
  static void Main(string[] args)
  {
    var stringArray = new string[] { "A", "B", "D", "D", "E"};

    //宣告執行的動作,未實際執行
    var q = from i in stringArray
            where i.StartsWith("D")
            select i;
    //列舉時才真的執行
    var count = q.Count();
    Console.WriteLine(count);
  }
}

 

這裡看來沒什麼問題。

問題範例

class Program
{
static void Main(string[] args)
{
  var stringArray = new string[] { "A", "B", "D", "D", "E"};
  var enumCount = 0;
  //宣告執行的動作,未實際執行
  var q = from i in stringArray
          select new {EnumCount = enumCount++, i};
  //列舉時才真的執行
  foreach (var item in q)
  {
    enumCount++;
    Console.WriteLine("{0}/{1}", enumCount, item.EnumCount );
  }
}
}

輸出

image

看到問題了嗎?原以為在 q 裡面的值應該是 0, 1, 2, 3, 4,結果輸出的卻是 0, 2, 4, 6, 8。

原因是在實際執行時(foreach loop 內)受到了 enumCount++這一行的干擾,讓原本的執行原意被扭曲了。

建議

LINQ 雖然好用,但在執行時千萬要注意 deferred execution 的現象。不該 deferred execution 時,就直接列舉吧。即 .ToList(), ToArray(), Sum() 等

2010年4月27日 星期二

Linq to Xml: 寫出 XDeclaration 的資料

當以 linq to xml 的方式組 xml 是非常輕易的事情。但在下面的程式碼,想要輸出 xml declaration 卻碰到了軟釘子

錯誤寫法

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   Console.WriteLine(doc.ToString());
   8: }

輸出如下所述,並沒有包含預期的<?xml version="1.0" encoding="big5"?>

   1: <r>
   2:   <a>3</a>
   3: </r>

輸出到文字檔

為什麼呢?但如果改成下面的程式輸出到文字檔,在檔案內容就是正確的。

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   doc.Save(@"c:\temp\a.xml");
   8: }

StringWriter

我們也可以改用 StringWriter 寫出。如下例

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   var writer = new StringWriter();
   8:   doc.Save(writer);
   9:   Console.WriteLine(writer.ToString());
  10: }

輸出如下

   1: <?xml version="1.0" encoding="utf-16"?>
   2: <r>
   3:   <a>3</a>
   4: </r>

使用 StringWriter 的特性更怪,它會視XDocument 的內容來覆寫實際的 encoding。由於c# 的 string 都是 unicode,故一律輸出成 utf-16 的編碼。

最後,找到一個較能符合需求的方式。如下

兩段式ToString

   1: static void Main(string[] args)
   2: {
   3:   string xml = "<r><a>3</a></r>";
   4:   XDocument doc = new XDocument(
   5:     new XDeclaration("1.0", "big5", null),
   6:     XElement.Parse(xml));
   7:   Console.WriteLine(doc.Declaration.ToString() +  doc.ToString());
   8: }

使用兩段式的 ToString()方式是蠻奇怪的。但這是我試到最簡單的方法了

2010年1月22日 星期五

Cannot convert lambda expression to type 'string' because it is not a delegate type

開發程式程,當然預設使用 Linq 的語法。發現了一個錯誤,訊息如下
Error 234 Cannot convert lambda expression to type 'string' because it is not a delegate type
錯誤出現在下方的 linq 語法上

 

var ctx = new ProjectServerEntities();
string email = (from i in ctx.Resources
       where i.NAME == displayName
       select i.Email).SingleOrDefault();

怎麼看都沒問題啊!

原來是少了 using System.Linq;

天啊!誰看的懂哪。

2009年2月23日 星期一

現在開始投資 Linq to entities

看了 LINQ to SQL next steps 這一篇,感到 Linq to sql 大概真的不行了呢。

Update on LINQ to SQL and LINQ to Entities Roadmap (由Tim Mallalieu,也是 LINQ to SQL and Entity Framework 的 Team Program Manager)更是建議直接改用 Entity Framework.

http://codebetter.com/blogs/david.hayden/archive/2008/10/31/linq-to-sql-is-dead-read-between-the-lines.aspx 更是宣告 Linq to Sql is Dead.
現在必須開始使用 Linq to entities.

2008年11月5日 星期三

Enumerable.Range

在寫測試時,常常需要產生整數陣列的資料。之前都是這樣寫的

	int[] ints = new int[5];
     	for (int i = 0; i < 5; i++)
	    ints[i] = i;
現在發現,這樣寫比較快。不過必須是.net 3.5
	int[] ints = System.Linq.Enumerable.Range(1, 5).ToArray();

2008年10月31日 星期五

LINQ to SQL 在 SQL server 2005 與 2000 執行效能大不同

使用一模一樣的資料庫,一個 run 在 SQL Server 2005, 一個則是 SQL Server 2000。
開發時使用 Linq to SQL連線到 2000 的資料庫。
執行時分別連線到  2000 及 2005.

發現執行的結果是一樣的,但使用 profiler 看到的 sql script 卻完全不同。
由於目前測試的是客戶的專案,不能放到這裡來公佈。


只能說,SQL Server 2000 趕快放棄吧。

2008年9月18日 星期四

XDocument 與 XElement 的一個不同處

使用 XElement 或 XDocument 來載入 xml 時,到底有什麼不同的地方呢?

範例

 

      string xml = @"el 1el 2el 3";
      XElement el = XElement.Parse(xml);
      var q = from e in el.Elements()
              select e;
      Console.WriteLine("Load in XElement");
      foreach (var item in q)
      {
        Console.WriteLine(item);
      }
      XDocument doc = XDocument.Parse(xml);
      q = from e in doc.Elements()
          select e;
      Console.WriteLine("Load in XDocument");
      foreach (var item in q)
      {
        Console.WriteLine(item);
      }
結果如下
Load in XElement
el 1
el 2
el 3
Load in XDocument

  
  el 1
  el 2
  el 3
由此可看出,使用 XElement 的 Elements() 時,不會讀取 xml comment,會將該 r 當作 element 的current node
而使用 XDocument 的 Elements()時,會將 xml comment 也當成 node。會將整份文件當成 current document,所以使用 Elements()方法時,就會讀到 r 這一個子節點了。

Linq to xml 的強制轉換好處

範例

以下範例,說明將 date 的attribute 取出後,轉成 datetime 的過程。注意 invoice 這個 element 可能沒有date這個attribute
      string xml = @"";
      XElement el = XElement.Parse(xml);
      string strDate = el.Element("invoice").Attribute("date").Value;
      DateTime? date = null;
      if (!string.IsNullOrEmpty(strDate))
        date = DateTime.Parse(strDate);
      Console.WriteLine(date);
這樣一來,程式碼就顯的有些長而不易維護。 因此,在linq to xml 的設計時,有考慮到這一點。

使用強制轉換的範例

      string xml = @"";
      XElement el = XElement.Parse(xml);
      
      DateTime? date = (DateTime?) el.Element("invoice").Attribute("date");
      
      Console.WriteLine(date);
注意到可直接使用 DateTime? date = (DateTime?) … 來取得可能為 null 的 attribute。
這樣的方法可適用於 element

2008年9月1日 星期一

Linq to XML: Write NameSpace

Linq to XML 的寫作非常方便。以往要組一個 xml ,都必須從 XmlDocument 開始,現在使用 Linq to XML 就不用了。

範例1:

例如,下列程式碼

      
	//code 1
	XElement el = new XElement("r",
        new XElement("invoice",
          new XElement("invoiceNumber", "AB12345678"),
          new XElement("invoiceDate", DateTime.Now)
          )
        );
產生的 xml 結果如下
<r>
  <invoice>
    <invoiceNumber>AB12345678</invoiceNumber>
    <invoiceDate>2008-09-01T10:08:22.9666505+08:00</invoiceDate>
  </invoice>
</r>

這樣的方式極為簡便。

範例2:


如果要產生 namespace 呢? 以下的程式碼會產生 default namespace

      //code 2
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement(ns + "r",
        new XElement(ns + "invoice",
          new XElement(ns + "invoiceNumber", "AB12345678"),
          new XElement(ns + "invoiceDate", DateTime.Now)
          ));
產生如下的 xml
<r xmlns="http://www.compnay.com.tw/Invoice">
  <invoice>
    <invoiceNumber>AB12345678</invoiceNumber>
    <invoiceDate>2008-09-01T10:16:48.2077244+08:00</invoiceDate>
  </invoice>
</r>

範例3:

也可以使用如下的寫法,但效能會差一些,原因 runtime 要計算用了哪些 namespace。
      //code 3
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement("{http://www.compnay.com.tw/Invoice}r",
        new XElement("{http://www.compnay.com.tw/Invoice}invoice",
          new XElement("{http://www.compnay.com.tw/Invoice}invoiceNumber", "AB12345678"),
          new XElement("{http://www.compnay.com.tw/Invoice}invoiceDate", DateTime.Now)
          )
        );
結果是一樣的
<r xmlns="http://www.compnay.com.tw/Invoice">
  <invoice>
    <invoiceNumber>AB12345678</invoiceNumber>
    <invoiceDate>2008-09-01T10:20:56.8615067+08:00</invoiceDate>
  </invoice>
</r>

這樣的寫法雖然較容易了解,但較不容易修改,我個人不建議這樣的寫法。

範例4:


另外,注意到雖然我們沒有說要寫成 default namespace,但Linq to XML 會「自動」地視狀況修改。
我們可以使用 XAttribute 來使用 namesapce prefix。如下程式碼。

      //code 4 
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement("r",
        new XAttribute(XNamespace.Xmlns + "i", "http://www.compnay.com.tw/Invoice"),
        new XElement("invoice",
          new XElement("invoiceNumber", "AB12345678"),
          new XElement("invoiceDate", DateTime.Now)
          )
        );
結果如下。注意到這樣的方法,可以很方便地產生了一個 namespace prefix i,但卻沒有任何節點使用到這個 prefix
<r xmlns:i="http://www.compnay.com.tw/Invoice">
  <invoice>
    <invoiceNumber>AB12345678</invoiceNumber>
    <invoiceDate>2008-09-01T10:27:11.773833+08:00</invoiceDate>
  </invoice>
</r>

範例5:

使用下面的程式碼,就不難了解 xml namespace 的特殊

      //code 5
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement("r",
        new XAttribute(XNamespace.Xmlns + "i", "http://www.compnay.com.tw/Invoice"),
        new XElement("{i}invoice",
          new XElement("invoiceNumber", "AB12345678"),
          new XElement("invoiceDate", DateTime.Now)
          )
        );
產生的xml 如下。xml:i="http:....." 這個 namespace 宣告了,卻沒有任何節點使用。i 是一個 namespace 的值,是inovice節點的 default namespace。invoiceNumber 與 invoiceDate的 namespace 卻是 ""。一個簡單的xml,卻有三個 namespace,基本上是錯誤的設計。
<r xmlns:i="http://www.compnay.com.tw/Invoice">
  <invoice xmlns="i">
    <invoiceNumber xmlns="">AB12345678</invoiceNumber>
    <invoiceDate xmlns="">2008-09-01T11:02:47.1886207+08:00</invoiceDate>
  </invoice>
</r>

範例6:

如果要像 code 2,產生相同的結果,但不要每一個節點使用 ns + "xxx"這樣的方式可不可以呢?如下的程式碼會發生錯誤

    //code 6, fails
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement("r",
        new XAttribute("xmlns", "http://www.compnay.com.tw/Invoice"),
        new XElement("invoice",
          new XElement("invoiceNumber", "AB12345678"),
          new XElement("invoiceDate", DateTime.Now)
          )
          );

錯誤的訊息為 Unhandled Exception: System.Xml.XmlException: The prefix '' cannot be redefined from '' to 'http://www.compnay.com.tw/Invoice' within the same start element tag. 看來,沒有一個好的方法,將其他的節點批次地設為 default namespace了。必須每一個節點都如 code 2 一般,將每一個節點,一個個設 namespace。也沒有辦法設哪一個 namespace 為 default namespace。

範例7:


最後的範例,是 prefix namespace, 請與範例2 對照

//code 7 
      XNamespace ns = "http://www.compnay.com.tw/Invoice";
      XElement el = new XElement(ns + "r",
        new XAttribute(XNamespace.Xmlns + "i", ns),
        new XElement(ns + "invoice",
          new XElement(ns + "invoiceNumber", "AB12345678"),
          new XElement(ns + "invoiceDate", DateTime.Now)
          )
          );
產生的 xml 結果如下
<i:r xmlns:i="http://www.compnay.com.tw/Invoice">
  <i:invoice>
    <i:invoiceNumber>AB12345678</i:invoiceNumber>
    <i:invoiceDate>2008-09-02T08:52:11.4240786+08:00</i:invoiceDate>
  </i:invoice>
</i:r>

2008年8月27日 星期三

Refactoring 的投影片

關於 Refactoring(重構),我一直並不了解其意義。
後來,讀了這本書後才發現,原來我也大致地遵循著相同的原則不斷的改善我的程式碼。

我將書中的第一二章節,做成了投影片,並且也改寫成 c# 3.0的版本。看來 c# 3.0 真的比 Java 來的好,程式碼又簡潔了不少。

 

投影片可於這裡下載

2008年7月25日 星期五

Could not find an implementation of the query pattern for source type 'XXX'

當我以 Linq 來查詢設定檔(app.config)的資料時,會使用下列的語法

System.Configuration.Configuration config =
       ConfigurationManager.OpenExeConfiguration(configFilePath);
      ZipConfigurationSection section = config.GetSection("zipSetting") as ZipConfigurationSection;
      var q = from i in section.Periods
              select i;

結果出現下面的錯誤

Could not find an implementation of the query pattern for source type 'A.B.PeriodsConfigurationElementCollection'.  'Select' not found.  Consider explicitly specifying the type of the range variable 'i'.   

這是因為 section.Periods 並未實作 IEnumable<T>。
這怎麼辦呢?

還好,這方面微軟已經幫我們注意到了。此時改用下面語法即可

var q = from i in section.Periods.Cast()
        select i;
Cast後的結果,就是 IEnumable<PeriodConfigurationElement>了。

同樣的方法,適用在 System.Collections 中的 Class,如 ArrayList
 

2008年7月22日 星期二

Linq to SQL 的 bug?

看來,我發現了一個 Linq to SQL 的 bug。
要還原現場,當然要 database 。

CREATE TABLE [dbo].[A](
	[AId] [int] NOT NULL,
	[AName] [char](10) NOT NULL,
 CONSTRAINT [PK_A] PRIMARY KEY CLUSTERED 
(
	[AName] ASC
),
 CONSTRAINT [IX_A] UNIQUE NONCLUSTERED 
(
	[AId] ASC
)
)
GO
INSERT [dbo].[A] ([AId], [AName]) VALUES (1, N'Charles   ')
go
CREATE TABLE [dbo].[A2](
	[AId] [int] NOT NULL,
	[AValue] [varchar](50) NOT NULL,
 CONSTRAINT [PK_A2] PRIMARY KEY CLUSTERED 
(
	[AId] ASC
)
)

GO
ALTER TABLE [dbo].[A2]  WITH CHECK ADD  CONSTRAINT [FK_A2_A] FOREIGN KEY([AId])
REFERENCES [dbo].[A] ([AId])
GO
ALTER TABLE [dbo].[A2] CHECK CONSTRAINT [FK_A2_A]
GO

注意到,A2.AId 是 reference 到 A.AId。而A.AId 是 unique key,而非 primary key。A 的 primary key 是 AName

再來,就開始拉 dbml ,如下圖

image

再 create 一個 Console Application,如下

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      TestDataContext ctx = new TestDataContext();
      A2 a2 = new A2 { AId = 1, AValue = "xxx" };
      ctx.A2s.InsertOnSubmit(a2);
      ctx.SubmitChanges();
    }
  }
}
程式錯誤訊息如下:

Type : System.InvalidCastException, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Message : Unable to cast object of type 'System.Int32' to type 'System.String'. Source : System.Data.Linq Help link : Data : System.Collections.ListDictionaryInternal TargetSite : Boolean TryCreateKeyFromValues(System.Object[], V ByRef) Stack Trace :    at System.Data.Linq.IdentityManager.StandardIdentityManager.SingleKeyManager`2.TryCreateKeyFromValues(Object[] values, V& v)    at System.Data.Linq.IdentityManager.StandardIdentityManager.IdentityCache`2.Find(Object[] keyValues)    at System.Data.Linq.IdentityManager.StandardIdentityManager.Find(MetaType type, Object[] keyValues)    at System.Data.Linq.CommonDataServices.GetCachedObject(MetaType type, Object[] keyValues)    at System.Data.Linq.ChangeProcessor.GetOtherItem(MetaAssociation assoc, Object instance)    at System.Data.Linq.ChangeProcessor.BuildEdgeMaps()    at System.Data.Linq.ChangeProcessor.SubmitChanges(ConflictMode failureMode)    at System.Data.Linq.DataContext.SubmitChanges(ConflictMode failureMode)    at System.Data.Linq.DataContext.SubmitChanges()

image

這個錯誤訊息,真是是令人不敢領教。


其實,這個錯誤,只需要修改 A 的 primary key 為 AId 即可。但實務上,卻非常有可能如上例的資料庫設計。
以發票的例子為例,常常會以發票號碼作為 primary key,而另設一個 ObjectId (int) 作為別的 table 的 reference key,因為 int 的size 較小。

Sql Script 可這裡下載, 而程式碼可這裡下載

目前我已向微軟提出這樣的 bug。這是算是我的第一次呢!

[註] 2008/08/19 MS Linq to SQL team 已經證實這個 issue ,而且會在下一次 release 修正。

2008年7月21日 星期一

Cannot implicitly convert type IEnumerable to System.Data.Linq.EntitySet. An explicit conversion exists (are you missing a cast?)

Cannot implicitly convert type IEnumerable<A> to System.Data.Linq.EntitySet. An explicit conversion exists (are you missing a cast?)

這是我開發時發生的問題。
大概是這樣的。

var q = from storeEl in buyerEl.Elements("store")
                      select new Store
              {
                StoreId = storeEl.Element("id").Value,
                TaxNo = buyerEl.Value,
                StoreName = storeEl.Element("cname").Value,
                Emails = from sub in storeEl.Elements("mails")
                                   select new StoreEmail
                                   {
                                     StoreId = storeEl.Attribute("id").Value,
                                     Email = sub.Attribute("email").Value
                                   },
              };
問題出現在 from sub in 那一段。Emails 是一個 EntitySet<Email>,而非 IEnumerable<Email>,於是發生了錯誤。
goole 了一下,找到了一個看來不是很好的方法。
  public static class LinqExtension
  {
    public static EntitySet ToEntitySet(this IEnumerable source) where T : class
    {
      var es = new EntitySet();
      es.AddRange(source);
      return es;
    }
  }

然後,將程式改為

var q = from storeEl in buyerEl.Elements("store")
                      select new Store
        {
                StoreId = storeEl.Element("id").Value,
                TaxNo = buyerEl.Value,
                StoreName = storeEl.Element("cname").Value,
                Emails = (from sub in storeEl.Elements("mails")
                                   select new StoreEmail
                                   {
                                     StoreId = storeEl.Attribute("id").Value,
                                     Email = sub.Attribute("email").Value
                                   }).ToEntitySet(),
        };

2007年3月15日 星期四

LINQ 的方法

今天看了 LINQ 的Demo 原來我之前寫的 ORM tool 早已不實用了。 微軟在 C# 3.0 加入了 LINQ,將 ORM 實作於語言層級,使的ORM 的工具更上一層樓。 然而使用者卻看不出來有 ORM 的外貌。 因此,LINQ 可以達到資料庫查詢,object (in memory) 的查詢,更進一步可達到 資料庫與 memory object 的 inner join 查詢。 硬是要得。

Share with Facebook