再論 Delegate 與 Lambda

Delegate 和 Lambda 的概念在 2008 年就出來了,Delegate 更是最早最早 (.NET 1.0) 時就已經有的概念,隨著技術的演進,Lambda 讓 Delegate 變得更好使用也更容易發揮,但似乎有些人不這麼認為,所以我還是再講一些吧。

這裡不談論太多的演進,因為這個早在 2012 年我就寫過一篇了,傳送門在這

Delegate 是 .NET Framework 裡面的事件 (event) 最重要的基礎,其根本的概念就是由外部指定一個自己的處理器,對於物件本身而言,不需要特別的去實作那些處理器,只要外部傳入一個處理器的指標 (也就是函式指標),物件利用這個指標去叫用該處理器即可, 這個概念在 C/C++ 中就已經有了,但因為 C# 本身 "萬物即物件" 的基本要求,不能用指標方式存在 (除非開啟 unsafe 模式),那勢必 .NET 團隊要設計一個機制讓 C# 程式可以使用函式指標的方法,而這個方法就被封裝成 Delegate (委派)。

而 Delegate 成形之後,在 .NET 裡面只要是具備回呼 (Callback) 要求的功能,就都可以使用 Delegate 來做,而 .NET 內的事件正是需要 callback 機制的功能,因此在 .NET 內的事件都是用 Delegate 來做的,Delegate 在實作上會強制要求叫用 Delegate 的程式必須要傳入它所指定的參數,所以 .NET 有很多地方的事件處理程式的參數都長得千篇一律:(object sender, EventArgs e),原因就是因為 Delegate 的關係,就算實作方法的程式不會用到,但呼叫端還是要傳入這些參數。

Delegate 和 Lambda 到底是什麼關係

我們先來看看什麼是 Lambda 好了。

λ演算(英語:lambda calculus,λ-calculus)是一套從數學邏輯中發展,以變數綁定和替換的規則,來研究函式如何抽象化定義、函式如何被應用以及遞迴形式系統。它由數學家阿隆佐·邱奇在20世紀30年代首次發表。lambda演算作為一種廣泛用途的計算模型,可以清晰地定義什麼是一個可計算函式,而任何可計算函式都能以這種形式表達和求值,它能類比單一磁帶圖靈機的計算過程;儘管如此,lambda演算強調的是變換規則的運用,而非實現它們的具體機器。 (取自維基百科)

這樣看起來,數學界對於 Lambda 的定義是指一個函式 (或函數) 的抽象化,以及將這個已被抽象化的函數循環再利用的過程,遞迴 (Recusive) 演算也是使用函數再利用來求解的最佳應用。對應到程式語言,相同的概念就是 LISP 已經於 1958 年提出,稱為匿名函式 (anonymous functions) 的玩意。

匿名函式(英語:Anonymous Function)在電腦編程中是指一類無需定義識別碼(函式名)的函式子程式,普遍存在於多種程式語言中。

匿名函式想要解決的問題就是能將函數當作參數 (function as parameter) 傳遞給其他的程式,但是函數的本體實作是取決於該函數的實作者,看起來其實很像是介面 (interface) 的味道,因為介面可以強制實作者實作它裡面的屬性、方法等,而函數型態的參數則是會強制呼叫端傳遞該函數所要求的參數,如此的作法和數學界的 Lambda 演算的想法幾乎一致,所以後來許多程式語言在實作這類機制時都不太會用函數參數,而是直接將它稱為 Lambda。

因此,.NET 裡面的 Delegate 基本上也是一種類似匿名函式的實作,只是它是在 C# 2.0 才比較成形,到了 C# 3.0 才完整具備 Lambda 所需要的匿名函式特性, 所以自 C# 3.0 開始,Lambda 已經是 Delegate 極佳的代名詞,也是在這個時候開始,Delegate 才被開始廣泛運用。

Lambda Expression 與其語法樹

Lambda 既然是一個匿名函數,想當然它會是一種具備完整程式語言機制的實作,有自己的變數、自己的判斷式、自己的資料管理能力等等,在 .NET 3.5 開發 LINQ 的時候,其中有一項重要功能是 LINQ to SQL,微軟需要實作一個機制讓 LINQ 可以被翻譯成 SQL 指令,再由 SQL 資料庫去執行,從而做到使用 LINQ 存取資料庫的需求。為了要實作這個翻譯的功能,微軟需要實作一組 API,來讓程式可以解析 Lambda 裡面的程式的各種屬性,才能運用這些屬性去產生或組合出對應的 SQL 指令,這組 API 被放在 System.Linq.Expressions 命名空間中,也就是語法樹 (Syntax Tree) 或稱運算式樹 (Expression Tree) 的 API。

運算式樹除了可以讓 LINQ 對其他資料來源的提供者 (LINQ Provider) 取得 Lamdba 本身的運算式結構外,其實它本身也可以做到動態語言執行期 (Dynamic Language Runtime, DLR) 所需要的基礎建設,講得白話一點,它可以當作比 IL 高階一點的動態程式產生器,但相信我,除非有這麼特殊一定要用動態程式產生器寫程式的理由,否則我是不太相信會有多少人會在工作上用 Lambda Expression API 來寫程式。

如果您有這個雄心壯志,建議您先看完這一篇:https://docs.microsoft.com/zh-tw/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-use-expression-trees-to-build-dynamic-queries,如果您還是保有原本的雄心壯志,那我只能給予祝褔了。

語法樹其實也有它好用的地方,例如我在 ORM 系列文中使用了 Lambda Expression 來設定欄位對應的屬性,善用 Lambda Expression 有時是可以有效提升開發人員生產力的方法,因為強大的 Visual Studio 內建的 IntelliSense 可以協助執行語法檢查與型別可用項目的列舉。 

結論:Lambda 是不是 Delegate?

在前面已經提過,Lambda 是一種函式指標物件,而它的基礎正是 Delegate,所以 Lambda 是 Delegate 是肯定的答案,即便你不相信官方文件的說法,但你永遠無法否認的是,下面的程式就真實存在於 .NET Framework 中。

public delegate void Action<in T1,in T2,in T3,in T4,in T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6,in T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
public delegate void Action<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);
 
public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5);
public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6);
public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, in T7, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7);
public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8);

至於 Lambda 是語法糖的說法,基本上可以算也可以不算,因為微軟在實作 Lambda 時並不是想將它做成語法糖 (官方並沒有要將它做成語法糖的打算),只是最後實作出的成果確實是具有語法糖的特徵而己。

註:我花了一小時45分寫這篇文章。