?Linq(Language Integrated Query,集成查詢語言),顧名思義就是用來查詢數(shù)據(jù)的一種語言(可以看作是一組功能、框架特性的集合)。在.NETFramework3.5(大概2007年)引入C#,用統(tǒng)一的C#語言快速查詢各種數(shù)據(jù),如數(shù)據(jù)庫、XML文檔、對象集合等等。Linq的誕生對 C# 編程領(lǐng)域產(chǎn)生了深遠(yuǎn)而持久的影響,改變了開發(fā)人員對查詢的思考方式。
- 使用簡單:統(tǒng)一語法(鏈?zhǔn)椒椒ㄕZ法、類似SQL的查詢語法),智能提示。
- 類型安全:編譯時(shí)強(qiáng)類型檢查,減少運(yùn)行時(shí)錯(cuò)誤。
- 延遲執(zhí)行,查詢本身只是構(gòu)建了一個(gè)表達(dá)式,在真正使用的時(shí)候(foreach、ToList、查詢數(shù)據(jù)庫)才會(huì)執(zhí)行。
- 支持多種數(shù)據(jù)源:內(nèi)存中的集合,以及各種外部數(shù)據(jù)庫。

Linq支持查詢?nèi)魏螌?shí)現(xiàn)了IEnumerable<T>接口的集合類型,基本上所有集合數(shù)據(jù)都支持Linq查詢。如下示例:大于 5 的偶數(shù),并倒敘排列取前三名
|
| var query = arr.Where(n => n > 5 && n % 2 == 0).OrderByDescending(n => n).Take(3); |
Linq 有兩種語法風(fēng)格,如下實(shí)例代碼,一種是常規(guī)C#方法調(diào)用方式,另外一種是類似SQL的查詢表達(dá)式。這兩種語法其本質(zhì)是一樣的,編譯后的中間語言(IL)是一樣的,確實(shí)僅僅只是語法形式不同而已。
??鏈?zhǔn)椒椒?/span>:就是字面意思,函數(shù)式方法調(diào)用。這些方法都來自 IEnumerable 接口或 IQueryable 接口的擴(kuò)展方法,這些方法提供了過濾、聚合、排序等多種查詢功能。
??查詢表達(dá)式:查詢表達(dá)式由一組用類似于 SQL 的聲明性語法所編寫的子句組成。 每個(gè)子句依次包含一個(gè)或多個(gè) C# 表達(dá)式,而這些表達(dá)式可能本身就是查詢表達(dá)式,或者包含查詢表達(dá)式。查詢表達(dá)式必須以 from 子句開頭,且必須以 select 或 group 子句結(jié)尾。
|
| var query = arr.Where(n => n > 5 && n % 2 == 0).OrderByDescending(n => n).Take(3); |
|
| var query2 = (from n in arr |
| where n > 5 && n % 2 == 0 |
| orderby n descending |
| select n).Take(3); |
| 比較 | 鏈?zhǔn)椒椒?/span> | 查詢表達(dá)式(SQL) |
|---|
| 特點(diǎn) | 鏈?zhǔn)椒椒ㄕ{(diào)用,函數(shù)式編程 | 類似SQL語句,自然語言,容易掌握 |
| 語法形式 | 點(diǎn)點(diǎn)點(diǎn)鏈?zhǔn)椒椒ㄕ{(diào)用,Where().Select().Order() | 以from開頭:from...where...select |
| 常用方法/語法 | System.Linq 上提供的擴(kuò)展方法或第三方擴(kuò)展:Where、OrderBy、Select、Skip、Take、Union | 僅支持編譯器識(shí)別的關(guān)鍵字:from、where、orderby、group、join、let、select、into、in、on等 |
| 本質(zhì) | System.Linq 提供的擴(kuò)展方法調(diào)用 | 編譯為標(biāo)準(zhǔn)查詢運(yùn)算符方法調(diào)用,編譯結(jié)果和鏈?zhǔn)椒椒ㄒ粯?/span> |
| 功能完整性 | 完整的Linq功能 | 有些能力沒有對應(yīng)語法(如Max),需要結(jié)合鏈?zhǔn)椒椒ㄊ褂?/td> |

