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 人的一天