owned this note
owned this note
Published
Linked with GitHub
---
tags: LinQ, LinQ基礎 , C#
---
# LINQ基礎 - 委派與Lambda
### 前言
為了要對於每一筆資料做特定的處理 , LinQ 方法常會在走訪資料集合的時候 , 透過執行委派來得到期望的結果. 而為求方便與簡潔 , LinQ 常使用 Lambda 來指定委派. 所以 Lambda 對於 LinQ 來說 , 非常重要.
### [委派](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/delegates/)
- 委派是一種方法簽章的型別
- 委派的概念類似於 C++ 的函式指標 , 也許可以想成是儲存方法的變數(?)
- 想執行委派儲存的方法 , 可直接呼叫(跟方法的呼叫方式相同)或透過執行 Invoke() 實例方法.
- 委派讓我們可以將方法當做引數傳遞給其他方法
- C# 中的委派是多重的 (鏈式委派)
- 委派的存在使得依賴可以降低到方法簽章
- 委派支援逆變
- 委派可想成是對介面作抽象 (僅在乎方法簽章型別是否正確)
- 委派家族(既有的委派型別)
- Action 家族
- Func 家族
- EventHandler 家族
- Predicate<T>
### 委派的繼承圖
```graphviz
digraph hierarchy {
nodesep=0.5 // increases the separation between nodes
node [color=Red,fontname=Courier,shape=box] //All nodes will this shape and colour
edge [color=Blue, style=line] //All the lines look like this
T [label="Delegate Class"] // node T
P [label="MulticastDelegate lass"] // node P
C [label="Action, Func, EventHandler ,以及你自訂的委派..."]
T->P [label="繼承", fontcolor=darkgreen]
P->C [label="繼承", fontcolor=darkgreen]
}
```
### [MulticastDelegate](https://docs.microsoft.com/zh-tw/dotnet/api/system.multicastdelegate?view=netcore-3.1)
- MulticastDelegate 為特殊類別. **只有**編譯器和其他工具可以衍生自這個類別 (不是給 Programmer 用的.)
- 我們使用的委派都繼承自這個類別(包含自定義).
- 委派是多重的 , 這代表委派可以儲存不只一個方法在它的引動過程清單中.
- MulticastDelegate 具有**可為複數項目組成的委派連結串列(Linked List),稱為引動過程清單**. 當叫用 Invoke() 多點傳送委派時 , 依指派的順序循序呼叫引動過程清單中的方法.
### 委派的使用方式
使用委派的過程如下
* 建立委派一個 , 步驟如下
1. 定義委派的結構 -> E.g. delegate void ShowMoneyType(string s, int x)
2. 宣告一個委派變數 -> E.g. ShowMoneyType someThing
3. 定義一個滿足上述委派結構的方法 -> E.g. void DisplayCash(string str , int value)
- 輸入參數個數以及型態和回傳值都必須與該委派結構相同 , 否則無法存入委派.
5. 指派給委派一個實體 -> E.g. someThing=DisplayCash
* 呼叫委派實體 (可使用 invoke , 進行呼叫)
由上面的步驟 , 或許可將委派想成有三個層面
- 宣告端 : 定義委派結構
- 宣告委派和定義方法很類似. 它包含一個回傳類型和任意數量的參數. 所以此階段最主要工作就是要明確定義回傳型別和參數型別
- 邏輯端 : 設定執行步驟細節(定義滿足委派結構的方法)
- 可透過具體函式或是匿名函式來設定.
- 呼叫端 : 負責連接宣告端和邏輯端 , 並呼叫委派
- 此端決定呼叫委派的時機 (會需要建立一個委派實體 , 以便呼叫委派)
以下為**指派**實作細節(邏輯端)給委派的三種方式.
1. 具名函式 : 有函式名的函式 lol
2. 匿名函式
* 匿名方法
* Lambda運算式
#### C#1.0 具名函式
將已宣告的方法指派給委派.
##### 範例
```C#
static class Program
{
// 邏輯端 - 實際要做的事情細節
public static void DisplayCash(string name , int value)
{
Console.WriteLine($"{name} have {value}");
}
static void Main(string[] args)
{
// 呼叫端 - 傳入參數以實體化委派(做了someThing=DisplayCash這個動作)
// 然後執行doSomeThing()
new City().DoSomeThing(DisplayCash);
Console.ReadLine();
}
}
public class City
{
private readonly int _money = 150;
private readonly string _name = "City";
// 宣告端 - 宣告委派
public delegate void ShowMoneyType(string s , int x);
public void DoSomeThing(ShowMoneyType someThing) => someThing(_name , _money);
}
```
---
#### C#2.0 匿名方法 delegate 關鍵字
格式 : delegate (arguments) { statements }
- delegate: 匿名方法的保留字
- arguments: 輸入參數
- 輸入參數可有多個. 使用逗號隔開. 但需要**定義參數型別**.
- statements: 此函式執行的程式碼片段
在 C# 2.0 中引入匿名方法後 , 就不必一定要使用某一個執行個體方法或靜態方法 , 來指定委派變數. **可直接透過匿名方法來定義委派要執行的內容**.
##### 範例
```C#
static class Program
{
static void Main(string[] args)
{
// 使用匿名方法即時定義執行細節.
new City().DoSomeThing(delegate (string name, int money)
{
Console.WriteLine($"{name} have {money}");
});
Console.ReadLine();
}
}
public class City
{
private readonly int _money = 150;
private readonly string _name = "City";
public delegate void ShowMoneyType(string s, int x);
public void DoSomeThing(ShowMoneyType someThing) => someThing(_name, _money);
}
```
---
#### C#3.0 Lambda
格式 : (arguments) => expression | { statements }
- arguments : 輸入參數
- 只有一個參數時可不加括號 , 但複數個參數必須加上括號 , 並且參數之間要用逗號隔開.
- ```C#
(x) => x * 15 //合法
x => x * 15 //合法
(x,y) => x * y //合法
```
- **可以不用明確指定型別** , 但明確指定型別時一定要加上括號
- ```C#
(int x) => x * 15 //合法
```
- 用空括號()來表示沒有輸入參數.
- ```C#
() => 1 * 15 //合法
```
- expression: 運算式
- 不使用大括號{} , 則僅能使用單行程式碼作為運算式.
- 運算式後**不需要加分號 ;**
- 若函數需要回傳 , expression 的運算結果代表回傳值.
- ```C#
(int x) => x * 15 //合法
```
- { statements }: 程式碼區塊
- statement為此函數執行的程式碼敘述.
- 程式碼區塊可以有複數行程式碼 , 另外每行**最後要加分號 ;**
- 若函數需要回傳 , 使用 return 關鍵字.
- ```C#
(int x) => {
var value = x * 15;
return value;
} //合法
```
#### 比較 Lambda 與匿名方法的差異
1. **不用透過 delegate 關鍵字**來建立匿名函式.
2. 除非會影響可讀性 , 否則**不需要定義參數的型別** .
3. 括弧可以省略 .
參考此[資料](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/statements-expressions-operators/anonymous-functions) , 微軟也建議我們開始使用 Lambda 取代匿名方法
> 我們建議使用 Lambda 運算式,因為它們提供更簡潔且更具表達性的方式來撰寫內嵌程式碼.
##### 範例
```C#
static class Program
{
static void Main(string[] args)
{
// 使用Lambda即時定義執行細節.
new City().DoSomeThing((name, money) => Console.WriteLine($"{name} have {money}"));
Console.ReadLine();
}
}
public class City
{
private readonly int _money = 150;
private readonly string _name = "City";
public delegate void ShowMoneyType(string s, int x);
public void DoSomeThing(ShowMoneyType someThing) => someThing(_name, _money);
}
```
---
#### 委派的多重特性
> 表示多重傳送的委派 (Delegate);也就是說,委派可以在它的引動過程清單中包含一個以上的項目。
由上述參考可知 , 委派有一個清單儲存多個方法實體 , 並且再呼叫委派時 , 依序呼叫這些方法.
##### 範例
```C#
public delegate void Print();
public static Print print = null;
static void Main(string[] args)
{
print += () => Console.WriteLine("Test1");
print += () => Console.WriteLine("Test2");
print += () => Console.WriteLine("Test3");
print();
Console.ReadKey();
}
```
輸出結果
- ![xWJm54q.png](https://github.com/s0920832252/LinQ-Note/blob/master/Resources/xWJm54q.png?raw=true)
#### GetInvocationList
若是委派具有回傳值 , 並需要個別取得其每一個方法的結果 , 可使用 GetInvocationList ()
###### 範例
```C#
public delegate int Math(int num);
public static Math math = null;
static void Main(string[] args)
{
math += (num) => num + 1;
math += (num) => num - 1;
math += (num) => num * 1;
foreach (Math deleglateItem in math.GetInvocationList())
{
Console.WriteLine(deleglateItem.Invoke(10));
}
Console.ReadKey();
}
```
##### 輸出
11
9
10
---
#### [委派參數與回傳值型別變異性](https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/covariance-contravariance/variance-in-delegates)
- 委派預設機制
- 實值型別只支援不變性
- 參考型別支援共變與逆變
- 自定義委派使用泛型
- 對於僅作為輸出的型別參數 , 可考慮加入 out 修飾詞使其支援共變性
- 對於僅作為輸入的型別參數 , 可考慮加入 in 修飾詞使其支援逆變性
- Action 與 Func 皆已完整宣告
```C#
delegate object ValDelegate(int i);
delegate object MyDelegate(string s);
static object SimulateDelegate(string s) => InObjOutStr(s);
static string InObjOutStr(object obj) => $"Invoke InObjOutStr {obj.ToString()}";
static int InStrOutInt(string s) => 3;
static void Main(string[] args)
{
// 參考型別支援共變與逆變
var func = new MyDelegate(InObjOutStr);
// 支援實作的機制大概是這樣
var result = SimulateDelegate("123");
// 實值型別只支援不變性
//var f = new ValDelegate(InObjOutStr); // 實質型別不支援逆變
//var func = new MyDelegate(InStrOutInt); // 實質型別不支援共變
Console.ReadLine();
}
```
---
### 委派與介面的選擇
##### 使用委派
- 使用事件設計模式時(EventHandle)
- 封裝的方法是靜態時 , 使用委派.
- C# 8.0 以前 , 介面內不能有靜態成員
- C# 8.0 以後 , 靜態方法是跟著介面名稱走的 , 享受不到原本使用介面的好處. 例如多型.
- 呼叫端不需要存取方法實作所在之物件的其他屬性、方法或介面
- 想要易於撰寫時
- 類別可能需要同樣簽章方法的多個實作時
##### 使用介面
- 存在有可能被呼叫的相關方法群組時
- 類別只需要方法的一個實作
- 實作此介面的類別可能有需要將介面轉型成另一介面或類別
- 要實作的方法會連結至類別的型別或類別本身 , 例如比較方法
---
### 結論
想要建立一個符合委派結構的實體 , 從一開始 C#1.0 只能透過指定一個具名函式 , 到 C#2.0 可以使用 delegate 關鍵字以建立匿名方法來定義匿名函式 , 到 C#3.0 可以使用**更簡易**的 Lambda 語法來定義匿名函式
透過 Lambda , 我們不必再額外定義一個方法去作為具名函式了 , 而是可以再需要呼叫委派時 , 馬上透過 Lambda 語法 , **即時**的實體化委派並呼叫.:crying_cat_face:
以下簡易地實作 LinQ 的 Where 方法來作為此篇的結論
```C#
public delegate bool CityPredicate<TSource>(TSource item);
static IEnumerable<TSource> MyWhere<TSource>(this IEnumerable<TSource> source, CityPredicate<TSource> predicate)
{
foreach (var item in source)
{
if (predicate(item))
{
yield return item;
}
}
}
static void Main(string[] args)
{
List<int> vs = new List<int>() { 5, 4, 8, 7 };
foreach (var item in vs.MyWhere(number => number >= 5))
{
Console.WriteLine(item);
}
Console.ReadKey();
}
```
##### 輸出結果
5
8
7
---
### Thank you!
You can find me on
- [GitHub](https://github.com/s0920832252)
- [Facebook](https://www.facebook.com/fourtune.chen)
若有謬誤 , 煩請告知 , 新手發帖請多包涵
# :100: :muscle: :tada: :sheep: