C# 基礎知識系列-7 Linq詳解
- 2020 年 4 月 4 日
- 筆記
前言
在上一篇中簡單介紹了Linq的入門級用法,這一篇嘗試講解一些更加深入的使用方法,與前一篇的結構不一樣的地方是,這一篇我會先介紹Linq里的支援方法,然後以實際需求為引導,分別以方法鏈的形式和類SQL的形式寫出來。
前置概念介紹
Predicate<T>
謂詞、斷言,等價於Func<T,bool>
即返回bool的表達式Expression<TDelegate>
表達式樹,這個類很關鍵,但是在這裡會細說,我們會講它的一個特殊的泛型類型:Expression<Func<T,bool>>
這個在某些數據源的查詢中十分重要,它代表lambda表達式中一種特殊的表達式,即沒有大括弧和return
關鍵字的那種。
我們先準備兩個類:
- Student/學生類:
/// <summary> /// 學生 /// </summary> public class Student { /// <summary> /// 學號 /// </summary> public int StudentId { get; set; } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 班級 /// </summary> public string Class { get; set; } /// <summary> /// 年齡 /// </summary> public int Age { get; set; } }
-
Subject/科目類:
/// <summary> /// 科目 /// </summary> public class Subject { /// <summary> /// 名稱 /// </summary> public string Name { get; set; } /// <summary> /// 年級 /// </summary> public string Grade { get; set; } /// <summary> /// 學號 /// </summary> public int StudentId { get; set; } /// <summary> /// 成績 /// </summary> public int Score { get; set; } }
Subject 和Student通過學號欄位一一關聯,實際工作中數據表有可能會設計成這。
那麼先虛擬兩個數據源:IEnumerable<Student> students
和 IEnumerable<Subject> subjects
。先忽略這兩個數據源的實際來源,因為在開發過程中數據來源有很多種情況,有資料庫查詢出來的結果、遠程介面返回的結果、文件讀取的結果等等。不過最後都會整理成IEnumerable<T>
的子介面或實現類的對象。
常見方法介紹
Where 過濾數據,查詢出符合條件的結果
where的方法聲明:
public IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate)
可以看出不會轉換數據類型,通過給定的lambda表達式或者一個方法進行過濾,獲取返回true的元素。
示例:
// 獲取年紀大於10但不大於12的同學們 List<Student> results = students.Where(t=>t.Age >10 && t.Age<= 12).ToList();
注意在調用ToList之後數據才會實質上查詢出來。
Group 分組,依照指定內容進行分組
Group的方法聲明有很多種:
最常用的一種是:
public static IEnumerable<System.Linq.IGrouping<TKey,TSource>> GroupBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);
示例:
//將學生按照班級進行分組 List<IGrouping<string,Student>> list = students.GroupBy(p => p.Class).ToList();
OrderBy/OrderByDescending 進行排序,按條件升序/降序
它們是一對方法,一個是升序一個降序,其聲明是一樣的:
常用的是:
public static System.Linq.IOrderedEnumerable<TSource> OrderBy<TSource,TKey> (this IEnumerable<TSource> source, Func<TSource,TKey> keySelector);
示例:
//按年齡的升序排列: List<Student> results = students.OrderBy(p => p.Age).ToList(); //按年齡的降序排列: List<Student> results = students.OrderByDescending(p => p.Age).ToList();
First/Last 獲取數據源的第一個/最後一個
這組方法有兩個常用的重載聲明:
First:
// 直接獲取第一個 public static TSource First<TSource> (this IEnumerable<TSource> source); // 獲取滿足條件的第一個 public static TSource First<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
Last:
// 直接獲取最後一個 public static TSource Last<TSource> (this IEnumerable<TSource> source); // 獲取最後一個滿足條件的元素 public static TSource Last<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
示例:
Student student = students.First();// 等價於 students[0]; Student student = students.First(p=>p.Class == "一班");//獲取數據源中第一個一班的同學 Student student = students.Last();//最後一個學生 Student student = students.Last(p=>p.Class == "三班");//獲取數據源中最後一個三班的同學
注意:
- 在某些數據源中使用Last會報錯,因為對於一些管道類型的數據源或者說非同步數據源,程式無法確認最後一個元素的位置,所以會報錯。解決方案:先使用OrderBy對數據源進行一次排序,使結果與原有順序相反,然後使用First獲取
- 當數據源為空,或者不存在滿足條件的元素時,調用這組方法會報錯。解決方案:調用FirstOrDefault/LastOrDefault,這兩組方法在無法查詢到結果時會返回一個默認值。
Any/All 是否存在/是否都滿足
Any:是否存在元素滿足條件
有兩個版本,不過意思可能不太一樣:
public static bool Any<TSource> (this IEnumerable<TSource> source);//數據源中是否有數據 //================ //是否存在滿足條件的數據 public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
All :是否都滿足條件:
public static bool Any<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
示例:
// 是否有學生 bool isAny = students.Any(); // 是否有五班的同學 bool isFive = students.Any(p=>p.Class == "五班"); // 是否所有學生的年紀都不小於9歲 bool isAll = students.All(p=>p.Age >= 9);
Skip 略過幾個元素
Skip一共有三個衍生方法:
第一個:Skip 自己: 略過幾個元素,返回剩下的元素內容
public static IEnumerable<TSource> Skip<TSource> (this IEnumerable<TSource> source, int count);
第二個:SkipLast,從尾巴開始略過幾個元素,返回剩下的元素內容
public static IEnumerable<TSource> SkipLast<TSource> (this IEnumerable<TSource> source, int count);
第三個:SkipWhile,跳過滿足條件的元素,返回剩下的元素
public static IEnumerable<TSource> SkipWhile<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
示例:
// 不保留前10個學生 List<Student> results = students.Skip(10).ToList(); // 不保留後10個學生 List<Student> results = students.SkipLast(10).ToList(); // 只要非一班的學生 List<Student> results = students.SkipWhere(p=>p.Class=="一班").ToList(); //上一行程式碼 等價於 = students.Where(p=>p.Class != "一班").ToList();
Take 選取幾個元素
Take與Skip一樣也有三個衍生方法,聲明的參數類型也一樣,這裡就不對聲明做介紹了,直接上示例。
//選取前10名同學 List<Student> results = students.Take(10).ToList(); // 選取最後10名同學 List<Student> results = students.TakeLast(10).ToList(); //選取 一班的學生 List<Student> results = students.TakeWhile(p=>p.Class=="一班").ToList(); // 上一行 等價於 = students.Where(p=>p.Class=="一班").ToList();
在使用Linq寫分頁的時候,就是聯合使用Take和Skip這兩個方法:
int pageSize = 10;//每頁10條數據 int pageIndex = 1;//當前第一頁 List<Student> results = students.Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();
其中 pageIndex可以是任意大於0 的數字。Take和Skip比較有意思的地方就是,如果傳入的數字比數據源的數據量大,根本不會爆粗,只會返回一個空數據源列表。
Select 選取
官方對於Select的解釋是,將序列中的每個元素投影到新的表單里。我的理解就是,自己 定義一個數據源單個對象的轉換器,然後按照自己的方式對數據進行處理,選擇出一部分欄位,轉換一部分欄位。
所以按我的理解,我沒找到java8的同效果方法。(實際上java用的是map,所以沒找到,?)
public static System.Collections.Generic.IEnumerable<TResult> Select<TSource,TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector);
示例:
// 選出班級和姓名 List<object> results = students.Select(p => new { p.Class, p.Name }).ToList();
簡單運算操作
Linq 里有幾個需要注意的簡單運算操作,這部分在使用中很常見。
Max 選取最大的一個
Max獲取數據源中最大的一個,不過只能是數字類型的,其他類型因為不能直接比較大小所以可以有替代方法,就是先排序取第一個。
以下是Max方法的兩個重載版本:
public static int Max (this IEnumerable<int> source); public static int Max <TSource>(this IEnumerable<TSource> source,Func<TSource,int> selector);
示例:
//查詢學生中最大的年紀是多少 int maxAge = students.Select(t=>t.Age).Max();
Min 選取最小的一個
方法類似與Max,不過與之不同的是獲取最小的一個,不能應用於非數字類型。
示例:
// 查詢學生中最小的年紀是多少 int minAge = students.Select(t=> t.Age).Min(); //======= int minAge = students.Min(p=>p.Age);
Average 求平均數
與 Max/Min是一樣類型的方法,依舊不能應用於非數字類型。
示例:
// 查詢學生的評價年紀 int averageAge = students.Select(t=>t.Age).Average(); int averageAge = students.Average(p=>p.Age);
Sum 求和
對數據源進行求和或者對數據源的某個欄位進行求和,還是不能對非數字類型進行求和
示例:
// 一個沒有實際意義的求和,學生的年齡總和 int sumAge = students.Select(t=>t.Age).Sum(); // int sumAge = students.Sum(p=>p.Age);
Contains 是否包含某個元素
判斷數據源中是否包含某個元素,返回一個bool值,如果包含則返回true,如果不包含則返回false。該方法有兩個重載版本,一個是使用默認的Equals
方法,一個是指定一個相等性比較器實現類。
public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value); //傳入相等性比較器的 public static bool Contains<TSource> (this IEnumerable<TSource> source, TSource value, IEqualityComparer<TSource> comparer);
值得注意的是,這裡的相等比較器是一個介面,也就是說需要使用類來實現這個方法。通常在實際開發過程中,我們會在TSource這個數據源所代表的類上增加 IEqualityCompare的實現。
示例1:
Student student1 = new Student();// 初始化一個學生類 Student student2 = students.First();// 從數據源中取一個 bool isContains = students.Contains(student1);// 返回 false, bool isContains2 = students.Contains(student2);// 返回 true
說明: 類的默認相等比較是比較是否是同一個對象,即返回的
示例2:
創建一個相等性比較器,值得注意的是,相等性比較器有兩個方法,一個是比較元素是否相等,一個是返回元素的HashCode,這兩個方法必須在判斷元素是否相等上保持結果一致。
public class StudentEqualityCompare: IEqualityComparer<Student> { public bool Equals(Student x, Student y) { // 省略邏輯 } public int GetHashCode(Student obj) { //省略邏輯 } }
使用:
StudentEqualityCompare compare = new StudentEqualityCompare(); Student student = students.First(); bool isContains = students.Contains(student, compare);
Count/LongCount 數量查詢
這是一組行為一樣的方法,就是對數據源進行計數,不同的是Count返回int,LongCount返回long。
它們的聲明有以下兩種,這裡選了Count的聲明:
public static int Count<TSource> (this IEnumerable<TSource> source); public static int Count<TSource> (this IEnumerable<TSource> source, Func<TSource,bool> predicate);
示例:
int count = students.Count();//返回一共有多少個學生 int count = students.Count(p=>p.Class=="一班");// 統計一班一共有多少學生
同類型數據源的操作
之前介紹了單個數據源的操作方法,這些方法不會讓數據源發生變化,更多的對數據源進行過濾和選擇或者統計。現在介紹幾個對多個數據源進行操作的方法。
Union 聯合另一個同類型的數據源
聯合另一個數據源,意思就是把兩個數據源合併到一個裡面,去掉重複的元素,只保留不重複的元素,並返回這個結果集。
與Contains方法差不多,這個方法有兩個重載的版本:
public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Union<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);
示例:
先假設一個業務場景:
學校舉辦運動會,現在教務處收到了田徑組 500米跑的報名名單和跳遠的報名名單,需要看看一共有哪些學生報名了這兩項賽事。
// 省略數據源,田徑組的名單 IEnumerable<Student> students1 = new List<Student>(); //省略數據源來源,跳遠組的名單 IEnumerable<Student> students2 = new List<Student>(); List<Student> all = students1.Union(student2).ToList();
這時候簡單統計了一下所有人,但是後來教務處在核對的時候,發現有的人名重複了,需要判斷是否是一個人,這時候就必須創建一個相等比較器了。
List<Student> all = students1.Union(student2,compare).ToList(); // 省略compare的實現,具體可參照Contains的比較器
Intersect 獲取兩個集合中都存在的數據
獲取同時存在於兩個集合中的元素,與Union類似。
方法的聲明如下:
public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Intersect<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);
示例:
繼續之前的業務場景,現在教務處需要知道有哪些同學同時報名了兩個比賽
List<Student> students = students1.Intersect(students2).ToList();
Except 獲取只在第一個數據源中存在的數據
獲取只存在於第一個集合的元素,從第一個集合中去除同時存在與第二個集合的元素,並返回。
方法的聲明如下:
public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second); public static IEnumerable<TSource> Except<TSource> (this IEnumerable<TSource> first, IEnumerable<TSource> second, IEqualityComparer<TSource> comparer);
示例:
繼續業務描述,教務處要一份只報名了500米的學生名單:
List<Student> students = students1.Except(students2).ToList();
Reverse 翻轉順序
數據源中的元素原本有一定的順序,這個方法可以將數據源中的順序翻轉過來,原本是最後一個的變成了第一個
,第一個變成了最後一個。
簡單示例:
char[] apple = { 'a', 'p', 'p', 'l', 'e' }; char[] reversed = apple.Reverse().ToArray();
Distinct 去重
對數據源進行去重,然後返回去重之後的結果。同樣,這個方法有兩個重載版本,一個有比較器,一個沒有比較器。
// 不用比較器的 public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source); // 設置比較器 public static IEnumerable<TSource> Distinct<TSource> (this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
示例:
先描述一個可能會出現的場景,每個班級在各個賽事組提交報名資訊的時候有點混亂,500米的負責老師把一個班的名單多錄了一次,但是學生已經亂序了,現在需要把多錄的去掉,也就是對數據進行去重。
List<Student> students = students1.Distinct();
多個類型數據源的操作
之前的方法基本都是對一個類型的數據源進行操作,不會涉及其他類型的數據源。現在介紹一下怎麼關聯多個類型的數據源,類似於SQL里的多錶鏈接查詢。
Join 關聯兩個數據源
按照一定的邏輯將兩個數據源關聯到一起,然後選擇出需要的數據。
方法有這幾個重載版本:
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); // 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, IEqualityComparer<TKey> comparer);
這個方法的參數比較多,我們大概介紹一下這個方法的所有參數:
類型參數
-
TOuter 第一個序列中的元素的類型。
-
TInner 第二個序列中的元素的類型。
-
TKey 選擇器函數返回的鍵的類型。
-
TResult 結果元素的類型。
參數
-
outer IEnumerable
要聯接的第一個序列。 -
inner IEnumerable
要與第一個序列聯接的序列。 -
outerKeySelector Func<TOuter,TKey> 用於從第一個序列的每個元素提取聯接鍵的函數。
-
innerKeySelector Func<TInner,TKey> 用於從第二個序列的每個元素提取聯接鍵的函數。
-
resultSelector Func<TOuter,TInner,TResult> 用於從兩個匹配元素創建結果元素的函數。
-
comparerIEqualityComparer
用於對鍵進行哈希處理和比較的 IEqualityComparer。
示例:
假設前天語文老師組織了一場考試,因為是模擬正式考試,所以答題紙上學生都只寫了學號,現在需要把考試成績和學生們聯繫在一起
List<object> results = students.Join(subjects, p => p.StudentId, s => s.StudentId, (p, s) => new { Student = p, Subject = s }).ToList(); /** 返回一個學生和科目的匿名對象,不過被我用object接了,這裡會有一個問題,如果有興緻可以提前了解一下C#的var關鍵字和匿名對象,這部分將會放在C#基礎系列補全篇講解 */
GroupJoin 關聯兩個數據源,並分組
基於鍵值等同性將兩個序列的元素進行關聯,並對結果進行分組。以上是官方介紹,我在開發過程中並沒有使用過這個方法,不過這個方法完全可以認為是Join和Group的組合體,即先進行了一次Join然後又對數據進行一次分組。
方法聲明:
// 使用默認比較器 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); //設置比較器 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, IEqualityComparer<TKey> comparer);
類型參數
-
TOuter 第一個序列中的元素的類型。
-
TInner 第二個序列中的元素的類型。
-
TKey 鍵選擇器函數返回的鍵的類型。
-
TResult 結果元素的類型。
參數
-
outer IEnumerable
要聯接的第一個序列。 -
inner IEnumerable
要與第一個序列聯接的序列。 -
outerKeySelector Func<TOuter,TKey> 用於從第一個序列的每個元素提取聯接鍵的函數。
-
innerKeySelector Func<TInner,TKey> 用於從第二個序列的每個元素提取聯接鍵的函數。
-
resultSelector Func<TOuter,IEnumerable
,TResult> 用於從第一個序列的元素和第二個序列的匹配元素集合中創建結果元素的函數。 -
comparer IEqualityComparer
用於對鍵進行哈希處理和比較的 IEqualityComparer。
以下是官方給的示例:
class Person { public string Name { get; set; } } class Pet { public string Name { get; set; } public Person Owner { get; set; } } public static void GroupJoinEx1() { Person magnus = new Person { Name = "Hedlund, Magnus" }; Person terry = new Person { Name = "Adams, Terry" }; Person charlotte = new Person { Name = "Weiss, Charlotte" }; Pet barley = new Pet { Name = "Barley", Owner = terry }; Pet boots = new Pet { Name = "Boots", Owner = terry }; Pet whiskers = new Pet { Name = "Whiskers", Owner = charlotte }; Pet daisy = new Pet { Name = "Daisy", Owner = magnus }; List<Person> people = new List<Person> { magnus, terry, charlotte }; List<Pet> pets = new List<Pet> { barley, boots, whiskers, daisy }; // Create a list where each element is an anonymous // type that contains a person's name and // a collection of names of the pets they own. var query = people.GroupJoin(pets, person => person, pet => pet.Owner, (person, petCollection) => new { OwnerName = person.Name, Pets = petCollection.Select(pet => pet.Name) }); foreach (var obj in query) { // Output the owner's name. Console.WriteLine("{0}:", obj.OwnerName); // Output each of the owner's pet's names. foreach (string pet in obj.Pets) { Console.WriteLine(" {0}", pet); } } } /* This code produces the following output: Hedlund, Magnus: Daisy Adams, Terry: Barley Boots Weiss, Charlotte: Whiskers */
以上是關於Linq的所有方法內容,但是這仍然不是Linq的全部。後續還會有一篇關於Linq的另一種查詢方式的內容文章。
更多內容煩請關注我的部落格