LinQ實戰學習筆記(三) 序列,查詢操作符,查詢表達式,表達式樹


  • 序列
  • 延遲查詢執行
  • 查詢操作符
  • 查詢表達式
  • 表達式樹

 

(一) 序列

先上一段代碼,

這段代碼使用擴展方法實現下面的要求:

  • 取進程列表,進行過濾(取大於10M的進程)
  • 列表進行排序(按內存占用)
  • 只保留列表中指定的信息(ID,進程名)
1             var res = Process.GetProcesses()
2                 .Where(s => s.WorkingSet64 > 10 * 1024 * 1024)
3                 .OrderByDescending(s => s.WorkingSet64)
4                 .Select(s => new { ID = s.Id, Name = s.ProcessName });

 為了能清楚理解上面代碼的內部動作,我們需要介紹幾組概念.

 

1.  IEnumerable<T>接口

Process.GetProcesses()的返回值是一個Process的數組,而在C#中,所有數組對象均實現了IEnumerable<T>接口.

IEnumerable<T>接口之所以重要,是因為 上面代碼中的Where, OrderByDescending, Select 等LINQ中的標准查詢操作符都需要使用該類型的對象做為參數.

那么,上面代碼中的Where, OrderByDescending, Select 是哪里來的呢? 它們是擴展方法, 基於IEnumerable<T>接口類型的擴展方法.

在LINQ中, 術語"序列" 就是指所有實現了IEnumerable<T>接口的對象.

 

我們給出Where擴展方法的實現代碼:

 1         public static IEnumerable<TSource> Where<TSource>(
 2             this IEnumerable<TSource> source,
 3             Func<TSource, Boolean> predicate)
 4         {
 5             foreach (TSource element in source)
 6             {
 7                 if (predicate(element))
 8                     yield return element;  9             }
10         }

其第一參數中的this關鍵字就證明了它是一個擴展方法,參數類型就是IEnumerable<T>.

關鍵字yield return 就構成了一個迭代器.

我們來看一下迭代器的背景知識.

 

2. 迭代器

 從結果的角度看,迭代器與一個返回集合數據的傳統方法沒有什么區別,因為都是返回按一序列排列的值.

比如下面的代碼,就返回一個集合的值.

1         int[] OneTwoThree()
2         {
3             return new[] { 1, 2, 3 };
4         }

不過,C#中的迭代器的行為卻非常特殊.迭代器將不會一次性返回整個集合中的所有值.而是每次返回一個.這樣的設計減少了內存需求.

我們構建一個迭代器的例子,看一看這個特性.

 1   private void button2_Click(object sender, EventArgs e)
 2         {
 3             foreach (var m in OneTwoThree())
 4             {
 5                 Console.WriteLine(m);
 6             }
 7         }
 8         static IEnumerable<int> OneTwoThree()
 9         {
10             Console.WriteLine("returning 1");
11             yield return 1;
12             Console.WriteLine("returning 2");
13             yield return 2;
14             Console.WriteLine("returning 3");
15             yield return 3;
16         }

 運行結果如下圖

 

可以看到,函數OneTwoThree直到執行完最后一條語句之后才完整退出.

每次遇到yield return語句時,該方法都向調用者返回一個值.

foreach循環收到這個值后進行了處理,然后控制權又交回給迭代器方法OneTwoThree方法,由它給出下一個元素.

看起來好像兩個方法在同時運行.這也正是可以將.NET中的迭代器當作是一類輕量級的協同程序(coroutine)的原因.

 

(二) 延遲查詢執行

LINQ查詢語句非常依賴於延遲查詢執行機制,惹是缺少了這個機制,LINQ的執行效率將會大大降低.

來看一段代碼:

 1  static double Square(double n)
 2         {
 3             Console.WriteLine("計算 Square(" + n + ")...");
 4             return Math.Pow(n, 2);
 5         }
 6         private void button3_Click(object sender, EventArgs e)
 7         {
 8             int[] numbers = { 1, 2, 3 };
 9             var res = from n in numbers
10                       select Square(n);
11             foreach (var m in res)
12                 Console.WriteLine(m);
13         }

運行結果如下:

