LINQ ,發音同 link, 中文稱作「語言整合查詢」,英文是 Language Integrated Query
- LINQ 於 .NET Framework 3.5 新加入
- 為了更方便處理集合物件
- 用類似 SQL 指令的方式來處理 data
我們可以在 .NET 專案的各個角落看見 LINQ 的身影
LINQ 的類型
- LINQ to Objects (LINQ to Collection):最基本的功能,包含彙總、過濾 ⋯⋯ 等
- LINQ to SQL:LINQ 功能的 SQLServer 資料庫版本,已由 Entity Framework 取代
- LINQ to XML:用來剖析與查詢 XML 內的元素
- LINQ to DataSet (LINQ to ADO.NET):將 DataSet 與 DataTable 的 data 轉換成
IEnumerable
的集合物件,供 LINQ to Objects 查詢使用
LINQ 的基礎
- 以
IEnumerable
及IEnumerable<T>
為兩個 interface 為基礎的實作 - .NET Framework 裡所有的集合物件都能使用 LINQ 進行查詢
- 要
using System.Linq
方可使用 LINQ
物件初始子(Object Initializer)
Object initializers allow you to assign values to the fields or properties at the time of creating an object without invoking a constructor.
- 利用物件初始子建立物件,可以省去要先建構函式的不便
- 當使用物件初始子後,編譯器會自動新增這個新的類別,稱為匿名型別(Anonymous type)
在 JavaScript 的世界裡,我們原本要這樣建立物件:
const cat = new Object();
cat.name = "Sasha";
cat.age = 14;
但用我們較常用的建立物件的方式還是這個——物件實字(Object Literal):
const cat = {
name: "Sasha",
age: 14,
};
在 TypeScript 加了型別的限制,寫法上幾乎一樣:
const cat: Cat = {
name: "Sasha",
Age: 14,
};
但在 C# 中,必須先宣告一個 class,才能用建構函式 new
去建立一個物件:
class Cat
{
string name;
int age;
// constructor
public Cat(string catName, int catAge)
{
name = catName;
age = catAge;
}
}
var cats = new List<Cat>();
cats.Add(new Cat("Sylvester", 8));
cats.Add(new Cat("Whiskers", 2));
cats.Add(new Cat("Sasha", 14));
而 LINQ 則略過了宣告 class 的階段,直接用型別去建立物件, 寫法比原本的簡短許多:
List<Cat> cats = new List<Cat>
{
new Cat() { name = "Sylvester", Age = 8 },
new Cat() { name = "Whiskers", Age = 2 },
new Cat() { name = "Sasha", Age = 14 },
}
匿名型別的限制
- 一般只能在同一個函式內使用
- 只能有屬性,不能有方法、事件或欄位
- 兩個匿名型別的相等,必須所有屬性的值都相等
- 利用初始子初始化後的匿名型別就會變成唯讀,不可再變動
型別推論 var
使用 LINQ 語法並以 select new
來產生結果的查詢,通常會用 var
表示
var
代表要求編譯器做型別推論(inference)
var 型別的限制
var
賦值陳述式右邊不可為null
,否則無法推論- 只能用在區域變數宣告,不可用於全域、類別層級的變數、函式回傳值
- 不可用於匿名委派或方法群組
yield 指令與延遲查詢
- 所有的陣列與集合物件都包含
IEnumerable<T>
的實作 IEnumerable<T>
中定義了一個迭代運算需要的GetEnumerator()
yield
指令可以以傳回每個元素的方式,自動產生IEnumerable<T>
物件yield
會在執行yield
的區塊產生一個迭代運算的有限狀態機,狀態機會控制巡覽(遍歷)時所做的存取動作,這也就是延遲查詢(Deferred Query)或延遲執行(Deferred Execution)的機制
原本增加元素的寫法:
private static IEnumerable<int> GetCollection1()
{
List<int> list = new List<int>();
for(int i = 1; i <= 5; i++)
list.Add(i);
return list;
}
yield
的寫法:
private static IEnumerable<int> GetCollection2()
{
for(int i = 1; i <=5; i++)
yield return i;
}
Fluent Interface
串聯多個 LINQ 函式,叫做 fluent interface
- 透過呼叫方法來定義物件內容
- 物件會自我參考(self-referential),且新的物件會和最後一個等價
- 透過回傳
void
,或非 fluent interface 的物件結束
範例:
var query = list.Where(c => c < 10000).Select(c => new { id = c });
這種寫法就是 functional programming 及柯理化(Curry)的概念
Lambda 運算式
Lambda 運算式首要解決的問題就是簡化委派的編寫
至於什麼是委派?書中是這樣描述的:
委派(delegate)機制是一種封裝函式的機制,讓程序員可以將函式視為物件,可將它傳遞給需要的物件使用,在 C/C++ 時成為函式指標(function pointer)。物件可在需要時呼叫該函式,並傳入相應的參數,實際的處理則由封裝該函式的物件進行,它也是被用來實作回呼(callback)機制的主要方法之一
這種寫法在 JavaScript 很常見, 因為在 JavaScript 裡,function 被視為一級函式(First-class Function), 所以 function 可以儲存在任何能夠儲存基礎型別的地方,例如:變數、物件裡面
Lambda 運算式的格式
Lambda 運算式分為三種:
- 運算式型 Lambda(Expression Lambdas)
- 陳述式型 Lambda(Statement Lambdas)
- 非同步型 Lambda(Asynchronous Lambdas)
以下是三種 Lambda 的寫法:
運算式型 Lambda:
(o) => o + 1;
(a, b) => a + b;
() => 3;
陳述式型 Lambda:
(o) => {
Console.WriteLine(o.ToString());
};
非同步型 Lambda 基本上與上面兩種相同,只差在前面要加上 async
指令,Lambda 裡面非同步的地方要加上 await
這看起來跟 JavaScript 的箭頭函式(Arrow Function)一模一樣嘛~
LINQ 陳述式
LINQ 陳述式就像是使用 SQL 指令一樣,查詢處集合物件我們想要的資料,例如遺下範例:
// Student collection
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 13} ,
new Student() { StudentID = 2, StudentName = "Mora", Age = 21 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 18 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20} ,
new Student() { StudentID = 5, StudentName = "Ron" , Age = 15 }
};
// LINQ Query Syntax to find out teenager students
var teenAgerStudent = from s in studentList
where s.Age > 12 && s.Age < 20
select s;
LINQ 函式
LINQ 函式是一種 functional chaining 的寫法,在前面也有展示過
Where():查詢結果過濾
用條件過濾集合物件
List<int> list1 = new List<int>() { 6, 4, 2, 7, 9, 0 };
// 過濾大於 5 的 item
list1.Where(c => c > 5);
// 過濾介於 1 ~ 5 之間的 item
list1.Where(c => c >= 1 && c <= 5);
// 或是可以將兩個條件分開來寫:
list1.Where(c => c >= 1).Where(c => c <= 5);
其實等同於 JS 的 filter
函式:
const list1 = [6, 4, 2, 7, 9, 0];
list1.filter((c) => c > 5);
list1.filter((c) => c >= 1 && c <= 5);
list1.filter((c) => c >= 1).filter((c) => c <= 5);
Select()、SelectMany():提取資料
Select()
Select()
與 Where()
相似,但要回傳一個物件,而非布林值:
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 18 } ,
new Student() { StudentID = 2, StudentName = "Mora", Age = 21 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 18 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20 } ,
new Student() { StudentID = 5, StudentName = "Ron" , Age = 21 }
};
var selectResult = studentList.Select(s => new { Name = s.StudentName ,
Age = s.Age });
除非要重組資料,或寫 LINQ 陳述式,不然使用頻率並不高
SelectMany()
SelectMany()
處理兩個集合物件來源的資料提取,類似 SQL 的 CROSS JOIN
,也就是取「聯集」
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", StandardID = 1 },
new Student() { StudentID = 2, StudentName = "Mora", StandardID = 1 },
new Student() { StudentID = 3, StudentName = "Bill", StandardID = 2 },
new Student() { StudentID = 4, StudentName = "Ram" , StandardID = 2 },
new Student() { StudentID = 5, StudentName = "Ron" }
};
IList<Standard> standardList = new List<Standard>() {
new Standard(){ StandardID = 1, StandardName="Standard 1"},
new Standard(){ StandardID = 2, StandardName="Standard 2"},
new Standard(){ StandardID = 3, StandardName="Standard 3"}
};
var innerJoin = studentList.Join(// outer sequence
standardList, // inner sequence
student => student.StandardID, // outerKeySelector
standard => standard.StandardID, // innerKeySelector
(student, standard) => new // result selector
{
StudentName = student.StudentName,
StandardName = standard.StandardName
});
GroupBy()、ToLookup():群組資料
將查詢到的資料群組化
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 18 } ,
new Student() { StudentID = 2, StudentName = "Steve", Age = 21 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 18 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20 } ,
new Student() { StudentID = 5, StudentName = "Abram" , Age = 21 }
};
var groupedResult = studentList.GroupBy(s => s.Age);
foreach (var ageGroup in groupedResult)
{
Console.WriteLine("Age Group: {0}", ageGroup.Key); //Each group has a key
foreach(Student s in ageGroup) //Each group has a inner collection
Console.WriteLine("Student Name: {0}", s.StudentName);
}
印出來的結果:
AgeGroup: 18
StudentName: John
StudentName: Bill
AgeGroup: 21
StudentName: Steve
StudentName: Abram
AgeGroup: 20
StudentName: Ram
ToLookup()
的用法跟 GroupBy()
一樣
GroupBy() 與 ToLookup() 的差異
GroupBy()
本身具有延遲執行的特性,而ToLookup()
沒有GroupBy()
也可以用於 LINQ 陳述式,ToLookup()
則不行
Join() 與 GroupJoin():連結資料
Join()
使用的是 INNER JOIN
的概念,也就是取「交集」
至於 GroupJoin()
,則是實作 LEFT OUTER JOIN
,或稱作 LEFT JOIN
整理三種 JOIN
及對應的函式:
JOIN Type | LINQ Function |
---|---|
CROSS JOIN | SelectMany() |
INNER JOIN | Join() |
LEFT JOIN | GroupJoin() |
這裡因專注在 ASP.NET MVC 本身,所以詳細的 SQL 語法就待有空的時候再深入了解
OrderBy() 與 ThenBy():資料排序
排序是很常見的功能,有時候我們會先在 API 送資料之前就先做好排序
Orderby
是做遞增的排序
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 18 } ,
new Student() { StudentID = 2, StudentName = "Steve", Age = 15 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 25 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20 } ,
new Student() { StudentID = 5, StudentName = "Ron" , Age = 19 }
};
var studentsInAscOrder = studentList.OrderBy(s => s.StudentName);
一些注意事項
- 若做遞減的排序,可以使用
OrderByDescending()
這個函式 ThenBy()
則是可以接續在OrderBy()
之後,做第二個條件的排序ThenBy()
也是遞增排序,遞減的排序可以使用ThenByDescending
- 做多重條件排序時,一定首先呼叫
OrderBy()
,再接續使用ThenBy()
一次或多次 - 若重複使用
OrderBy()
,排序結果會是最後一個OrderBy()
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 18 } ,
new Student() { StudentID = 2, StudentName = "Steve", Age = 15 } ,
new Student() { StudentID = 3, StudentName = "Bill", Age = 25 } ,
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20 } ,
new Student() { StudentID = 5, StudentName = "Ron" , Age = 19 },
new Student() { StudentID = 6, StudentName = "Ram" , Age = 18 }
};
var thenByResult = studentList.OrderBy(s => s.StudentName).ThenBy(s => s.Age);
var thenByDescResult = studentList.OrderBy(s => s.StudentName).ThenByDescending(s => s.Age);
擷取集合
LINQ 本身支援四種不同的集合產生方式,包含:
- 陣列
ToArray()
- 串列
ToList()
- 字典集合
ToDictionary()
- Lookup 類別
ToLookup()
劃分並擷取集合
有時候為了增進效能,所以需要做資料的切分(像是處理分頁 pagination 的資料),這時候可以用以下方法:
Skip()
:用來在集合中跳躍,不用透過巡覽(遍歷)的方式來移動,在大型集合中可以節省時間SkipWhile()
:同Skip()
,但是多了判斷式,跳過符合條件的元素Take()
:用來回傳特定數量的元素,適合使用在分頁(pagination)的功能TakeWhile()
:跟SkipWhile()
概念類似,也是在取出的元素中,再過濾出滿足條件的部分
在第四章會詳細說明(可是第四章會跳過)
資料集合的運算
集合運算分為四種:聯集(Union
)、交集(Intersect
)、補集(Except
)、獨特(Distinct
)
在看下去之前,先稍微複習一下高中數學的集合運算
Union 聯集
IList<string> strList1 = new List<string>() { "One", "Two", "three", "Four" };
IList<string> strList2 = new List<string>() { "Two", "THREE", "Four", "Five" };
var result = strList1.Union(strList2);
foreach(string str in result)
Console.WriteLine(str);
Output:
One
Two
three
THREE
Four
Five
Intersect 交集
IList<string> strList1 = new List<string>() { "One", "Two", "Three", "Four", "Five" };
IList<string> strList2 = new List<string>() { "Four", "Five", "Six", "Seven", "Eight"};
var result = strList1.Intersect(strList2);
foreach(string str in result)
Console.WriteLine(str);
Output:
Four
Five
Except 補集
差集的概念
IList<string> strList1 = new List<string>(){"One", "Two", "Three", "Four", "Five" };
IList<string> strList2 = new List<string>(){"Four", "Five", "Six", "Seven", "Eight"};
var result = strList1.Except(strList2);
foreach(string str in result)
Console.WriteLine(str);
strList1
對 strList2
的差集運算
$$ strList1 \setminus strList2 $$
Output:
One
Two
Three
IList<string> strList1 = new List<string>(){"One", "Two", "Three", "Four", "Five" };
IList<string> strList2 = new List<string>(){"Four", "Five", "Six", "Seven", "Eight"};
var result = strList1.Except(strList2);
foreach(string str in result)
Console.WriteLine(str);
Output:
One
Two
Three
Distinct 獨特
將集合中的重複值過濾後取出
IList<string> strList = new List<string>(){ "One", "Two", "Three", "Two", "Three" };
IList<int> intList = new List<int>(){ 1, 2, 3, 2, 4, 4, 3, 5 };
var distinctList1 = strList.Distinct();
foreach(var str in distinctList1)
Console.WriteLine(str);
var distinctList2 = intList.Distinct();
foreach(var i in distinctList2)
Console.WriteLine(i);
Output:
One
Two
Three
1
2
3
4
5
或是,我們用一張圖解釋:
存取元素的運算
經過集合運算之後,就開始提取其中想要的部分,有以下方法可以使用:
First()
:回傳第一個元素,若沒有則回傳null
Last()
:回傳最後一個元素,若沒有則回傳null
FirstOrDefault()
:回傳第一個元素,若沒有則回傳其型別預設值LastOrDefault()
:回傳最後一個元素,若沒有則回傳其型別預設值Single()
SingleOrDefault()
ElementAt()
:依index
存取元素ElementAtOrDefault()
Contains()
Count()
:集合的長度,相當於 JS 的Array.prototype.length
Any()
:判斷是否為空(集合長度為 1)All()
:利用All()
及傳入的條件判斷所有元素都符合條件,相當於 JS 的Array.prototype.every()
OfType()
Cast()
聚合與彙總運算
聚合運算(aggregation)的常用函式有:
Max()
Min()
Sum()
Average()
在 JS 的話,大概要用 Array.prototype.reduce()
或 Math
的方法去實踐
或是可以用 Aggregate()
自訂聚合規則:
// Student collection
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", Age = 13} ,
new Student() { StudentID = 2, StudentName = "Mora", Age = 21 },
new Student() { StudentID = 3, StudentName = "Bill", Age = 18 },
new Student() { StudentID = 4, StudentName = "Ram" , Age = 20},
new Student() { StudentID = 5, StudentName = "Ron" , Age = 15 }
};
int SumOfStudentsAge = studentList.Aggregate<Student, int>(0, (totalAge, s) => totalAge += s.Age);
從以上範例可以看出,Aggregate()
就如同 JS 的 Array.prototype.reduce()
的用法
遠端查詢:IQueryable<T>
與 IEnumerable<T>
前面所提到的所有 LINQ 函式,都是基於 IEnumerable<T>
來實作
而 IEnumerable<T>
的特性是,要有實際存在的集合才能操作
對於來自遠端的資料而言,就會是一個問題,因為當資料還沒回來的時候,我們什麼都沒拿到
此時則無法用 IEnumerable<T>
來處理
因此,微軟提供一個介於 IEnumerable<T>
與資料來源之間的中介提供者(middle provider? middleware?)
這個 provider 接受來自 LINQ 的操作(IEnumerable<T>
),並將 LINQ 指令轉換成運算式
再交給遠端資料來源(例如:DB)的查詢提供者(Query Provider),將這個運算式轉換成遠端可執行的查詢指令(例如:SQL 語法?)
然後交由遠端去執行這段查詢指令
根據微軟官網的定義,IQueryable
這個 interface 繼承自 IEnumerable
,因此也可以使用 Enumerable
所擁有的方法,之所以 Queryable
class 要實作與 Enumerable
相同的方法,是希望能夠使用完全相同的語法使用 IQueryable<T>
與 IEnumerable<T>
public interface IQueryable: System.Collections.IEnumerable
IQueryable<T>
與 IQueryProvider
的查詢資料流程如下:
AsEnumerable()
與 AsQueryable()
透過 AsEnumerable()
這個方法,就可以將 IQueryable<T>
的資料,轉換成 IEnumerable<T>
因爲在轉換成 IEnumerable<T>
之後,就變成是在記憶體内做集合物件的操作,少了將查詢傳回遠端的過程,因此加快了速度
而 AsQueryable()
則是反向操作
Expression
Expression 中文稱為「運算式」
前面所提到的,IQueryable<T>
會將查詢指令交給 IQueryProvider
去執行遠端的查詢動作
因為未轉換 LINQ 指令,IQueryProvider
並無法解讀,所以必須經過 LINQ 運算式的轉換
這時 Expression 就擔任這個轉譯的角色
另外前面提過的 Lambda Expression,其核心也是由 Expression 所提供
補充
「立即執行」與「延遲執行」的差異
在 LINQ 當中,有其 LINQ 函式可以分為兩種類型:延遲執行及立即執行
延遲執行
英文稱作:Deferred Execution
屬於延遲執行的函式(Deferred or Lazy Operators)有:Select()
、 SelectMany()
、Where()
、Take()
、Skip()
⋯⋯ 等
延遲執行的意思是,在使用 LINQ 查詢的當下,不會馬上進行查詢與運算,直到我們需要使用到查詢結果的時候,才會開始執行
立即執行
英文稱作:Immediate Execution
屬於立即執行的函式(Immediate or Greedy Operators)有:Count()
、Average()
、Min()
、Max()
、First()
、Last()
、ToArray()
、ToList()
⋯⋯ 等
IList<Student> studentList = new List<Student>() {
new Student() { StudentID = 1, StudentName = "John", age = 13 } ,
new Student() { StudentID = 2, StudentName = "Steve", age = 15 } ,
new Student() { StudentID = 3, StudentName = "Bill", age = 18 } ,
new Student() { StudentID = 4, StudentName = "Ram" , age = 12 } ,
new Student() { StudentID = 5, StudentName = "Ron" , age = 21 }
};
// 這裡只定義查詢,尚未執行查詢
var teenAgerStudents = from s in studentList
where s.age > 12 && s.age < 20
select s;
// 這裡開始執行查詢,從 studentList 中拿出需要的資料
foreach (Student teenStudent in teenAgerStudents)
Console.WriteLine("Student Name: {0}", teenStudent.StudentName);
高中數學的復習:集合運算
常見的集合運算有:交集、聯集、差集、對稱差 ⋯⋯ 等
交集 Intersection
數學符號:
$$ A \cap B$$
聯集 Union
數學符號:
$$ A \cup B$$
差集 Complement / Set Difference
差集存在兩種定義:相對差集(差集)及絕對差集(補集)
相對差集(差集)
數學符號:
$$ A \setminus B \thinspace 或 \thinspace A - B $$
絕對差集(補集)
數學符號:
$$ A^C \thinspace 或 \thinspace A’ $$
對稱差 Symmetric difference
數學符號:
$$ A \bigtriangleup B \thinspace 或 \thinspace A \oplus B $$
切記不要將數學的集合概念套用到 TypeScript 上!TS 的 Union 跟數學的 Union 定義上並不一樣!
參考資料
- Standard Query Operators
- Deferred Execution vs Immediate Execution in LINQ - Dot Net Tutorials
- Difference Between Array And ArrayList In C#
- Deferred Execution of LINQ Query
- Set theory - Wikipedia
- JavaScript Arrays — Finding The Minimum, Maximum, Sum, & Average Values | by Brandon Morelli | codeburst
- IEnumerable and IQueryable in C# - Dot Net Tutorials
- IQueryable Interface (System.Linq) | Microsoft Docs
- 仔細體會 yield 的甜美: yield 介紹 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天