LINQ中提供了很多集合的擴充套件方法,配合lambda能簡化資料處理。
例如我們想要找出一個IEnumerable<int>
中所有大於10的元素,使用LINQ則可以這樣寫
static void Main(string[] args)
{
int[] nums = new int[] { 3, 5, 6, 5, 10, 12, 14, 7 };
IEnumerable<int> res = nums.Where(a => a > 10);
foreach (int i in res)
Console.WriteLine(i);
}
其中使用IEnumerable
要using System.Collections.Generic;
使用Where
方法要using System.Linq;
,該方法會遍歷每個元素然後去判斷是否大於10
為了解LINQ背後的原理,我們首先去實現一個簡單的Where
方法
第一種方案:
static IEnumerable<int> MyWhere1(IEnumerable<int> items, Func<int, bool> f)
{
List<int> res = new List<int>();
foreach (int i in items)
if (f(i) == true)
res.Add(i);
return res;
}
第二種方案:
static IEnumerable<int> MyWhere2(IEnumerable<int> items, Func<int, bool> f)
{
List<int> res = new List<int>();
foreach (int i in items)
if (f(i) == true)
yield return i;
}
那麼這兩種方案的區別是什麼?第一種方案是把所有元素全部檢查一遍,把符合要求的元素放到List<int> res
裡面,然後返回res
;然而第二種方案使用yield
,是一種「流水線」方式處理,找到符合條件的元素立即返回,返回後Console.WriteLine
立即能夠列印,從而提高了資料處理效率。
LINQ提供了很多擴充套件方法,大部分都在System.Linq
名稱空間中。
接下來準備一些資料,用於下面的操作。
首先定義一個員工類,裡面有姓名工資等成員。
class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public bool Gender { get; set; }
public int Salary { get; set; }
public override string ToString()
{
return $"ID={Id}, Name={Name}, Age={Age}, Gender={Gender}, Salary={Salary}";
}
}
然後再Main方法中建立範例匯入資料,並將所有範例儲存到列表中。
List<Employee> lst = new List<Employee>();
lst.Add(new Employee { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
lst.Add(new Employee { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
lst.Add(new Employee { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
lst.Add(new Employee { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
lst.Add(new Employee { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
lst.Add(new Employee { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
lst.Add(new Employee { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
lst.Add(new Employee { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
該方法會遍歷每個元素然後去判斷是否符合條件,符合條件的元素則被返回。
IEnumerable<Employee> res = lst.Where(e => e.Age > 20); //把年齡大於20的返回過來
foreach (Employee e in res)
Console.WriteLine(e);
返回結果
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
ID=2, Name=jim, Age=33, Gender=True, Salary=3000
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
ID=6, Name=nancy, Age=35, Gender=False, Salary=8000
ID=7, Name=zack, Age=35, Gender=True, Salary=8500
ID=8, Name=jack, Age=33, Gender=True, Salary=8000
該方法會返回符合條件的元素的個數
Console.WriteLine(lst.Count(e=>e.Salary>8000));
返回結果
2
該方法會判斷是否存在至少一個元素符合條件。另外,如果傳入的引數為空,則會判斷IEnumerable
(或者實現了IEnumerable
介面的其他類,如List
)是否存在元素。
如以下程式碼
List<int> test = new List<int>();
Console.WriteLine(test.Any());
由於列表為空,test.Any()
返回的就是false
如果判斷是否存在Employee
型別的元素於lst
中,則程式碼如下,其返回值為true
Console.WriteLine(lst.Any(e=>e.Salary>8000));
同樣的,我們使用Count
方法也可以達成此目的(判斷返回元素個數是否為0),但是Count
方法相對於Any
方法效率較低。
這是因為Any
找到一個符合條件的元素會立即返回,而Count
方法是統計個數,找到一個符合元素後還要繼續向後找。
有關一條資料的方法有好幾種,不過在細節上略有不一樣,所以我們把他們放一塊介紹。
方法 | 描述 |
---|---|
Single() |
有且只有一條滿足要求的資料 |
SingleOrDefault() |
最多隻有一條滿足要求的資料 |
First() |
至少有一條,並且返回第一條 |
FirstOrDefault() |
返回第一條或預設值 |
Signle方法
Employee elem = lst.Single(e => e.Salary > 8000); //錯誤,有多條資料滿足條件
Employee elem = lst.Single(e => e.Salary > 8500); //正確,僅一條資料滿足條件
SingleOrDefault方法
對於SingleOrDefault
,當且僅當存在一條資料滿足條件,返回該資料;如果存在多條則報錯;如果不存在則返回預設值。
Employee elem = lst.SingleOrDefault(e=>e.Salary>8000); //錯誤,有多條資料滿足條件
Employee elem = lst.SingleOrDefault(e=>e.Salary>8500); //正確,僅一條資料滿足條件
下面我們來看下預設值的情況
int[] nums = new int[] { 1, 2, 3 };
int i = nums.SingleOrDefault(i => i > 10);
Console.WriteLine(i);
由於不存在大於10的整型數位,所以該方法返回變數i
的預設值,輸出結果為0
First方法
該方法要求資料至少有一條滿足條件,並且只返回查詢到的第一條資料。
Employee test = lst.First(e=>e.Salary>9000); //報錯,不存在資料滿足條件
//正確,滿足年齡大於16的有多條,僅按照我們新增資料的順序返回第一條
Employee test = lst.First(e=>e.Age>16);
Console.WriteLine(test);
輸出結果
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
FirstOrDefault方法
該方法返回符合條件的第一條資料,否則返回預設值
如下方程式碼,從陣列中返回一個大於2的整數,其輸出結果為3
int[] nums = new int[] { 1, 2, 3, 4, 5, 6 };
int i = nums.FirstOrDefault(e => e > 2);
Console.WriteLine(i);
我們再來看看返回預設值的情況
int[] nums = new int[] { 1, 2, 3, 4, 5, 6 };
int i = nums.FirstOrDefault(e => e > 10);
Console.WriteLine(i);
由於陣列中不存在大於10的數,所以i
的值就是其預設值0
排序方法有兩種
方法 | 描述 |
---|---|
OrderBy() |
正序排序 |
OrderByDescending() |
逆序排序 |
二者用法幾乎一致,此處僅演示OrderBy
方法
IEnumerable<Employee> res2 = lst.OrderBy(e => e.Age);
foreach (Employee e in res2)
Console.WriteLine(e);
其輸出結果為
ID=4, Name=lucy, Age=16, Gender=False, Salary=2000
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
ID=2, Name=jim, Age=33, Gender=True, Salary=3000
ID=8, Name=jack, Age=33, Gender=True, Salary=8000
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=6, Name=nancy, Age=35, Gender=False, Salary=8000
ID=7, Name=zack, Age=35, Gender=True, Salary=8500
很顯然資料已經按照年齡從小至大的順序進行排序了。
此外應該注意的是,該方法必須有引數。如果想要對一個陣列進行排序,正確寫法如下
int[] nums2 = new int[] { 3, 1, 2, 4, 5, 6 };
IEnumerable<int> resNum = nums2.OrderBy(i => i);
//寫成nums2.OrderBy()是錯誤的
所謂多排序,就是按照一個條件對資料進行排序後,存在多個資料該條件下的值一致,然後再對這些值一致的資料按照其他條件排序。
其方法也有兩個,一般是在OrderBy()
或OrderByDescending()
之後呼叫
方法 | 描述 |
---|---|
ThenBy() |
正序再排序 |
ThenByDescending() |
逆序再排序 |
我們對文章上方的資料首先按照對年齡進行排序,然後對年齡一致的員工再按照工資進行逆排序,編寫程式碼如下:
IEnumerable<Employee> sortTest = lst.OrderBy(x => x.Age).ThenByDescending(x => x.Salary);
foreach (Employee emp in sortTest)
Console.WriteLine(emp);
輸出如下:
ID=4, Name=lucy, Age=16, Gender=False, Salary=2000
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
ID=8, Name=jack, Age=33, Gender=True, Salary=8000
ID=2, Name=jim, Age=33, Gender=True, Salary=3000
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=7, Name=zack, Age=35, Gender=True, Salary=8500
ID=6, Name=nancy, Age=35, Gender=False, Salary=8000
限制結果集,獲取部分資料的方法一般是利用Skip
和Take
方法
例如我想要從上述員工資料中,獲取從第2條開始連續的3條資料,則程式碼可以這樣寫;
IEnumerable<Employee> sortTest = lst.Skip(2).Take(3);
foreach (Employee emp in sortTest)
Console.WriteLine(emp);
其輸出結果如下:
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=4, Name=lucy, Age=16, Gender=False, Salary=2000
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
當然,Skip
方法和Take
方法也可以單獨使用:
Take單獨使用
IEnumerable<Employee> sortTest = lst.Take(3);
foreach (Employee emp in sortTest)
Console.WriteLine(emp);
輸出:
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
ID=2, Name=jim, Age=33, Gender=True, Salary=3000
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
Skip單獨使用
IEnumerable<Employee> sortTest = lst.Skip(2);
foreach (Employee emp in sortTest)
Console.WriteLine(emp);
輸出:
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=4, Name=lucy, Age=16, Gender=False, Salary=2000
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
ID=6, Name=nancy, Age=35, Gender=False, Salary=8000
ID=7, Name=zack, Age=35, Gender=True, Salary=8500
ID=8, Name=jack, Age=33, Gender=True, Salary=8000
LINQ聚合函數常用的有這些,但應當注意的是它們的返回值型別不與其他LINQ的方法一樣是IEnumerable
,而是條件的值的型別。
方法 | 描述 |
---|---|
Max() |
返回給定條件的最大值 |
Min() |
返回給定條件的最小值 |
Average() |
返回給定條件的平均值 |
Sum() |
返回給定條件的和 |
Count() |
統計滿足條件的資料的個數 |
這些方法用法大致相同,甚至Count
方法在上文中已經介紹過,此處僅用Max
方法演示
int maxSalary = lst.Max(x => x.Salary);
Console.WriteLine(maxSalary);
該樣例會輸出所有員工的最大工資(請注意maxSalary的型別)
如果想要找到大於30歲的員工的最高工資,則可以
int maxSalary = lst.Where(x=>x.Age > 30).Max(x => x.Salary);
該方法用於對資料分組,其引數是分組條件表示式,返回值為IGrouping<TKey, TSource>
型別的泛型IEnumerable。
我們編寫程式碼來實現很具年齡分組:
IEnumerable<IGrouping<int, Employee>> items = lst.GroupBy(x => x.Age);
foreach (var item in items)
{
Console.WriteLine($"年齡為{item.Key}的分組成員有:");
foreach (var i in item)
Console.WriteLine(i);
Console.WriteLine();
}
其輸出結果為:
年齡為28的分組成員有:
ID=1, Name=jerry, Age=28, Gender=True, Salary=5000
年齡為33的分組成員有:
ID=2, Name=jim, Age=33, Gender=True, Salary=3000
ID=8, Name=jack, Age=33, Gender=True, Salary=8000
年齡為35的分組成員有:
ID=3, Name=lily, Age=35, Gender=False, Salary=9000
ID=6, Name=nancy, Age=35, Gender=False, Salary=8000
ID=7, Name=zack, Age=35, Gender=True, Salary=8500
年齡為16的分組成員有:
ID=4, Name=lucy, Age=16, Gender=False, Salary=2000
年齡為25的分組成員有:
ID=5, Name=kimi, Age=25, Gender=True, Salary=1000
通過列印我們可以發現IEnumerable
元素為IGrouping
型別,其鍵與值對應關係是一對多的。在這裡每個元素的鍵就是年齡,而值為具有相同年齡的Employee
型別的員工資料。
投影是把集合中每一項轉化為另外一種型別。
IEnumerable<string> items = lst.Where(x => x.Salary > 5000).Select(x => x.Gender ? "男" : "女");
foreach (var item in items)
Console.WriteLine(item);
輸出結果為:
女
女
男
男
匿名型別沒有名稱,所以我們沒有辦法去用型別名去宣告它,而是需要用到var
關鍵字
var items = lst.Select(e => new
{
XingMing = e.Name,
NianLing = e.Age,
Xingbie = e.Gender ? "男" : "女"
});
foreach (var item in items)
Console.WriteLine(item);
其輸出結果為:
{ XingMing = jerry, NianLing = 28, Xingbie = 男 }
{ XingMing = jim, NianLing = 33, Xingbie = 男 }
{ XingMing = lily, NianLing = 35, Xingbie = 女 }
{ XingMing = lucy, NianLing = 16, Xingbie = 女 }
{ XingMing = kimi, NianLing = 25, Xingbie = 男 }
{ XingMing = nancy, NianLing = 35, Xingbie = 女 }
{ XingMing = zack, NianLing = 35, Xingbie = 男 }
{ XingMing = jack, NianLing = 33, Xingbie = 男 }
在實際使用中,我們往往不是一定用IEnumerable
,還有可能是List
等,所以需要用到型別轉換
例如我們利用Where
方法返回工資大於6000的員工存放到IEnumerable
中,然後將其轉化為List
型別
List<Employee> lst2 = lst.Where(e => e.Salary > 6000).ToList();
此外還有ToArray
等方法,此處不過多說明。
所謂鏈式呼叫就是呼叫完一個函數(方法)後還能再後面繼續跟著呼叫其它函數(方法)。
由於LINQ絕大多數方法返回的都是IEnumerable
方法,而且絕大部分都是針對IEnumerable
介面,所以可以在呼叫方法後繼續呼叫其他方法。
lst.Where(e => e.Salary > 6000).ToList();
例如我們定義陣列
int[] nums = new int[] { 1, 2, 3, 4, 5, 6 };
在小於3的元素中選取最大值
int a = nums.Where(x=>x<3).Max();
對於上面這行nums.Where(x=>x<3).Max()
在Where
方法後加一個點然後再呼叫Max
方法的形式就叫做鏈式呼叫。
對於上述的使用Where
、Select
等擴充套件方法進行資料查詢的寫法叫做LINQ方法語法。
然而還有一種叫做查詢語法。
我們同樣定義一個陣列演示
int[] nums = new int[] { 6,5,4,3,2,1 };
我們取小於3的元素,然後進行正序排序,則用查詢語法則可以如下:
var items = from e in nums
where e < 3
orderby e
select e;
這裡需要注意,查詢語法需要以select或group子句結尾
那麼問題來了,方法語法與查詢語法有什麼區別?我們可以用方法語法寫一段相同效果的語法,然後用反編譯器(ILSpy)去看一下程式碼。
其反編譯結果給出了查詢語法的形式,然後對查詢語法生成檔案進行反編譯,發現結果相同,這說明兩種方法在編譯後沒有任何區別只是寫法不同
LINQ的基本操作大致就這些,感謝楊中科老師提供的課程