結果可以看到,明顯該查詢並不是一次性執行完畢的.只有在迭代到某一項時,查詢才開始求出這一項的值.

這就是所謂的查詢延遲執行的機制在發揮作用.

我們來討論一下其中的原理:

var res = from n in numbers
                      select Square(n);

上面的LINQ查詢在編譯后,實際上變成了這樣的:

 IEnumerable<double> res = Enumerable.Select<int, double>(numbers, n => Square(n));

也就是LINQ查詢轉為一系列擴展方法的調用,其中的Enumerable.Select方法正是一個迭代器--這也就是其實現了延遲執行的原理.

如果我們需要查詢強制立即執行,可以通過調用ToList方法來實現.

我們把上面的代碼改動一下:

1   private void button4_Click(object sender, EventArgs e)
2         {
3             int[] numbers = { 1, 2, 3 };
4             var res = from n in numbers
5                       select Square(n);
6             foreach (var m in res.ToList())
7                 Console.WriteLine(m);
8         }

可以看到結果就不同了:

可以見到是先得到查詢的結果,最后才把結果迭代輸出的.

 

(三) 查詢操作符

上面代碼所示的 Where,OrderByDescending, Select這些擴展方法 包含有共同的特性:

  • 操作於可被迭代的集合對象之上
  • 允許管道形式的數據處理
  • 依賴於延遲執行

正是上面這些特征讓這些擴展方法能用於編寫查詢.因此這些擴展方法也稱為"查詢操作符"

查詢操作符是LINQ的核心,甚至比語言方面的特性(比如查詢表達式)更重要.

 下圖是按照操作類型分組的標准查詢操作符:

 

(四) 查詢表達式

開往篇的程序是使用查詢操作符實現的.再次引用一下:

1   var res = Process.GetProcesses()
2                 .Where(s => s.WorkingSet64 > 10 * 1024 * 1024)
3                 .OrderByDescending(s => s.WorkingSet64)
4                 .Select(s => new { ID = s.Id, Name = s.ProcessName });

另一種語法則讓LINQ查詢更像是SQL的查詢語句.

1   var res = from s in Process.GetProcesses()
2                       where s.WorkingSet64 > 10 * 1024 * 1024
3                       orderby s.WorkingSet64 descending
4                       select new { ID = s.Id, Name = s.ProcessName };

上面的這種寫法就叫做查詢表達式,或者查詢式語法.

這兩種代碼的寫法從語義上來講是完全相同的,而且實現的功能也一致.

查詢表達式是由C#語言提供的語言級特性,一種語法糖,這種語法類似於SQL,它可以操作於一個或者多個數據源之上,並為這些數據源應用若干個標准或者自定義的查詢操作符.在上面的示例代碼中,使用了3個標准的查詢操作符:Where, orderByDescending以及Select.

在使用查詢表達式語法時,編譯器會自動將其轉化為對標准查詢操作符的調用.

查詢表達式存在的最主要意義在於,它能夠大大簡化查詢所需要的代碼,並提高查詢語句的可讀性(類似熟悉的SQL).

 

下圖是查詢表達式的完整語法:

標准查詢操作符與查詢表達式的關系,見下表所示:

通過上表可以看到,不是每一個操作符都有與之對應的C#關鍵字.在前面那個簡單的查詢中,我們當然完全可以使用語言所提供的關鍵字實現.不過對於那些較為復雜的查詢來說,我們將不得不直接調用查詢操作符完成.

因為查詢表達式最終都會被編譯成各個標准操作符的調用.因此如果願意的話,完全可以只用查詢操作符編寫所有查詢語句,根本不理會查詢表達式的存在.

 

(五) 表達式樹

 Lambda表達式在前面提到過它的主要作用之一是實現匿名委托.如下例:

Func<int,bool> isOdd=i=>(i & 1)==1;

但是,Lambda表達式也能夠以數據的形式使用,這正是表達式樹所要求的.

當把代碼改成下面這樣時,我們就無法以委托的形式來使用isOdd了.因為在這里isOdd並不是委托,而是個表達式樹.

Expression<Func<int,bool>> isOdd =i => (i & 1) ==1;

