示例代碼下載:Linq之旅:Linq入門詳解(Linq to Objects)
本博文詳細介紹 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,語言集成查詢)。通過LINQ,我們可以使用相同API操作不同的數據源。接下來就讓我們看看LINQ是什么以及如何使用?
再此之前,需要先了解的相關技術
1. 隱式類型、匿名類型、對象初始化器
1) 隱式類型,使用var關鍵字創建,C#編譯器會根據用於初始化局部變量的初始值推斷出變量的數據類型。(不過我個人認為,能用具體類型的地方盡量不要用var關鍵字,因為這樣會讓你遺忘“被封裝類庫”方法的返回值類型--有損可讀性)
隱式類型使用限制:
a) 隱式類型只能應用於方法或者屬性內局部變量的聲明,不能使用var來定義返回值、參數的類型或類型的數據成員。
b) 使用var進行聲明的局部變量必須賦初始值,並且不能以null作為初始值。
2) 匿名類型,只是一個繼承了Object的、沒有名稱的類。C#編譯器會在編譯時自動生成名稱唯一的類。
3) 對象初始化器,提供一種非常簡潔的方式來創建對象和為對象的屬性賦值。(相關還有“集合初始化器”)
由於C#強類型語言,即我們在聲明變量時必須指定變量的具體類型。所以在創建匿名對象時,需要結合隱式類型、匿名類型、對象初始化器一起創建匿名對象。(避免類型轉換)
示例:
var person = new { name = “heyuquan” , age = 24 }
2. Lambda表達式,Func委托
1) Lambda表達式只是用更簡單的方式來書寫匿名方法,從而徹底簡化.NET委托類型的使用。
Lambda表達式在C#中的寫法是“arg-list => expr-body”,“=>”符號左邊為表達式的參數列表,右邊則是表達式體(body)。參數列表可以包含0到多個參數,參數之間使用逗號分割。
2) Func委托
Func委托,是微軟為我們預定義的常用委托,封裝一個具有:零個或多個指定類型的輸入參數並返回一個指定類型的結果值的方法。
示例:
static void Main(string[] args) { // 委托函數 Func<string, string, string> func1 = Hello; // 匿名方法 Func<string, string, string> func2 = delegate(string a, string b) { return "歡迎光臨我的博客" + Environment.NewLine + a + " " + b; }; // Lambda表達式 Func<string, string, string> func3 = (a, b) => { return "歡迎光臨我的博客" + Environment.NewLine + a + " " + b; }; // 調用Func委托 string helloStr = func2("滴答的雨", @"http://www.cnblogs.com/heyuquan/"); Console.WriteLine(helloStr); } static string Hello(string a, string b) { return "歡迎光臨我的博客" + Environment.NewLine + a + " " + b; }
3. 擴展方法
1) 擴展方法聲明在靜態類中,定義為一個靜態方法,其第一個參數需要使用this關鍵字標識,指示它所擴展的類型。
2) 擴展方法可以將方法寫入最初沒有提供該方法的類中。還可以把方法添加到實現某個接口的任何類中,這樣多個類就可以使用相同的實現代碼。(LINQ中,System.Linq.Queryable.cs和System.Linq.Enumerable.cs 正是對接口添加擴展方法)
3) 擴展方法雖定義為一個靜態方法,但其調用時不必提供定義靜態方法的類名,只需引入對應的命名空間,訪問方式同實例方法。
4) 擴展方法不能訪問它所擴展的類型的私有成員。
示例:
public static IEnumerable<TSource> MyWhere<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate) { foreach (TSource item in source) { if (predicate(item)) yield return item; } }
4. Yield迭代器,延遲計算
1) Yield迭代器
在上面定義的MyWhere擴展方法中,我們使用了yield迭代器。使我們不必“顯示”實現IEnumerable或IEnumerator接口。只需要簡單的使用 yield 關鍵字,由 JIT 編譯器幫我們編譯成實現 IEnumerable或IEnumerator 接口的對象(即:本質還是傳統遍歷,只是寫法上非常簡潔),就能使用foreach進行遍歷。
請詳看:《C#穩固基礎:傳統遍歷與迭代器》,通過這篇博文我們可以學會如何實現foreach遍歷以及foreach執行遍歷的詳細過程如下圖所示:
2) 延遲計算(Lazy evaluation)
a) 定義:來源自函數式編程,在函數式編程里,將函數作為參數來傳遞,傳遞過程中不會執行函數內部耗時的計算,直到需要這個計算結果的時候才調用,這樣就可以因為避免一些不必要的計算而改進性能。
b) Yield迭代器的延遲計算原理:JIT 編譯器會幫助我們將迭代主體編譯到IEnumerator.MoveNext()方法中。從上圖foreach的執行流程來看,迭代主體是在每次遍歷執行到 in 的時候才會調用MoveNext(),所以其迭代器耗時的指令是延遲計算的。
c) LINQ查詢的延遲計算原理:通過給LINQ擴展方法傳遞方法委托,作為yield迭代器的主體,讓遍歷執行到MoveNext()時才執行耗時的指令。
5. 表達式樹
表達式樹:表達式樹允許在運行期間建立對數據源的查詢,因為表達式樹存儲在程序集中。(后續在Linq to entities博文中與Queryable一起解說)
Language Integrated Query(LINQ,語言集成查詢)
從這幅圖中,我們可以知道LINQ包括五個部分:LINQ to Objects、LINQ to XML、LINQ to SQL、LINQ to DataSet、LINQ to Entities。
|
程序集 |
命名空間 |
描述 |
LINQ to Objects |
System.Core.dll |
System.Linq |
提供對內存中集合操作的支持 |
LINQ to XML |
System.Xml.Linq.dll |
System.Xml.Linq |
提供對XML數據源的操作的支持 |
LINQ to SQL |
System.Data.Linq.dll |
System.Data.Linq |
提供對Sql Server數據源操作的支持。(微軟已宣布不再更新,推薦使用LINQ to Entities) |
LINQ to DataSet |
System.Data.DataSetExtensions.dll |
System.Data |
提供對離線數據操作的支持。 |
LINQ to Entities |
System.Core.dll 和 System.Data.Entity.dll |
System.Linq 和System.Data.Objects |
LINQ to Entities 是 Entity Framework 的一部分並且取代 LINQ to SQL 作為在數據庫上使用 LINQ 的標准機制。(Entity Framework 是由微軟發布的開源對象-關系映射(ORM)框架,支持多種數據庫。) |
目前,還可以下載其他第三方提供程序,例如LINQ to JSON、LINQ to MySQL、LINQ to Amazon、LINQ to Flickr和LINQ to SharePoint。無論使用什么數據源,都可以通過LINQ使用相同的API進行操作。
1. 怎樣區分LINQ操作時,使用的是哪個LINQ提供程序?
LINQ提供程序的實現方案是:根據命名空間和第一個參數的類型來選擇的。實現擴展方法的類的命名空間必須是打開的,否則擴展類就不在作用域內。Eg:在LINQ to Objects中定義的 Where() 方法參數和在 LINQ to Entities中定義的 Where() 方法實現是不同。
// LINQ to Objects: public static class Enumerable { public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate); } // LINQ to Entities public static class Queryable { public static IQueryable<TSource> Where<TSource>( this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); }
2. LINQ查詢提供幾種操作語法?
LINQ查詢時有兩種語法可供選擇:查詢表達式(Query Expression)和方法語法(Fluent Syntax)。
.NET公共語言運行庫(CLR)並不具有查詢表達式的概念。所以,編譯器會在程序編譯時把查詢表達式轉換為方法語法,即對擴展方法的調用。所以使用方法語法會讓我們更加接近和了解LINQ的實現和本質,並且一些查詢只能表示為方法調用。但另一方面,查詢表達式通常會比較簡單和易讀。不管怎樣,這兩種語法是互相補充和兼容的,我們可以在一個查詢中混合使用查詢表達式和方法語法。
以下擴展方法存在對應的查詢表達式關鍵字:Where、Select、SelectMany、OrderBy、ThenBy、OrderByDescending、ThenByDescending、GroupBy、Join、GroupJoin。
LINQ查詢表達式
約束 |
LINQ查詢表達式必須以from子句開頭,以select或group子句結束。 |
|
|
關鍵字 |
功能 |
from…in… |
指定要查找的數據源以及范圍變量,多個from子句則表示從多個數據源查找數據。 注意:c#編譯器會把“復合from子句”的查詢表達式轉換為SelectMany()擴展方法。 |
join…in…on…equals… |
指定多個數據源的關聯方式 |
let |
引入用於存儲查詢表達式中子表達式結果的范圍變量。通常能達到層次感會更好,使代碼更易於閱讀。 |
orderby、descending |
指定元素的排序字段和排序方式。當有多個排序字段時,由字段順序確定主次關系,可指定升序和降序兩種排序方式 |
where |
指定元素的篩選條件。多個where子句則表示了並列條件,必須全部都滿足才能入選。每個where子句可以使用謂詞&&、||連接多個條件表達式。 |
group |
指定元素的分組字段。 |
select |
指定查詢要返回的目標數據,可以指定任何類型,甚至是匿名類型。(目前通常被指定為匿名類型) |
into |
提供一個臨時的標識符。該標識可以引用join、group和select子句的結果。 1) 直接出現在join子句之后的into關鍵字會被翻譯為GroupJoin。(into之前的查詢變量可以繼續使用) 2) select或group子句之后的into它會重新開始一個查詢,讓我們可以繼續引入where, orderby和select子句,它是對分步構建查詢表達式的一種簡寫方式。(into之前的查詢變量都不可再使用) |
書寫模版如下:
下面以 LINQ to Objects 為例,介紹LINQ中的各種查詢。
LINQ to Objects
LINQ to Objects 提供對內存中集合操作的支持,由程序集System.Core.dll中System.Linq命名空間下的Enumerable靜態類提供。
運算符圖解:
一、 示例業務背景介紹
示例參考《C#高級編程(第六版)》LINQ章節(P267 - P296),進行改編。
打開示例代碼我們看到:
1. Racer.cs 文件,定義一級方程式世界車手冠軍信息。
2. Team.cs 文件,定義一級方程式世界車隊冠軍信息。
3. Formula1.cs 文件,包含兩個重要靜態方法:(F1是"Formula One"的縮寫)
1) GetChampions():返回一組車手列表。這個列表包含了1950到2007年之間的所有一級方程式世界車手冠軍。
2) GetContructorChampions():返回一組車隊列表。這個列表包含了1985到2007年之間的所有一級方程式世界車隊冠軍,車隊冠軍是從1985年開始設立的(是由國際汽車聯合會頒發給一個賽季內最成功的一級方程式車隊的獎勵)。
二、 各種LINQ示例
1. 過濾操作符
根據條件返回匹配元素的集合IEnumerable<T>。
1) Where:根據返回bool值的Func委托參數過濾元素。
業務說明:查詢獲得車手冠軍次數大於15次且是Austria國家的一級方程式賽手
// 查詢表達式 var racer = from r in Formula1.GetChampions() where r.Wins > 15 && r.Country == "Austria" select r; // 方法語法 var racer = Formula1.GetChampions().Where(r => r.Wins > 15 && r.Country == "Austria");
2) OfType<TResult>:接收一個非泛型的IEnumerable集合,根據OfType泛型類型參數過濾元素,只返回TResult類型的元素。
業務說明:過濾object數組中的元素,返回字符串類型的數組。
object[] data = { "one", 2, 3, "four", "five", 6 }; var query = data.OfType<string>(); // "one", "four", "five"
3) Distinct:刪除序列中重復的元素。
2. 投影操作符
1) Select 將序列的每個元素經過lambda表達式處理后投影到一個新類型元素上。(與SelectMany不同在於,若單個元素投影到IEnumerable<TResult>,Select不會對多個IEnumerable<TResult>進行合並)
API:
public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source , Func<TSource, TResult> selector);
2) SelectMany
a) c#編譯器會把“復合from子句”的查詢表達式轉換為SelectMany()擴展方法。
b) 將序列的每個元素經過lambda表達式處理后投影到一個 IEnumerable<TResult>,再將多個IEnumerable<TResult>序列合並為一個返回序列IEnumerable<TResult>。
public static IEnumerable<TResult> SelectMany<TSource , TResult>(this IEnumerable<TSource> source , Func<TSource, IEnumerable<TResult>> selector); //示例: string[] fullNames = { "Anne Williams", "John Fred Smith", "Sue Green" }; IEnumerable<string> query = fullNames.SelectMany(name => name.Split()); foreach (string name in query) Console.Write(name + "|"); // Anne|Williams|John|Fred|Smith|Sue|Green| //如果使用Select,則需要雙重循環。 IEnumerable<string[]> query = fullNames.Select(name => name.Split()); foreach (string[] stringArray in query) foreach (string name in stringArray) Console.Write(name + "|"); // Anne|Williams|John|Fred|Smith|Sue|Green|
c) 將序列的每個元素經過lambda表達式處理后投影到一個 IEnumerable<TCollection>,再將多個IEnumerable<TCollection>序列合並為一個返回序列IEnumerable<TCollection>,並對其中每個元素調用結果選擇器函數。
public static IEnumerable<TResult> SelectMany<TSource, TCollection , TResult>(this IEnumerable<TSource> source , Func<TSource, IEnumerable<TCollection>> collectionSelector , Func<TSource, TCollection, TResult> resultSelector);
示例:
業務說明:(Racer類定義了一個屬性Cars,Cars是一個字符串數組。)過濾駕駛Ferrari的所有冠軍
// 查詢表達式 var ferrariDrivers = from r in Formula1.GetChampions() from c in r.Cars where c == "Ferrari" orderby r.LastName select r.FirstName + " " + r.LastName; // 方法語法 var ferrariDrivers = Formula1.GetChampions() .SelectMany( r => r.Cars, (r, c) => new { Racer = r, Car = c } ) .Where(r => r.Car == "Ferrari") .OrderBy(r => r.Racer.LastName) .Select(r => r.Racer.FirstName + " " + r.Racer.LastName);
3. 排序操作符
1) OrderBy<TSource,TKey>,OrderByDescending<TSource,TKey>:根據指定鍵按升序或降序對集合進行第一次排序,輸出IOrderedEnumerable<TSource>。
2) ThenBy<TSource,TKey>,ThenByDescending<TSource,TKey>:只會對那些在前一次排序中擁有相同鍵值的elements重新根據指定鍵按升序或降序排序。輸入IOrderedEnumerable <TSource>。
業務說明:獲取車手冠軍列表,並依次按照Country升序、LastName降序、FirstName升序進行排序。
// 查詢表達式 var racers = from r in Formula1.GetChampions() orderby r.Country, r.LastName descending, r.FirstName select r; // 方法語法 var racers = Formula1.GetChampions() .OrderBy(r => r.Country) .ThenByDescending(r => r.LastName) .ThenBy(r => r.FirstName);
3) Reverse<TSource>:反轉集合中所有元素的順序。
4. 連接操作符
先准備兩個集合,如下:(racers表示在1958到1965年間獲得車手冠軍的信息列表;teams表示在1958到1965年間獲得車隊冠軍的信息列表)
var racers = from r in Formula1.GetChampions() from y in r.Years where y > 1958 && y < 1965 select new { Year = y, Name = r.FirstName + " " + r.LastName }; var teams = Formula1.GetContructorChampions() .SelectMany(y => y.Years, (t, y) => new { Year = y, Name = t.Name }) .Where(ty => ty.Year > 1958 && ty.Year < 1965);
注意:join…on…關鍵字后的相等使用equals關鍵字。
1) Join:基於匹配鍵對兩個序列的元素進行關聯。
API:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner , Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector , Func<TOuter, TInner, TResult> resultSelector);
業務說明:返回1958到1965年間的車手冠軍和車隊冠軍信息,根據年份關聯
// 查詢表達式 var racersAndTeams = from r in racers join t in teams on r.Year equals t.Year select new { Year = r.Year, Racer = r.Name, Team = t.Name }; // 方法語法 var racersAndTeams = racers.Join(teams , r => r.Year, t => t.Year , (r, t) => new { Year = r.Year, Racer = r.Name, Team = t.Name } );
2) GroupJoin:基於鍵相等對兩個序列的元素進行關聯並對結果進行分組。常應用於返回“主鍵對象-外鍵對象集合”形式的查詢。
API:
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>( this IEnumerable<TOuter> outer, IEnumerable<TInner> inner , Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector , Func<TOuter, IEnumerable<TInner>, TResult> resultSelector);
業務說明:返回1958到1965年間的車手冠軍和車隊冠軍信息,根據年份關聯並分組
注意:直接出現在join子句之后的into關鍵字會被翻譯為GroupJoin,而在select或group子句之后的into表示繼續一個查詢。
// 查詢表達式 var racersAndTeams = from r in racers join t in teams on r.Year equals t.Year into groupTeams select new { Year = r.Year, Racer = r.Name, GroupTeams = groupTeams }; // 方法語法 var racersAndTeams = racers .GroupJoin(teams , r => r.Year, t => t.Year , (r, t) => new { Year = r.Year, Racer = r.Name, GroupTeams = t } );
3) join…on…equals…支持多個鍵關聯
可以使用匿名類型來對多個鍵值進行Join,如下所示:
from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
equals new { K1 = y.Prop3, K2 = y.Prop4 }
...
兩個匿名類型的結構必須完全一致,這樣編譯器會把它們對應到同一個實現類型,從而使連接鍵值彼此兼容。
4) Join與GroupJoin結果集對比(為了實現此業務,將1959年設置了兩個車隊冠軍)
5. 分組操作符
1) 返回值為 IEnumerable<IGrouping<TKey, TSource>> ,根據指定的鍵選擇器函數對序列中的元素進行分組。
業務說明:按城市分組,獲取每個城市的車手冠軍。
// 查詢表達式 var countries = from r in Formula1.GetChampions() group r by r.Country into g select new { Country = g.Key, Racers = g }; // 方法語法 var countries = Formula1.GetChampions() .GroupBy(r => r.Country) .Select(g => new { Country = g.Key, Racer = g });
2) 返回值為 IEnumerable<TResult>,根據指定的鍵選擇器函數對序列中的元素進行分組,並且從每個組及其鍵中創建結果值。
業務說明:按城市分組,獲取每個城市的車手冠軍。
// 方法語法 (等價上面兩種方式) var countries = Formula1.GetChampions() .GroupBy(r => r.Country, (k, g) => new { Country = k, Racer = g });
6. 量詞操作符
如果元素序列滿足指定的條件,量詞操作符就返回布爾值。
1) Any:確定序列是否包含任何元素;或確定序列中的任何元素是否都滿足條件。
2) All:確定序列中的所有元素是否滿足條件。
3) Contains:確定序列是否包含指定的元素。
// 獲取是否存在姓為“Schumacher”的車手冠軍 var hasRacer_Schumacher = Formula1.GetChampions() .Any(r => r.LastName == "Schumacher");
7. 分區操作符
添加在查詢的“最后”,返回集合的一個子集。
1) Take:從序列的開頭返回指定數量的連續元素。
2) TakeWhile:只要滿足指定的條件,就會返回序列的元素。
3) Skip:跳過序列中指定數量的元素,然后返回剩余的元素。
4) SkipWhile:只要滿足指定的條件,就跳過序列中的元素,然后返回剩余元素。
業務說明:將車手冠軍列表按每頁5個名字進行分頁。
private static void Paging() { int pageSize = 5; int numberPages = (int)Math.Ceiling( Formula1.GetChampions().Count() / (double)pageSize); for (int page = 0; page < numberPages; page++) { Console.WriteLine("Page {0}", page); var racers = ( from r in Formula1.GetChampions() orderby r.LastName select r.FirstName + " " + r.LastName ) .Skip(page * pageSize).Take(pageSize); foreach (var name in racers) { Console.WriteLine(name); } Console.WriteLine(); } }
8. 集合操作符
1) Union:並集,返回兩個序列的並集,去掉重復元素。
2) Concat:並集,返回兩個序列的並集。
3) Intersect:交集,返回兩個序列中都有的元素,即交集。
4) Except:差集,返回只出現在一個序列中的元素,即差集。
業務說明:獲取使用車型”Ferrari”和車型”Mclaren”都獲得過車手冠軍車手列表
Func<string, IEnumerable<Racer>> racersByCar = Car => from r in Formula1.GetChampions() from c in r.Cars where c == Car orderby r.LastName select r; foreach (var racer in racersByCar("Ferrari") .Intersect(racersByCar("McLaren"))) { Console.WriteLine(racer); }
5) Zip:通過使用指定的委托函數合並兩個序列,集合的總個數不變。
API:
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>( this IEnumerable<TFirst> first, IEnumerable<TSecond> second , Func<TFirst, TSecond, TResult> resultSelector);
示例:合並html開始標簽和結束標簽
string[] start = {"<html>","<head>","<body>" }; string[] end = { "</html>", "</head>", "</body>" }; var tags = start.Zip(end, (s, e) => { return s + e; }); foreach (string item in tags) { Console.WriteLine(item); }
6) SequenceEqual:判斷兩個序列是否相等,需要內容及順序都相等。
示例:
int[] arr1 = { 1, 4, 7, 9 }; int[] arr2 = { 1, 7, 9, 4 }; Console.WriteLine("排序前 是否相等:{0}" , arr1.SequenceEqual(arr2) ? "是" : "否"); // 否 Console.WriteLine(); Console.WriteLine("排序后 是否相等:{0}" , arr1.SequenceEqual(arr2.OrderBy(k => k)) ? "是" : "否"); // 是
9. 元素操作符
這些元素操作符僅返回一個元素,不是IEnumerable<TSource>。(默認值:值類型默認為0,引用類型默認為null)
1) First:返回序列中的第一個元素;如果是空序列,此方法將引發異常。
2) FirstOrDefault:返回序列中的第一個元素;如果是空序列,則返回默認值default(TSource)。
3) Last:返回序列的最后一個元素;如果是空序列,此方法將引發異常。
4) LastOrDefault:返回序列中的最后一個元素;如果是空序列,則返回默認值default(TSource)。
5) Single:返回序列的唯一元素;如果是空序列或序列包含多個元素,此方法將引發異常。
6) SingleOrDefault:返回序列中的唯一元素;如果是空序列,則返回默認值default(TSource);如果該序列包含多個元素,此方法將引發異常。
7) ElementAt:返回序列中指定索引處的元素,索引從0開始;如果索引超出范圍,此方法將引發異常。
8) ElementAtOrDefault:返回序列中指定索引處的元素,索引從0開始;如果索引超出范圍,則返回默認值default(TSource)。
業務說明:獲取冠軍數排名第三的車手冠軍
var Racer3 = Formula1.GetChampions() .OrderByDescending(r => r.Wins) .ElementAtOrDefault(2);
10. 合計操作符
1) Count:返回一個 System.Int32,表示序列中的元素的總數量。
2) LongCount:返回一個 System.Int64,表示序列中的元素的總數量。
3) Sum:計算序列中元素值的總和。
4) Max:返回序列中的最大值。
5) Min:返回序列中的最小值。
6) Average:計算序列的平均值。
7) Aggregate:對序列應用累加器函數。
Aggregate比較復雜,所以只列出Aggregate示例。
Aggregate的第一個參數是算法的種子,即初始值。第二個參數是一個表達式,用來對每個元素進行計算(委托第一個參數是累加變量,第二個參數當前項)。第三個參數是一個表達式,用來對最終結果進行數據轉換。
int[] numbers = { 1, 2, 3 }; // 1+2+3 = 6 int y = numbers.Aggregate((prod, n) => prod + n); // 0+1+2+3 = 6 int x = numbers.Aggregate(0, (prod, n) => prod + n); // (0+1+2+3)*2 = 12 int z = numbers.Aggregate(0, (prod, n) => prod + n, r => r * 2);
11. 轉換操作符
1) Cast:將非泛型的 IEnumerable 集合元素轉換為指定的泛型類型,若類型轉換失敗則拋出異常。
2) ToArray:從 IEnumerable<T> 創建一個數組。
3) ToList:從 IEnumerable<T> 創建一個 List<T>。
4) ToDictionary:根據指定的鍵選擇器函數,從 IEnumerable<T> 創建一個 Dictionary<TKey,TValue>。
5) ToLookup:根據指定的鍵選擇器函數,從 IEnumerable<T> 創建一個 System.Linq.Lookup<TKey,TElement>。
6) DefaultIfEmpty:返回指定序列的元素;如果序列為空,則返回包含類型參數的默認值的單一元素集合。
Eg:
var defaultArrCount = (new int[0]).DefaultIfEmpty().Count(); // 1
7) AsEnumerable:返回類型為 IEnumerable<T> 。用於處理LINQ to Entities操作遠程數據源與本地集合的協作。(后續在LINQ to Entities博文中會詳細解說)
ToLookup使用比較復雜,所以以ToLookup為示例。
Lookup類似於Dictionary,不過,Dictionary每個鍵只對應一個值,而Lookup則是1:n 的映射。Lookup沒有公共構造函數,而且是不可變的。在創建Lookup之后,不能添加或刪除其中的元素或鍵。(可以將ToLookup 視為GroupBy與ToDictionary的功能合體)
API:
public static ILookup<TKey, TElement> ToLookup<TSource, TKey, TElement>( this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector);
業務說明:將車手冠軍按其使用車型進行分組,並顯示使用”williams”車型的車手名字。
ILookup<string, Racer> racers = (from r in Formula1.GetChampions() from c in r.Cars select new { Car = c, Racer = r } ).ToLookup(cr => cr.Car, cr => cr.Racer); if (racers.Contains("Williams")) { foreach (var williamsRacer in racers["Williams"]) { Console.WriteLine(williamsRacer); } }
12. 生成操作符
生成操作符返回一個新的集合。(三個生成操作符不是擴展方法,而是返回序列的正常靜態方法)
1) Empty:生成一個具有指定類型參數的空序列 IEnumerable<T>。
2) Range:生成指定范圍內的整數的序列 IEnumerable<Int32>。
3) Repeat:生成包含一個重復值的序列 IEnumerable<T>。
API:
public static IEnumerable<TResult> Empty<TResult>(); public static IEnumerable<int> Range(int start, int count); public static IEnumerable<TResult> Repeat<TResult>(TResult element, int count);
三、 Linq to Objects中的延遲計算
Linq查詢的延遲計算原理:通過給LINQ擴展方法傳遞方法委托,作為yield迭代器的主體,讓遍歷執行到MoveNext()時才執行耗時的指令。
1. Linq延遲計算的注意點
IEnumerable<char> query = "Not what you might expect"; foreach (char item in "aeiou") query = query.Where(c => c != item); // 只刪除了'u'----Not what yo might expect foreach (char c in query) Console.Write(c);
我們原本的期望結果是:刪除掉字符串中所有的原音字母。但現在只刪除’u’,因為item變量是循環外部聲明的,同一個變量重復聲明更新,所以結束當前循環時,item記錄的是循環最后一個值,即結束當前循環后再執行的lambda表達式引用item值為'u'。(注意 .NET4.5 對這一實現有變動,后面有介紹)
為了解決這個問題,必須將循環變量賦值到一個在循環代碼塊內聲明的變量:
IEnumerable<char> query1 = "Not what you might expect"; foreach (char item in "aeiou") { char temp = item; query1 = query1.Where(c => c != temp); } // Nt wht y mght xpct foreach (char c in query1) Console.Write(c);
園友 @JulioZou 反饋了一個問題,上面延遲計算的示例在.NET4.5環境下兩段代碼最后輸出都是:“Nt wht y mght xpct”。現在看下原因:
從Reflector工具看對於代碼段一兩者的編譯情況是不一樣的:
即,.NET 4.5會自己生成一個內部循環變量。更詳細的分析可見《關於foreach語句在C#4.5中的改進》
所以演示延遲計算,可通過此段代碼進行說明:
IEnumerable<char> query = "Not what you might expect"; var item = 'a'; query = query.Where(c => c != item); item = 'e'; query = query.Where(c => c != item); item = 'i'; query = query.Where(c => c != item); item = 'o'; query = query.Where(c => c != item); item = 'u'; query = query.Where(c => c != item); // 只刪除了'u'----Not what yo might expect foreach (char c in query) Console.Write(c);
2. 整理Linq to Objects中運算符延遲計算特性
按字母順序整理:
具有延遲計算的運算符 |
Cast,Concat,DefaultIfEmpty,Distinct,Except,GroupBy,GroupJoin,Intersect |
立即執行的運算符 |
Aggregate,All,Any,Average,Contains,Count,ElementAt,ElementAtOrDefault |
特殊的AsEnumerable運算符,用於處理LINQ to Entities操作遠程數據源,將IQueryable遠程數據立即轉化為本地的IEnumerable集合。若AsEnumerable接收參數是IEnumerable內存集合則什么都不做。
示例代碼截圖:(已在文章開頭提供下載連接)
本博文就到此結束了,通過本博文,我們學會什么是LINQ查詢、LINQ中涉及的.NET基礎知識、LINQ中各種運算符、延遲計算等等……