走進 LINQ 的世界
序
在此之前曾發表過三篇關於 LINQ 的隨筆:
進階:《LINQ 標准查詢操作概述》(強烈推薦)
技巧:《Linq To Objects - 如何操作字符串》 和 《Linq To Objects - 如何操作文件目錄》
現在,自己打算再整理一篇關於 LINQ 入門的隨筆,也是圖文並茂的哦。
目錄
LINQ 簡介
語言集成查詢 (LINQ) 是 Visual Studio 2008 和 .NET Framework 3.5 版中引入的一項創新功能。
傳統上,針對數據的查詢都是以簡單的字符串表示,而沒有編譯時類型檢查或 IntelliSense 支持。此外,您還必須針對以下各種數據源學習一種不同的查詢語言:SQL 數據庫、XML 文檔、各種 Web 服務等等。 通過LINQ, 您可以使用語言關鍵字和熟悉的運算符針對強類型化對象集合編寫查詢。
一、介紹 LINQ 查詢
查詢是一種從數據源檢索數據的表達式。隨着時間的推移,人們已經為各種數據源開發了不同的語言;例如,用於關系數據庫的 SQL 和用於 XML 的 XQuery。因此,開發人員不得不針對他們必須支持的每種數據源或數據格式而學習新的查詢語言。LINQ 通過提供一種跨數據源和數據格式使用數據的一致模型,簡化了這一情況。在 LINQ 查詢中,始終會用到對象。可以使用相同的編碼模式來查詢和轉換 XML 文檔、SQL 數據庫、ADO.NET 數據集、.NET 集合中的數據以及對其有 LINQ 提供程序可用的任何其他格式的數據。
1.1 查詢操作的三個部分
操作三部曲:①取數據源 ②創建查詢 ③執行查詢

1 internal class Program 2 { 3 private static void Main(string[] args) 4 { 5 //1.獲取數據源 6 var nums = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; 7 8 //2.創建查詢 9 var numQuery = 10 from num in nums 11 where (num % 2) == 0 12 select num; 13 14 //3.執行查詢 15 foreach (var num in numQuery) 16 { 17 Console.WriteLine("{0}", num); 18 } 19 } 20 }
下圖顯示了完整的查詢操作。在 LINQ 中,查詢的執行與查詢本身截然不同;換句話說,查詢本身指的是只創建查詢變量,不檢索任何數據。
1.2 數據源
在上一個示例中,由於數據源是數組,因此它隱式支持泛型 IEnumerable<T> 接口。支持 IEnumerable<T> 或派生接口(如泛型 IQueryable<T>)的類型稱為可查詢類型。
//從 XML 中創建數據源 //using System.Xml.Linq; var contacts = XElement.Load(@"c:\xxx.xml");
在 LINQ to SQL 中,首先需要創建對象關系映射。 針對這些對象編寫查詢,然后由 LINQ to SQL 在運行時處理與數據庫的通信。

1 var db = new Northwnd(@"c:\northwnd.mdf"); 2 3 //查詢在倫敦的客戶 4 var custQuery = 5 from cust in db.Customers 6 where cust.City == "London" 7 select cust;
1.3 查詢
查詢指定要從數據源中檢索的信息。 查詢還可以指定在返回這些信息之前如何對其進行排序、分組和結構化。 查詢存儲在查詢變量中,並用查詢表達式進行初始化。
1.4 查詢執行
1.延遲執行
如前所述,查詢變量本身只是存儲查詢命令。 實際的查詢執行會延遲到在 foreach 語句中循環訪問查詢變量時發生。 此概念稱為“延遲執行”。
2.強制立即執行
對一系列源元素執行聚合函數的查詢必須首先循環訪問這些元素。Count、Max、Average 和 First 就屬於此類查詢。由於查詢本身必須使用 foreach 以便返回結果,因此這些查詢在執行時不使用顯式 foreach 語句。另外還要注意,這些類型的查詢返回單個值,而不是 IEnumerable 集合。

1 var numbers = new int[7] { 0, 1, 2, 3, 4, 5, 6 }; 2 3 var evenNumQuery = 4 from num in numbers 5 where (num % 2) == 0 6 select num; 7 8 var evenNumCount = evenNumQuery.Count();
若要強制立即執行任意查詢並緩存其結果,可以調用 ToList<TSource> 或 ToArray<TSource> 方法。

1 var numQuery2 = 2 (from num in numbers 3 where (num % 2) == 0 4 select num).ToList(); 5 6 var numQuery3 = 7 (from num in numbers 8 where (num % 2) == 0 9 select num).ToArray();
此外,還可以通過在緊跟查詢表達式之后的位置放置一個 foreach 循環來強制執行查詢。但是,通過調用 ToList 或 ToArray,也可以將所有數據緩存在單個集合對象中。
二、基本 LINQ 查詢操作
2.1 獲取數據源:from
在 LINQ 查詢中,第一步是指定數據源。像在大多數編程語言中一樣,必須先聲明變量,才能使用它。在 LINQ 查詢中,最先使用 from 子句的目的是引入數據源和范圍變量。

1 //queryAllCustomers 是 IEnumerable<Cutsomer> 類型 2 //數據源 (customers) 和范圍變量 (cust) 3 var queryAllCustomers = from cust in customers 4 select cust;
范圍變量類似於 foreach 循環中的迭代變量,但在查詢表達式中,實際上不發生迭代。執行查詢時,范圍變量將用作對 customers 中的每個后續元素的引用。因為編譯器可以推斷 cust 的類型,所以您不必顯式指定此類型。
2.2 篩選:where
也許最常用的查詢操作是應用布爾表達式形式的篩選器。此篩選器使查詢只返回那些表達式結果為 true 的元素。使用 where 子句生成結果。實際上,篩選器指定從源序列中排除哪些元素。

1 var queryLondonCustomers = from cust in customers 2 where cust.City = "London" 3 select cust;
您可以使用熟悉的 C# 邏輯 AND(&&)和 OR(||) 運算符來根據需要在 where 子句中應用任意數量的篩選表達式。

where cust.City = "London" && cust.Name = "Devon"

where cust.City = "London" || cust.Name = "Paris"
2.3 排序:orderby
通常可以很方便地將返回的數據進行排序。orderby 子句將使返回的序列中的元素按照被排序的類型的默認比較器進行排序。

1 var queryLondonCustomers = from cust in customers 2 where cust.City = "London" 3 orderby cust.Name descending 4 select cust;
因為 Name 是一個字符串,所以默認比較器執行從 A 到 Z 的字母排序。若要按相反順序(從 Z 到 A)對結果進行排序,請使用 orderby…descending 子句。
2.4 分組:group
使用 group 子句,您可以按指定的鍵分組結果。

1 var queryLondonCustomers = from cust in customers 2 group cust by cust.City; 3 4 foreach (var queryLondonCustomer in queryLondonCustomers) 5 { 6 Console.WriteLine(queryLondonCustomer.Key); 7 foreach (var cust in queryLondonCustomer) 8 { 9 Console.WriteLine(cust.Name); 10 } 11 }
在本例中,cust.City 是鍵。
在使用 group 子句結束查詢時,結果采用列表的列表形式。列表中的每個元素是一個具有 Key 成員及根據該鍵分組的元素列表的對象。在循環訪問生成組序列的查詢時,您必須使用嵌套的 foreach 循環。外部循環用於循環訪問每個組,內部循環用於循環訪問每個組的成員。
如果您必須引用組操作的結果,可以使用 into 關鍵字來創建可進一步查詢的標識符。

1 //custQuery 是 IEnumable<IGrouping<string, Customer>> 類型 2 var custQuery = from cust in customers 3 group cust by cust.City 4 into custGroup 5 where custGroup.Count() > 2 6 orderby custGroup.Key 7 select custGroup;
2.5 聯接:join
聯接運算創建數據源中沒有顯式建模的序列之間的關聯。例如,您可以執行聯接來查找位於同一地點的所有客戶和經銷商。在 LINQ 中,join 子句始終針對對象集合而非直接針對數據庫表運行。

1 var innerJoinQuery = from cust in customers 2 join dist in distributors on cust.City equals dist.City 3 select new {CustomerName = cust.Name, DistributorName = dist.Name};
在 LINQ 中,join 子句始終針對對象集合而非直接針對數據庫表運行。
在 LINQ 中,您不必像在 SQL 中那樣頻繁使用 join,因為 LINQ 中的外鍵在對象模型中表示為包含項集合的屬性。

from order in Customer.Orders...
2.6 選擇(投影):select
select 子句生成查詢結果並指定每個返回的元素的“形狀”或類型。
例如,您可以指定結果包含的是整個 Customer 對象、僅一個成員、成員的子集,還是某個基於計算或新對象創建的完全不同的結果類型。當 select 子句生成除源元素副本以外的內容時,該操作稱為“投影”。
三、使用 LINQ 進行數據轉換
語言集成查詢 (LINQ) 不僅可用於檢索數據,而且還是一個功能強大的數據轉換工具。通過使用 LINQ 查詢,您可以將源序列用作輸入,並采用多種方式修改它以創建新的輸出序列。您可以通過排序和分組來修改該序列,而不必修改元素本身。但是,LINQ 查詢的最強大的功能是能夠創建新類型。這一功能在 select 子句中實現。 例如,可以執行下列任務:
3.1 將多個輸入聯接到一個輸出序列

1 class Student 2 { 3 public string Name { get; set; } 4 5 public int Age { get; set; } 6 7 public string City { get; set; } 8 9 public List<int> Scores { get; set; } 10 } 11 12 class Teacher 13 { 14 public int Id { get; set; } 15 16 public string Name { get; set; } 17 18 public int Age { get; set; } 19 20 public string City { get; set; } 21 22 }

1 internal class Program 2 { 3 private static void Main(string[] args) 4 { 5 //創建第一個數據源 6 var students = new List<Student>() 7 { 8 new Student() 9 { 10 Age = 23, 11 City = "廣州", 12 Name = "小C", 13 Scores = new List<int>(){85,88,83,97} 14 }, 15 new Student() 16 { 17 Age = 18, 18 City = "廣西", 19 Name = "小明", 20 Scores = new List<int>(){86,78,85,90} 21 }, 22 new Student() 23 { 24 Age = 33, 25 City = "夢里", 26 Name = "小叄", 27 Scores = new List<int>(){86,68,73,97} 28 } 29 }; 30 31 //創建第二個數據源 32 var teachers = new List<Teacher>() 33 { 34 new Teacher() 35 { 36 Age = 35, 37 City = "夢里", 38 Name = "啵哆" 39 }, 40 new Teacher() 41 { 42 Age = 28, 43 City = "雲南", 44 Name = "小紅" 45 }, 46 new Teacher() 47 { 48 Age = 38, 49 City = "河南", 50 Name = "麗麗" 51 } 52 }; 53 54 //創建查詢 55 var peopleInDreams = (from student in students 56 where student.City == "夢里" 57 select student.Name) 58 .Concat(from teacher in teachers 59 where teacher.City == "夢里" 60 select teacher.Name); 61 62 //執行查詢 63 foreach (var person in peopleInDreams) 64 { 65 Console.WriteLine(person); 66 } 67 68 Console.Read(); 69 } 70 }
3.2 選擇各個源元素的子集
1. 若要只選擇源元素的一個成員,請使用點運算。
1 var query = from cust in Customers 2 select cust.City;
2. 若要創建包含源元素的多個屬性的元素,可以使用具有命名對象或匿名類型的對象初始值設定項。
1 var query = from cust in Customer 2 select new {Name = cust.Name, City = cust.City};
3.3 將內存中的對象轉換為 XML

1 //創建數據源 2 var students = new List<Student>() 3 { 4 new Student() 5 { 6 Age = 18, 7 Name = "小A", 8 Scores = new List<int>() {88,85,74,66 } 9 }, 10 new Student() 11 { 12 Age = 35, 13 Name = "小B", 14 Scores = new List<int>() {88,85,74,66 } 15 }, 16 new Student() 17 { 18 Age = 28, 19 Name = "小啥", 20 Scores = new List<int>() {88,85,74,66 } 21 } 22 }; 23 24 //創建查詢 25 var studentsToXml = new XElement("Root", 26 from student in students 27 let x = $"{student.Scores[0]},{student.Scores[1]},{student.Scores[2]},{student.Scores[3]}" 28 select new XElement("student", 29 new XElement("Name", student.Name), 30 new XElement("Age", student.Age), 31 new XElement("Scores", x)) 32 ); 33 34 //執行查詢 35 Console.WriteLine(studentsToXml);
3.4 對源元素執行操作
輸出序列可能不包含源序列的任何元素或元素屬性。輸出可能是通過將源元素用作輸入參數計算出的值的序列。

1 //數據源 2 double[] radii = {1, 2, 3}; 3 4 //創建查詢 5 var query = from radius in radii 6 select $"{radius * radius * 3.14}"; 7 8 //執行查詢 9 foreach (var i in query) 10 { 11 Console.WriteLine(i); 12 }
【備注】$"{radius * radius * 3.14}" 相當於 string.Format("{0}",radius * radius * 3.14),這里采用的是 C# 6.0 的語法。
四、LINQ 查詢操作的類型關系
LINQ 查詢操作在數據源、查詢本身及查詢執行中是強類型的。查詢中變量的類型必須與數據源中元素的類型和 foreach 語句中迭代變量的類型兼容。強類型可以保證在編譯時捕獲類型錯誤,以便及時改正。
4.1 不轉換源數據的查詢
下圖演示不對數據執行轉換的 LINQ to Objects 查詢操作。源包含一個字符串序列,查詢輸出也是一個字符串序列。
①數據源的類型參數決定范圍變量的類型。
②選擇的對象的類型決定查詢變量的類型。此處的 name 為一個字符串。因此,查詢變量是一個 IEnumerable<字符串>。
③在 foreach 語句中循環訪問查詢變量。因為查詢變量是一個字符串序列,所以迭代變量也是一個字符串。
4.2 轉換源數據的查詢
下圖演示對數據執行簡單轉換的 LINQ to SQL 查詢操作。查詢將一個 Customer 對象序列用作輸入,並只選擇結果中的 Name 屬性。因為 Name 是一個字符串,所以查詢生成一個字符串序列作為輸出。
①數據源的類型參數決定范圍變量的類型。
②select 語句返回 Name 屬性,而非完整的 Customer 對象。因為 Name 是一個字符串,所以 custNameQuery 的類型參數是 string,而非Customer。
③因為 custNameQuery 是一個字符串序列,所以 foreach 循環的迭代變量也必須是 string。
下圖演示另一種轉換。select 語句返回只捕獲原始 Customer 對象的兩個成員的匿名類型。
①數據源的類型參數始終為查詢中的范圍變量的類型。
②因為 select 語句生成匿名類型,所以必須使用 var 隱式類型化查詢變量。
③因為查詢變量的類型是隱式的,所以 foreach 循環中的迭代變量也必須是隱式的。
4.3 讓編譯器推斷類型信息
您也可以使用關鍵字 var,可用於查詢操作中的任何局部變量。但是,編譯器為查詢操作中的各個變量提供強類型。
五、LINQ 中的查詢語法和方法語法
我們編寫的 LINQ 查詢語法,在編譯代碼時,CLR 會將查詢語法轉換為方法語法。這些方法調用標准查詢運算符的名稱類似 Where、Select、GroupBy、Join、Max和 Average,我們也是可以直接使用這些方法語法的。
查詢語法和方法語法語義相同,但是,許多人員發現查詢語法更簡單、更易於閱讀。某些查詢必須表示為方法調用。例如,必須使用方法調用表示檢索元素的數量與指定的條件的查詢。還必須使用方法需要檢索元素的最大值在源序列的查詢。System.Linq 命名空間中的標准查詢運算符的參考文檔通常使用方法語法。
5.1 標准查詢運算符擴展方法

1 static void Main(string[] args) 2 { 3 var nums = new int[4] { 1, 2, 3, 4 }; 4 5 //創建查詢表達式 6 var qureyNums = from n in nums 7 where n % 2 == 0 8 orderby n descending 9 select n; 10 11 Console.WriteLine("qureyNums:"); 12 foreach (var n in qureyNums) 13 { 14 Console.WriteLine(n); 15 } 16 17 //使用方法進行查詢 18 var queryNums2 = nums.Where(n => n % 2 == 0).OrderByDescending(n => n); 19 20 Console.WriteLine("qureyNums2:"); 21 foreach (var n in queryNums2) 22 { 23 Console.WriteLine(n); 24 } 25 26 Console.Read(); 27 }
兩個示例的輸出是相同的。您可以看到兩種形式的查詢變量的類型是相同的:IEnumerable<T>。
若要了解基於方法的查詢,讓我們進一步地分析它。注意,在表達式的右側,where 子句現在表示為對 numbers 對象的實例方法,在您重新調用該對象時其類型為 IEnumerable<int>。如果您熟悉泛型 IEnumerable<T> 接口,那么您就會了解,它不具有 Where 方法。但是,如果您在 Visual Studio IDE 中調用 IntelliSense 完成列表,那么您不僅將看到 Where 方法,而且還會看到許多其他方法,如 Select、SelectMany、Join 和Orderby。下面是所有標准查詢運算符。
盡管看起來 IEnumerable<T> 似乎已被重新定義以包括這些附加方法,但事實上並非如此。這些標准查詢運算符都是作為“擴展方法”實現的。
5.2 Lambda 表達式
在前面的示例中,通知該條件表達式 (num % 2 == 0) 是作為內聯參數。Where 方法:Where(num => num % 2 == 0) 此內聯表達式稱為 lambda 表達式。將代碼編寫為匿名方法或泛型委托或表達式樹是一種便捷的方法,否則編寫起來就要麻煩得多。=> 是 lambda 運算符,可讀為“goes to”。運算符左側的 num 是輸入變量,與查詢表達式中的 num 相對應。編譯器可推斷 num 的類型,因為它了解 numbers 是泛型 IEnumerable<T> 類型。lambda 表達式與查詢語法中的表達式或任何其他 C# 表達式或語句中的表達式相同;它可以包括方法調用和其他復雜邏輯。“返回值”就是表達式結果。
5.3 查詢的組合性
在上面的代碼示例中,請注意 OrderBy 方法是通過在對 Where 的調用中使用點運算符來調用的。Where 生成篩選序列,然后 Orderby 通過對該序列排序來對它進行操作。因為查詢會返回 IEnumerable,所以您可通過將方法調用鏈接在一起,在方法語法中將這些查詢組合起來。這就是在您通過使用查詢語法編寫查詢時編譯器在后台所執行的操作。並且由於查詢變量不存儲查詢的結果,因此您可以隨時修改它或將它用作新查詢的基礎,即使在執行它后。
傳送門
入門:《走進 LINQ 的世界》
進階:《LINQ 標准查詢操作概述》(強烈推薦)
技巧:《Linq To Objects - 如何操作字符串》 和 《Linq To Objects - 如何操作文件目錄》
本文首聯:http://www.cnblogs.com/liqingwen/p/5832322.html
【參考】https://msdn.microsoft.com/zh-cn/library/bb397897(v=vs.100).aspx 等
【來源】本文引用部分微軟官方文檔的圖片