[深入学习C#]LINQ查询表达式详解(1)——基本语法、使用扩展方法和Lambda表达式简化LINQ查询
此文章非原創,轉載自詩人江湖老,原文地址
在Git上下載源碼
在工程中我們少不了要定義類或者結構去儲存數據,這些數據將被臨時地儲存在內存中,現在我們想要對其完成一些類似于查找、過濾等等常見的任務的時候,我們該如何去做呢?
我們可以自己寫代碼去對集合中的每個對象進行遍歷,檢查變量的每個字段看其是否滿足條件。這樣的故事已經發生太多次了,微軟怎么可能容忍在C#里發生如此弱智的事情呢?于是,C#的設計者決定在C#中集成查詢的語法,以最大限度地減少程序員書寫類似代碼的情況。
這也就是我們說的LINQ(Language Intergated Query)也就是語言集成查詢,我們可以使用同樣的語法訪問不同的數據源。
為什么要使用LINQ?
幾乎所有的應用程序都需要對數據進行處理,大部分的程序都通過自定義的邏輯來完成這些操作。這樣做的弊端之意就是,代碼邏輯將會跟它處理的數據的結構緊密耦合在一起,如果數據結構發生了改變,也許將會帶來海量的代碼改動。
為了解決這個問題,C#將這些處理數據的代碼都抽象出來,提供給了廣大開發者。這就是LINQ。
LINQ的語法類似于關系和分層查詢語言(SQL和XQuery),我們可以在不更改查詢代碼的情況下對數據結構進行更改。LINQ比之SQL要更加靈活,可以處理更廣泛的邏輯數據結構。當然,這些數據結構都需要實現了IEnumerable或者IEnumerable< T >接口,才可以進行LINQ查詢。
LINQ查詢表達式語法詳解
表達式基礎語法
LINQ查詢表達式以from子句開始,以select或者group子句結束。在這兩個子句之間可以跟零個或者多個from、let、where、join或者orderby子句。
每個from子句都是一個生成器,該生成器將引入一個包括序列(Sequence)的元素的范圍變量(range variable)。每個let子句都會引入一個范圍變量,以表示通過前一個范圍變量計算的值。每個where子句都是一個篩選器,用于從結果中排除項。每個join子句都將指定的源序列鍵與其他序列的鍵進行比較,以產生匹配對。每個orderby子句都會根據指定的條件對各項進行重新排序。而最后的select或者group子句根據范圍變量來指定結果的表現形式。最后可以使用into子句來連接查詢,將某一查詢結果的視為后續查詢的生成器。
標準查詢操作符
在了解LINQ查詢表達式之前,怎么能不了解下它的查詢操作符呢?下面列表列出了LINQ定義的標準查詢操作符。
| where OfType<TResult> | 篩選操作符定義了返回元素的條件。在Where查詢操作符中,可以使用謂詞,例如Lambda表達式定義的謂詞,來返回布爾值。OfType<TResult>根據類型篩選元素,只返回TResult的類型元素 |
| Select 和SelectMany | 投射操作符用于把對象轉換為另一個類型的新對象。Select和SelectMany定義了根據選擇器函數選擇結果值的投射。 |
| OrderBy、ThenBy 、OrderByDescending 、ThenByDescending 、Reverse | 排序操作符改變所返回的元素的順序。OrderBy按升序排列,OrderByDescending按降序排列。如果第一次排序結果很類似,就可以使用ThenBy和ThenByDescending操作符進行第二次排序。Reverse反轉集合中的元素順序。 |
| GroupBy、ToLookUp | 組合運算符把數據放在組里面。GroupBy操作符組合有公共鍵的元素。ToLookUp通過創建一個一對多的字典,來組合元素。 |
| Join、GroupJoin | 鏈接運算符用于合并不直接相關的集合。使用Join操作符,可以根絕鍵選擇器函數連接兩個集合,這類似于SQL中的Join。GroupJoin操作符連接兩個集合,組合其結果。 |
| Any、All、Contains | 如果元素序列滿足指定的條件,兩次操作符就返回布爾值。Any、ALll和Contains都是限定符操作符。Any確定集合中是否有確定滿足謂詞函數的元素。ALll確定集合中的所有元素是否都滿足謂詞函數。Contains檢查某個元素是否在集合中。這些操作符都返回一個布爾值。 |
| Take、Skip、TakeWhile、SkipWhile | 分區操作符返回集合的一個子集,Take、Skip、TakeWhile、SkipWhile都是分區操作符。使用它們可以得到部分結果,使用Take必須指定要從集合中提取的元素個數;Skip跳過指定個數的元素,提取其它元素;TakeWhile提取條件為真的元素。 |
| Distinct、Union、Intersect、Except、Zip | Set操作符返回一個集合。Distinct從集合中刪除重復的元素,除了Distinct之外,其它的Set操作符都需要兩個集合。Union返回出現在其中一個集合中的唯一元素。Intersect返回兩個集合中都有的元素。Except返回值出現在一個集合中的元素。Zip是.NET 4新增的,它把兩個集合合并為一個。 |
| First、FirstOrDefault、Last、LastOrDefault、ElementAt、ElementAtOrDefault、Single、SingleOrDefault | 這些元素操作符僅返回一個元素。First返回第一個滿足條件的元素。FirstOrDefault類似于First,單如果沒有找到滿足條件的元素,就返回類型的默認值。Last返回最后一個滿足條件的元素。ElementAt指定了要返回的元素的位置。Single只返回一個滿足條件的元素。如果有多個元素都滿足條件,就拋出一個異常。 |
| Count、Sum、Min、Max、Average、Aggregate | 聚合操作符計算集合的一個值。利用這些聚合操作符,可以計算所有值的總和、所有元素的個數、值最大和最小的元素,以及平均值等等。 |
| ToArray、ToEnumerable、ToList、ToDictionary、Cast<TRsult> | 這些轉換操作符將集合轉換為數組:IEnumerable、IList、IDictionary等。 |
| Empty、Range、Repeat | 這些生成操作符返回一個心機和。使用Empty時集合是空的;Range返回一系列數字;Repeat返回一個始終重復一個值的集合。 |
設置案例背景
假設我們有4個類,Customer,Order,Detail,Product,它們的定義如下:
| CustomerID | int |
| Country | string |
| Name | string |
| City | string |
| Orders | List<Order> |
| OrderID | int |
| CustomerID | int |
| Total | int |
| OrderDate | DateTime |
| Details | List<Detail> |
| DetailID | int |
| OrderID | int |
| UnitPrice | double |
| Quantity | double |
| ProductID | int |
| ProductID | int |
| ProductName | string |
假設現在它們各自有一個List集合,分別為Customers,Orders,Details,Products。我們在這基礎之上來一步步闡述LINQ查詢表達式。
customers數據:
| 0 | 北京 | 中國 | 小米 | orders.FindAll(c => c.CustomerID == 0) |
| 1 | 首爾 | 韓國 | 三星 | orders.FindAll(c => c.CustomerID == 1) |
| 2 | 加州 | 美國 | 蘋果 | orders.FindAll(c => c.CustomerID == 2) |
| 3 | 臺北 | 中國 | HTC | orders.FindAll(c => c.CustomerID == 3) |
| 4 | 珠海 | 中國 | 魅族 | orders.FindAll(c => c.CustomerID == 4) |
| 5 | 北京 | 中國 | 華為 | orders.FindAll(c => c.CustomerID == 5) |
| 6 | 上海 | 中國 | 索尼 | orders.FindAll(c => c.CustomerID == 6) |
| 7 | 北京 | 中國 | 聯想 | orders.FindAll(c => c.CustomerID == 7) |
| 8 | 上海 | 中國 | 諾基亞 | orders.FindAll(c => c.CustomerID == 8) |
orders數據:
| 0 | 0 | DateTime.Now | details.FindAll(d => d.OrderID == 0) |
| 1 | 0 | DateTime.Now | details.FindAll(d => d.OrderID == 1) |
| 2 | 1 | DateTime.Now | details.FindAll(d => d.OrderID == 2) |
| 3 | 1 | DateTime.Now | details.FindAll(d => d.OrderID == 3) |
| 4 | 2 | DateTime.Now | details.FindAll(d => d.OrderID == 4) |
| 5 | 2 | DateTime.Now | details.FindAll(d => d.OrderID == 5) |
| 6 | 3 | DateTime.Now | details.FindAll(d => d.OrderID == 6) |
| 7 | 3 | DateTime.Now | details.FindAll(d => d.OrderID == 7) |
| 8 | 4 | DateTime.Now | details.FindAll(d => d.OrderID == 8) |
| 9 | 5 | DateTime.Now | details.FindAll(d => d.OrderID == 9) |
| 10 | 6 | DateTime.Now | details.FindAll(d => d.OrderID == 10) |
| 11 | 6 | DateTime.Now | details.FindAll(d => d.OrderID == 11) |
| 12 | 7 | DateTime.Now | details.FindAll(d => d.OrderID == 12) |
| 13 | 7 | DateTime.Now | details.FindAll(d => d.OrderID == 13) |
| 14 | 8 | DateTime.Now | details.FindAll(d => d.OrderID == 14) |
| 15 | 8 | DateTime.Now | details.FindAll(d => d.OrderID == 15) |
| 16 | 8 | DateTime.Now | details.FindAll(d => d.OrderID == 16) |
details數據:
| 0 | 0 | 0 | 1000 | 10 |
| 1 | 1 | 1 | 2134 | 8 |
| 2 | 2 | 1 | 1236 | 9 |
| 3 | 3 | 0 | 754 | 7 |
| 4 | 4 | 2 | 2354 | 12 |
| 5 | 5 | 0 | 6985 | 13 |
| 6 | 6 | 2 | 4213 | 10 |
| 7 | 7 | 3 | 1977 | 10 |
| 8 | 8 | 2 | 287 | 6 |
| 9 | 9 | 5 | 9778 | 12 |
| 10 | 10 | 4 | 854 | 11 |
| 11 | 11 | 2 | 756 | 10 |
| 12 | 12 | 3 | 1000 | 9 |
| 13 | 13 | 1 | 786 | 8 |
| 14 | 14 | 3 | 346 | 7 |
| 15 | 15 | 2 | 576 | 6 |
| 16 | 16 | 0 | 782 | 10 |
products數據:
| 0 | samsung |
| 1 | nokia |
| 2 | apple |
| 3 | xiaomi |
| 4 | huawei |
| 5 | lenovo |
Demo查詢表達式
條件篩選
使用where子句,可以按照一個或者多個條件篩選集合,where子句的表達式的結果類型應該是布爾類型。
篩選在北京且名稱以‘小’開頭的顧客。
該LINQ查詢會返回在北京而且名字以“小”開頭的Customer集合。
輸出結果:
復合from子句篩選
當需要根絕對象的一個成員進行篩選,而該成員本身是一個集合或者列表,就可以使用復合的from子句。
篩選訂單數量大于800的信息。
輸出結果:
排序
要對序列排序,需要使用orderby子句。
按照城市、顧客ID進行排序。
輸出結果:
分組
要根木一個關鍵值對查詢結果分組,可以使用group子句。
統計各個產品的訂單數量。
輸出結果:
對嵌套的對象分組
如果分組的對象包含嵌套的序列,則各個改變select子句創建的匿名類型。
統計各個產品的訂單數量,并輸出各個訂單的訂貨數量。
輸出結果:
連接
使用join子句可以根據特定的條件合并兩個數據源,但之前要獲得兩個要連接的列表。
統計各個顧客和其訂單的信息。
輸出結果:
聚合操作符
聚合操作符(包括Count()、Sum()、Min()、Max()、Average()和Aggregate())它們不返回一個序列,而是返回一個值。
Count()方法返回集合中的項數;Sum()方法匯總序列中所有數字,返回這些數字的和;Min()方法返回集合中的最小值;Max()方法返回集合中的最大值;Average()方法計算集合中的平均值;Aggregate()方法,可以傳遞一個Lambda表達式,該表達式對所有的值進行聚合。
這些方法的使用方式類似,都是直接對序列或者集合進行操作。下面用Sum()做一個示例:
統計各個顧客總共訂單的訂貨數量。
輸出結果:
使用擴展方法和Lambda表達式簡化LINQ查詢
什么是擴展方法
當方法的第一個形參包含this修飾符的時候,該方法稱為擴展方法。擴展方法只能在非泛型、非嵌套的靜態類中聲明,擴展方法的第一個形參不能帶有除this之外的其他任何修飾符,而且形參類型不能是指針類型。
下面的程序是一個擴展方法的示例:
現在就可以調用該方法了:
string str="Jay"; str.HelloWorld(); 我們可以看到,控制臺中輸出了“Jay 調用了:HelloWorld”。
之所以這樣,是因為HelloWorld第一個參數類型為string類型,因此該方法就是string類型的擴展方法,所有的string類型變量都可以調用,而變量的內容就是傳遞給HelloWorld的參數。
上面的程序和下面的代碼結果一樣:
LINQ擴展方法
LINQ為IEnumerable<T>接口提供了各種擴展方法,以便用戶在實現了該接口的任意集合上使用LINQ查詢。表1中列出的LINQ查詢操作符,都有相應的擴展方法實現。
使用擴展方法可以和使用LINQ查詢表達式獲得十分類似甚至是相同的結果,當擴展方法和Lambda表達式結合的時候,會大大簡化LINQ查詢。
簡化LINQ查詢
前面一節的條件篩選LINQ表達式可以簡化為:
var query = customers.Where(c => c.City == "北京" && c.Name.StartsWith("小")).Select(c => c);- 1
前面一節的條件復合from子句篩選LINQ表達式可以簡化為:
- 1
前面一節的排序LINQ表達式可以簡化為:
- 1
前面一節的分組LINQ表達式可以簡化為:
- 1
前面一節的對嵌套的對象分組LINQ表達式可以簡化為:
- 1
前面一節的連接LINQ表達式可以簡化為:
var query = customers.SelectMany(c => c.Orders, (c, o) => new { Name = c.Name, City = c.City, Details = o.Details }).SelectMany(_var => _var.Details, (_var, _detail) => new { Name = _var.Name, City = _var.City, Money = _detail.Quantity * _detail.UnitPrice, ProductID = _detail.ProductID }).Join(products, a => a.ProductID, b => b.ProductID, (a, b) => new { Name = a.Name, City = a.City, Money = a.Money, ProductName = b.ProductName });- 1
前面一節的聚合操作LINQ表達式可以簡化為:
- 1
以上利用擴展方法和Lambda表達式的簡化后的LINQ查詢代碼的查詢結果,與LINQ查詢表達式結果完全一樣,證明這樣是完全可行的。
唯一的問題就是代碼看起來比較費解了。
LINQ查詢表達式簡化轉換原則
看到這里我們可能要奇怪,為什么我們能用這樣的方式來簡化LINQ查詢表達式呢?
關于這個問題,我們將在下一篇文章進行詳細講解。
歡迎大家點擊閱讀。
本人還是菜鳥,寫的不對的地方還請各位不吝賜教!
總結
以上是生活随笔為你收集整理的[深入学习C#]LINQ查询表达式详解(1)——基本语法、使用扩展方法和Lambda表达式简化LINQ查询的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SQL Server语句
- 下一篇: centos7 配置http服务器