LINQ ,發音同 link, 中文稱作「語言整合查詢」,英文是 Language Integrated Query

  • LINQ 於 .NET Framework 3.5 新加入
  • 為了更方便處理集合物件
  • 用類似 SQL 指令的方式來處理 data

我們可以在 .NET 專案的各個角落看見 LINQ 的身影

linq everywhere

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 的基礎

  • IEnumerableIEnumerable<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 運算式分為三種:

  1. 運算式型 Lambda(Expression Lambdas)
  2. 陳述式型 Lambda(Statement Lambdas)
  3. 非同步型 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 ,也就是取「聯集」

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 的概念,也就是取「交集」

inner join

至於 GroupJoin(),則是實作 LEFT OUTER JOIN,或稱作 LEFT JOIN

left join

整理三種 JOIN 及對應的函式:

JOIN TypeLINQ Function
CROSS JOINSelectMany()
INNER JOINJoin()
LEFT JOINGroupJoin()

這裡因專注在 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);

strList1strList2 的差集運算

$$ 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

或是,我們用一張圖解釋:

linq distinct

存取元素的運算

經過集合運算之後,就開始提取其中想要的部分,有以下方法可以使用:

  • 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 的查詢資料流程如下:

graph LR A[Client] -->|LINQ 指令| B[Queryable] B -->|LINQ 運算式| C[IQueryProvider] C -->|遠端查詢指令| D[遠端資料]

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

intersect

數學符號:

$$ A \cap B$$

聯集 Union

union

數學符號:

$$ A \cup B$$

差集 Complement / Set Difference

差集存在兩種定義:相對差集(差集)及絕對差集(補集)

相對差集(差集)

set difference

數學符號:

$$ A \setminus B \thinspace 或 \thinspace A - B $$

絕對差集(補集)

complement

數學符號:

$$ A^C \thinspace 或 \thinspace A’ $$

對稱差 Symmetric difference

symmetric difference

數學符號:

$$ A \bigtriangleup B \thinspace 或 \thinspace A \oplus B $$

切記不要將數學的集合概念套用到 TypeScript 上!TS 的 Union 跟數學的 Union 定義上並不一樣!

參考資料