編譯器不會把上面的Lambda表達式換成IL代碼,而是會構造出一個樹狀的,用來表示該表達式的對象.

但是需要注意的是:只有那些帶有表達式體的Lambda表達式才能夠用於表達式樹.主體部分是語句的Lambda表達式則沒有類似的支持.

例如,下面第1行代碼可以用來生成一顆表達式樹,因為其帶有表達式體.

第2行的就不能,因為它的主體部分是一個語句.

1 Expression<Func<Object, Object>> identity = o=>o;
2 Expression<Func<Object, Object>> identity = o=>{ return o;};

當編譯器看到某個Lambda表達式賦值給類型為Expression<>的變量時,就會將其編譯成一系列工廠方法的調用,這些工廠方法將在程序運行時動態地構造出表達式樹.

下面就是編譯器為上述表達式自動生成的代碼:

1   ParameterExpression i = Expression.Parameter(typeof(int), "i");
2             Expression<Func<int, bool>> isOdd =
3                 Expression.Lambda<Func<int, bool>>(
4                 Expression.Equal(
5                 Expression.And(
6                 i,
7                 Expression.Constant(1, typeof(int))),
8                 Expression.Constant(1, typeof(int))),
9                 new ParameterExpression[] { i });

上面的代碼是可以手工編寫的,但是編譯器可以代勞.

表達式樹將在程序運行中動態構造,不過一旦構造完成,則無法被再次修改.

表達式樹在第5章中用以創建動態查詢這種高級場景上得到了應用.

上面的表達式樹,在內存中以樹的數據結構存儲,它表示解析了后的Lambda表達式,如下圖:

上面的表達式樹,還可以"逆向"編譯成委托方法:

1    Expression<Func<int, bool>> isOddExpression = i => (i & 1) == 1;
2             Func<int, bool> isOddCompiledExpression = isOddExpression.Compile();

這時候,上面的isOddCompiledExpression和下面的委托isOdd就完全相同了,它們生成的IL代碼就沒有任何區別了.

Func<int,bool> isOdd=i=>(i & 1)==1;

為什么要使用表達式樹呢?

實際上,表達式樹就是一顆抽象語法樹(AST).抽象語法樹用來表示一段經過解析的代碼.在上面例子中,這顆樹就是C#對於Lambda表達式解析后的結果.這樣做的目的是便於其它代碼對該表達式樹進行分析,並執行一些必要的操作.

表達式樹可以在運行時傳遞給其它的工具,隨后這些工具可以根據該樹開始執行查詢,或者是將其轉化為其它形式的代碼,例如LINQ to SQL中的SQL語句.

 

最后我們來看看表達式樹執行延遲查詢執行的方法:

引用之前LINQ to SQL例子中的代碼:

1  var contacts =
2               from contact in db.GetTable<HelloLinqToSql.Contact>()
3               where contact.City == "武漢"
4               select contact;
5 
6             Console.WriteLine("查找在武漢的聯系人"+Environment.NewLine);
7             foreach (var contact in contacts)
8                 Console.WriteLine("聯系人: " + contact.Name.Trim()+" ID:"+contact.ContactID);

我們知道使用IEnumerable<T>迭代器可以產生延遲查詢的行為,在上面代碼中 contacts變量的類型不是IEnumerable<T>,而是IQueryable<Contact>.

處理IQueryable<Contact>數據與處理序列完全不同.IQueryable<Contact>的實例將要接受一棵表達式樹,由些分析出下一步將要進行的操作.

在上面代碼中,一旦我們開始遍歷contacts變量,那么程序就會開始分析其中包含的表達式樹,隨后生成SQL語句並執行,最后該SQL語句的返回結果以Contact對象集合的形式給出.

與基於IEnumerable<T>的序列相比, IQueryable<Contact>更加強大,因為程序可以根據表達式樹的分析結果進行智能地處理.通過查看某個查詢的表達式樹,編譯器即可智能地進行推斷並進行大量的優化.IQueryable<Contact>和表達式樹的組合將給我們帶來更強大的可定制能力.

 

原創文章,出自"博客園, 豬悟能'S博客" : http://www.cnblogs.com/hackpig/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM