前言
為了要對於每一筆資料做特定的處理 , LinQ 方法常會在走訪資料集合的時候 , 透過執行委派來得到期望的結果. 而為求方便與簡潔 , LinQ 常使用 Lambda 來指定委派. 所以 Lambda 對於 LinQ 來說 , 非常重要.
委派
- 委派是一種方法簽章的型別
- 委派的概念類似於 C++ 的函式指標 , 也許可以想成是儲存方法的變數(?)
- 想執行委派儲存的方法 , 可直接呼叫(跟方法的呼叫方式相同)或透過執行 Invoke() 實例方法.
- 委派讓我們可以將方法當做引數傳遞給其他方法
- C# 中的委派是多重的 (鏈式委派)
- 委派的存在使得依賴可以降低到方法簽章
- 委派支援逆變
- 委派可想成是對介面作抽象 (僅在乎方法簽章型別是否正確)
- 委派家族(既有的委派型別)
- Action 家族
- Func 家族
- EventHandler 家族
- Predicate<T>
委派的繼承圖
Delegate ClassMulticastDelegate lass繼承Action, Func, EventHandler ,以及你自訂的委派...繼承
MulticastDelegate
- MulticastDelegate 為特殊類別. 只有編譯器和其他工具可以衍生自這個類別 (不是給 Programmer 用的.)
- 我們使用的委派都繼承自這個類別(包含自定義).
- 委派是多重的 , 這代表委派可以儲存不只一個方法在它的引動過程清單中.
- MulticastDelegate 具有可為複數項目組成的委派連結串列(Linked List),稱為引動過程清單. 當叫用 Invoke() 多點傳送委派時 , 依指派的順序循序呼叫引動過程清單中的方法.
委派的使用方式
使用委派的過程如下
- 建立委派一個 , 步驟如下
- 定義委派的結構 -> E.g. delegate void ShowMoneyType(string s, int x)
- 宣告一個委派變數 -> E.g. ShowMoneyType someThing
- 定義一個滿足上述委派結構的方法 -> E.g. void DisplayCash(string str , int value)
- 輸入參數個數以及型態和回傳值都必須與該委派結構相同 , 否則無法存入委派.
- 指派給委派一個實體 -> E.g. someThing=DisplayCash
- 呼叫委派實體 (可使用 invoke , 進行呼叫)
由上面的步驟 , 或許可將委派想成有三個層面
- 宣告端 : 定義委派結構
- 宣告委派和定義方法很類似. 它包含一個回傳類型和任意數量的參數. 所以此階段最主要工作就是要明確定義回傳型別和參數型別
- 邏輯端 : 設定執行步驟細節(定義滿足委派結構的方法)
- 可透過具體函式或是匿名函式來設定.
- 呼叫端 : 負責連接宣告端和邏輯端 , 並呼叫委派
- 此端決定呼叫委派的時機 (會需要建立一個委派實體 , 以便呼叫委派)
以下為指派實作細節(邏輯端)給委派的三種方式.
- 具名函式 : 有函式名的函式 lol
- 匿名函式
- 匿名方法
- Lambda運算式
C#1.0 具名函式
將已宣告的方法指派給委派.
範例
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 中引入匿名方法後 , 就不必一定要使用某一個執行個體方法或靜態方法 , 來指定委派變數. 可直接透過匿名方法來定義委派要執行的內容.
範例
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 : 輸入參數
-
只有一個參數時可不加括號 , 但複數個參數必須加上括號 , 並且參數之間要用逗號隔開.
-
(x) => x * 15 //合法 x => x * 15 //合法 (x,y) => x * y //合法
-
-
可以不用明確指定型別 , 但明確指定型別時一定要加上括號
-
(int x) => x * 15 //合法
-
-
用空括號()來表示沒有輸入參數.
-
() => 1 * 15 //合法
-
-
-
expression: 運算式
- 不使用大括號{} , 則僅能使用單行程式碼作為運算式.
- 運算式後不需要加分號 ;
- 若函數需要回傳 , expression 的運算結果代表回傳值.
-
(int x) => x * 15 //合法
-
-
{ statements }: 程式碼區塊
- statement為此函數執行的程式碼敘述.
- 程式碼區塊可以有複數行程式碼 , 另外每行最後要加分號 ;
- 若函數需要回傳 , 使用 return 關鍵字.
-
(int x) => { var value = x * 15; return value; } //合法
-
比較 Lambda 與匿名方法的差異
- 不用透過 delegate 關鍵字來建立匿名函式.
- 除非會影響可讀性 , 否則不需要定義參數的型別 .
- 括弧可以省略 .
參考此資料 , 微軟也建議我們開始使用 Lambda 取代匿名方法
我們建議使用 Lambda 運算式,因為它們提供更簡潔且更具表達性的方式來撰寫內嵌程式碼.
範例
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);也就是說,委派可以在它的引動過程清單中包含一個以上的項目。
由上述參考可知 , 委派有一個清單儲存多個方法實體 , 並且再呼叫委派時 , 依序呼叫這些方法.
範例
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();
}
輸出結果
GetInvocationList
若是委派具有回傳值 , 並需要個別取得其每一個方法的結果 , 可使用 GetInvocationList ()
範例
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
委派參數與回傳值型別變異性
- 委派預設機制
- 實值型別只支援不變性
- 參考型別支援共變與逆變
- 自定義委派使用泛型
- 對於僅作為輸出的型別參數 , 可考慮加入 out 修飾詞使其支援共變性
- 對於僅作為輸入的型別參數 , 可考慮加入 in 修飾詞使其支援逆變性
- Action 與 Func 皆已完整宣告
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 語法 , 即時的實體化委派並呼叫.
以下簡易地實作 LinQ 的 Where 方法來作為此篇的結論
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
若有謬誤 , 煩請告知 , 新手發帖請多包涵