?? 兩種編寫方式編譯后生成的IL代碼實(shí)際上是一樣的,也可以混合使用,因此他們并沒有性能差異。
查詢表達(dá)式并不能實(shí)現(xiàn)獲取前3個(gè)元素,此時(shí)就需要兩者混合使用,
| var query = from u in list |
| where u.Age>14 |
| group u by u.Address into gu |
| orderby gu.Count() descending |
| select (gu.Key,gu.Count()); |
| query = query.Take(3); |
LINQ 提供了兩種用途的架構(gòu):針對本地(內(nèi)存)對象的本地查詢,以及針對遠(yuǎn)程數(shù)據(jù)源(數(shù)據(jù)庫)的解釋性查詢。兩者的語法形式基本一樣,都支持鏈?zhǔn)椒椒?、查詢表達(dá)式。
??本地查詢:實(shí)現(xiàn)了針對IEnumerable的內(nèi)存集合(數(shù)組、List)的查詢,其Linq的擴(kuò)展方法都在 System.Linq.Enumerable 類中。查詢只是構(gòu)建了一個(gè)可枚舉的迭代裝飾器序列,延遲在使用(消費(fèi))數(shù)據(jù)時(shí)執(zhí)行。
??解釋查詢:解釋查詢是描述性的,實(shí)現(xiàn)了針對IQueryable(Table、DbSet)的遠(yuǎn)程數(shù)據(jù)查詢,對應(yīng)擴(kuò)展方法都在 System.Linq.Queryable 類中。他們在運(yùn)行時(shí)生成表達(dá)式樹,并進(jìn)行解釋為SQL語句,在數(shù)據(jù)庫中執(zhí)行該SQL語句并獲取數(shù)據(jù)。
| 比較 | 本地查詢 Enumerable | 解釋查詢 Queryable |
|---|
| 操作對象 | 內(nèi)存中的集合(IEnumerable<T>) | 外部數(shù)據(jù)源的查詢接口(IQueryable<T>) |
| 延遲執(zhí)行 | 支持,真正使用(消費(fèi))數(shù)據(jù)時(shí)才執(zhí)行,如 foreach、ToList | 支持,消費(fèi)數(shù)據(jù)時(shí)才翻譯成SQL并在數(shù)據(jù)庫中執(zhí)行獲取數(shù)據(jù) |
| 執(zhí)行原理 | 參數(shù)為委托方法,C#內(nèi)部執(zhí)行委托、迭代器 | 參數(shù)為表達(dá)式樹,LINQ Provider 在運(yùn)行時(shí)遍歷該樹轉(zhuǎn)換為目標(biāo)語言(如 SQL) |
| 誰來執(zhí)行 | CLR本地執(zhí)行,數(shù)據(jù)在內(nèi)存中 | 數(shù)據(jù)庫執(zhí)行SQL,數(shù)據(jù)在數(shù)據(jù)庫中 |
| 執(zhí)行過程 | 本地逐個(gè)元素迭代調(diào)用委托 | 數(shù)據(jù)庫中執(zhí)行SQL,返回查詢結(jié)果 |
| 使用場景 | List、Array、普通內(nèi)存數(shù)據(jù) | Entity Framework、LINQ to SQL、MongoDB 查詢 |
| 語法 | 都支持鏈?zhǔn)椒椒ā⒈磉_(dá)式查詢 | 同樣支持鏈?zhǔn)椒椒?、表達(dá)式查詢 |
| Linq方法在哪里? | System.Linq.Enumerable 靜態(tài)類 | System.Linq.Queryable 類,方法和 Enumerable 大部分對應(yīng)。有些方法并不能生成數(shù)據(jù)庫兼容的SQL語法。 |
| 擴(kuò)展性 | 內(nèi)存查詢支持任意C#方法,擴(kuò)展性強(qiáng) | 受限,只能使用數(shù)據(jù)庫兼容的方法。如正則表達(dá)式SQLServer就不支持。 |
| 結(jié)合使用 | 本地?cái)?shù)據(jù)只能用本地查詢 | 遠(yuǎn)程數(shù)據(jù)可以結(jié)合本地查詢混用。 |
IQueryable 繼承自 IEnumerable,因此解釋查詢可以轉(zhuǎn)換為本地查詢,query.AsEnumerable(),不過需謹(jǐn)慎使用,會(huì)將數(shù)據(jù)庫的相應(yīng)數(shù)據(jù)都加載到內(nèi)存中。
| public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable |
| { |
| } |
??表達(dá)式樹是一個(gè)微型的代碼DOM結(jié)構(gòu),樹中的節(jié)點(diǎn)是Expression類型的節(jié)點(diǎn),涵蓋各種語法形式,如參數(shù)、變量、常量、賦值、比較、循環(huán)等等。表達(dá)式樹可以轉(zhuǎn)換(Compile)為委托,反之則不能。
延遲執(zhí)行是指查詢代碼不會(huì)立刻執(zhí)行,而是在正真取數(shù)的時(shí)候才會(huì)執(zhí)行。他是Linq最主要的特點(diǎn),是優(yōu)點(diǎn),也不全是,有些需要注意的地方。
- 并不是所有的Linq方法都是延遲的,如:First()、Last()、ToArray()、ToList(),及Count、Max等聚合計(jì)算方法會(huì)立即執(zhí)行。
- 如果數(shù)據(jù)源變了,結(jié)果也會(huì)變化。
| List<int> list = [2,3,9,4,5]; |
| var query = list.Where(s=>s>5); |
| Console.WriteLine(query.Sum()); |
| list.Add(6); |
| Console.WriteLine(query.Sum()); |
- 重復(fù)取數(shù)時(shí),查詢也會(huì)重復(fù)執(zhí)行,可能會(huì)浪費(fèi)性能,特別是復(fù)雜、耗時(shí)的查詢。避免的方式就是
query.ToList() 一次性立即獲取數(shù)據(jù)。 - Lambda變量捕獲,變量的值在真正執(zhí)行查詢的時(shí)候才會(huì)獲取,這是方法閉包的特點(diǎn)。
| List<int> list = [2,3,9,4,5]; |
| int n = 5; |
| var query = list.Where(s=>s>n); |
| n = 4; |
| Console.WriteLine(query.Sum()); |
為了支持延遲執(zhí)行,Linq內(nèi)部封裝了很多迭代裝飾器,偷偷看了下源碼,如 WhereIterator、SelectEnumerableIterator、ReverseIterator、UnionIterator 等,都是Linq內(nèi)部的迭代裝飾器。迭代裝飾器會(huì)保留輸入序列的引用及其他相關(guān)參數(shù),僅當(dāng)枚舉結(jié)果時(shí)才會(huì)執(zhí)行。
迭代序列裝飾器本身繼承自IEnumerable,因此就支持裝飾器之間的嵌套。下面為迭代裝飾器序列基類的源碼 Iterator.cs。
| internal abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource> |
| { |
| private readonly int _threadId; |
| internal int _state; |
| internal TSource _current = default!; |
| } |
內(nèi)存集合的Linq擴(kuò)展方法,基本都來自Enumerable類,參考官方 Enumerable 類。用于數(shù)據(jù)庫的解釋性查詢方法在 System.Linq.Queryable 類中,方法和 Enumerable 基本上都是對應(yīng)的?;旧纤械腖inq方法都在這里匯總:
| 方法 | 說明 |
|---|
| Chunk(Int32) | 分塊拆分為多個(gè)固定大小的數(shù)組,返回IEnumerable<TSource[]>,內(nèi)部每次迭代會(huì)構(gòu)建一個(gè)數(shù)組new TSource[arraySize] |
| Append(T) | 末尾追加一個(gè)元素,原理是內(nèi)部構(gòu)建了一個(gè)新的迭代器AppendPrepend1Iterator實(shí)現(xiàn)返回這個(gè)元素。 |
| Prepend(T) | 在前面追加一個(gè)元素,原理同上,是同一個(gè)AppendPrepend1Iterator |
| ??聚合計(jì)算,立即執(zhí)行 |
|
| Count() | 獲取集合中元素的數(shù)量,可指定條件參數(shù)Func。arr.Count(),內(nèi)部原理比較簡單,如果集合是ICollection等,則直接獲取Count,否則只能e.MoveNext()一個(gè)一個(gè)的數(shù)了。 |
| TryGetNonEnumeratedCount | 獲取元素?cái)?shù)量,在不真正遍歷(不枚舉)集合的情況下,盡量嘗試快速拿到集合元素的數(shù)量 |
| Max() | 返回最大的那個(gè)元素。截止.NET8,整數(shù)類型用了Vector提升性,其他循環(huán)比較,性能一般??。 |
| Min() | 返回最小的那個(gè)元素,性能原理同Max |
| Average() | 計(jì)算平均值,對于數(shù)值類型,內(nèi)部用到了Vector?,性能還是不錯(cuò)的。var a = arr.Average() |
| Sum() | 求和,arr1.Sum() |
| Aggregate(Func) | 執(zhí)行累加器函數(shù),函數(shù)的的輸出為作為下一輪迭代的輸入,依次迭代執(zhí)行。 示例,計(jì)算序列最大值:var max = arr.Aggregate((acc,n)=>acc>n?acc:n) |
| ??條件判斷 |
|
| Contains(T) | 判斷是否包含指定元素,返回bool,可指定比較器。bool f = arr.Contains(6) |
| Any() | 集合是否包含元素,判斷集合是否不為空。if(arr.Any()){} |
| Any(Func) | 集合是否包含指定條件的元素,示例:是否有人考試滿分,bool flag = arr.All(n=>n==100) |
| All(Func) | 所有元素是否滿足條件,示例:是否所有同學(xué)都及格了,bool flag = arr.All(n=>n>=60) |
| SequenceEqual(IEnumerable) | 序列相等比較,比較兩個(gè)序列是否相同,長度相同、每個(gè)元素相等則返回True |
| ??元素選擇 |
|
| First() | 返回第一個(gè)元素,如果一個(gè)都沒有拋出異常,arr1.First() |
| FirstOrDefault() | 返回第一元素,如果一個(gè)都沒有則返回默認(rèn)值,arr1.FirstOrDefault() |
| Last() | 返回最后一個(gè)元素,如果一個(gè)都沒有拋出異常。如果不是常規(guī)集合,會(huì)foreach循環(huán)所有??。 |
| LastOrDefault() | 同上,如果一個(gè)都木有則返回默認(rèn)值 |
| Single()、SingleOrDefault() | 獲取唯一元素,如果元素?cái)?shù)量大于1則拋出異常。這個(gè)方法在數(shù)據(jù)庫按主鍵查詢時(shí)比較有用。 |
| ElementAt(Index) | 返回指定索引Index位置的元素,arr.ElementAt(0)。還有個(gè)更安全的 ElementAtOrDefault |
| DefaultIfEmpty(defaultT) | 如果集合為空(集合中沒有元素)返回含一個(gè)默認(rèn)值的IEnumerable,否則返回原序列。 |
| ??篩選查詢 |
|
| Where(Func) | 條件查詢,最常用的Linq函數(shù)了,arr1.Where(s=>s>5) |
| Select(selector) | 返回指定Key(元素選擇處理器結(jié)果)的集合,list.Select(s=>s.Name+s.Age) |
| SelectMany() | 將每個(gè)元素的“內(nèi)部集合”展開合并為一個(gè)大集合,list.SelectMany(s=>s.Name.Split('-')) |
| Distinct() | 去重,arr.Distinct(),內(nèi)部使用HashSet<TSource>來去重。DistinctBy>可指定鍵Key。 |
| OfType() | 根據(jù)類型T篩選集合,源碼中用obj is TResult來篩選,不符合的丟棄。list.OfType<double>() |
| Skip(int count) | 跳過指定數(shù)量的元素,返回剩余的元素,arr1.Skip(5) |
| SkipLast(int count) | 忽略后面的元素,返回前面剩余的元素。arr1.SkipLast(3) |
| SkipWhile(Func) | 從開頭跳過符合條件的元素,直到遇到不符合條件時(shí)停下,返回剩下的元素。 |
| Take(int count) | 返回前n個(gè)元素,Skip的逆運(yùn)算,Take(3) |
| TakeLast(int count) | 返回最后n個(gè)元素,arr1.TakeLast(3) |
| TakeWhile(Func) | 從開頭返回符合條件的元素,直到遇到不符合條件時(shí)停下,與SkipWhile相反arr1.TakeWhile(s=>s<5) |
| ??排序分組 |
|
| Order() | 升序排列集合,arr2.Order() |
| OrderBy(TKey) | 指定Key鍵升序排列集合,list.OrderBy(s=>s.Age) |
| OrderByDescending(TKey) | 指定Key鍵降序排列集合,list.OrderByDescending(s=>s.Age) |
| ThenBy、ThenByDescending | 二次排序,跟著OrderBy使用,設(shè)置第二排序鍵。list.OrderBy(s=>s.Grade).ThenBy(s=>s.Age) |
| Reverse() | 反轉(zhuǎn)序列中元素的順序,arr2.Reverse()。內(nèi)部源碼是創(chuàng)建了一個(gè)數(shù)組來實(shí)現(xiàn)翻轉(zhuǎn),性能不佳??,數(shù)組推薦使用Array.Reverse(),原地翻轉(zhuǎn),不會(huì)創(chuàng)建額外對象。 |
| GroupBy | 按指定的Key分組,返回一個(gè)分組集合IGrouping<TKey, TSource>,list.GroupBy(s=>s.Name) |
| GroupJoin | 帶分組的連接(Join)操作,類似Sql中的Left Join + 分組,每個(gè)「左邊元素」對應(yīng)到「右邊的一組元素」 |
| ??多集合操作 |
|
| Union(IEnumerable) | 并集,合并兩個(gè)集合并去重,arr1.Union(arr2) |
| Intersect(IEnumerable) | 交集(Intersect /??nt??sekt/ 相交),返回兩個(gè)集合都包含的元素。IntersectBy 可指定鍵Key。 |
| Except(IEnumerable) | 移除(Except /?k?sept/ 除外)arr1.Except(arr2)移除arr2中也存在的元素。ExceptBy可指定鍵Key。 |
| Concat(IEnumerable) | “合并”兩個(gè)序列集合(),內(nèi)部由私有的ConcatIterator實(shí)現(xiàn)的連接迭代,arr.Concat([3]) |
| Join(arr2, k2,k1,Func) | 兩個(gè)“表”內(nèi)連接,類似Sql中的 Inner Join,用于兩個(gè)不同類型元素的的連接,兩個(gè)表Key匹配的元素合并 |
| Zip | 就像拉鏈(zipper)一樣,把兩個(gè)序列一對一地配對合并成一個(gè)新序列,arr1.Zip(arr2,(n1,n2)=>n1+n2) |
| ??轉(zhuǎn)換,ToXX立即執(zhí)行 | ?謹(jǐn)慎使用,會(huì)創(chuàng)建新的集合對象 |
| Cast() | 強(qiáng)制類型轉(zhuǎn)換,內(nèi)部使用強(qiáng)制轉(zhuǎn)換“(TResult)obj” |
| ToArray() | 從 IEnumerable 創(chuàng)建新數(shù)組,慎用。var narr = arr1.Order().ToArray() |
| ToList() | 從 IEnumerable 創(chuàng)建新List,arr1.Take(3).ToList() |
| ToHashSet | 從 IEnumerable 創(chuàng)建新HashSet(不可重復(fù)集合,自動(dòng)去重),arr1.ToHashSet() |
| ToDictionary() | 從 IEnumerable 創(chuàng)建新字典Dictionary<TK,TV>,list.ToDictionary(s=>s.Name,s=>s.Age) |
| ToLookup() | 從 IEnumerable 創(chuàng)建新 Lookup(分組的字典),arr1.ToLookup(s=>s%2) |
| ??其他 |
|
| Range(start, end) | 靜態(tài)方法,創(chuàng)建一個(gè)連續(xù)的序列,可用來創(chuàng)建測試數(shù)據(jù),Enumerable.Range(1,10).ToArray() |
| Repeat(T, count) | 靜態(tài)方法,創(chuàng)建一個(gè)重復(fù)值的序列,Enumerable.Repeat(18,10) |
| Empty() | 靜態(tài)方法,獲得一個(gè)空的序列,Enumerable.Empty<int>().Any(); //false |
| AsEnumerable() | 返回自己,什么也不干。在Linq to SQL中可以強(qiáng)制讓后續(xù)操作在本地內(nèi)存中進(jìn)行,而不會(huì)翻譯成SQL。 |
|
|
根據(jù)用戶輸入條件,構(gòu)建動(dòng)態(tài)查詢條件,使用 Skip 和 Take 實(shí)現(xiàn)分頁。
| var query = list.AsEnumerable(); |
| if (!string.IsNullOrWhiteSpace(name)) |
| query = query.Where(s => s.Name.Contains(name, StringComparison.OrdinalIgnoreCase)); |
| if (age.HasValue) |
| query = query.Where(s => s.Age == age); |
| if (!string.IsNullOrWhiteSpace(address)) |
| query = query.Where(s => s.Address.Contains(address)); |
|
| query = query |
| .Skip((pageNumber - 1) * pageSize) |
| .Take(pageSize); |
|
| var result = query.ToArray(); |
本地查詢擴(kuò)展是很容易的,基于IEnumerable<T>實(shí)現(xiàn)擴(kuò)展方法即可。IQueryable擴(kuò)展則要考慮數(shù)據(jù)庫的支持和映射,一般無需自定義擴(kuò)展。
|
| public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source) |
| { |
| int index = 0; |
| foreach (T element in source) |
| { |
| if (index % 2 == 0) |
| { |
| yield return element; |
| } |
|
| index++; |
| } |
| } |
|
|
| var query = list.AlternateElements(); |
??版權(quán)申明:版權(quán)所有@安木夕,本文內(nèi)容僅供學(xué)習(xí),歡迎指正、交流,轉(zhuǎn)載請注明出處!原文編輯地址-語雀
該文章在 2025/7/2 9:59:31 編輯過