下面的這段C#3.0代碼看似再普通不過:
Stack stack = StackFactory.New();
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
Console.WriteLine(stack.Pop());
運行結果:
>>3
>>2
>>1
但如果我告訴你Stack並不是一個普通的class Stack,而是一個類型別名:using Stack = System.Func<T1, R1>,它其實是一個委託,你會不會覺得很神奇?說得更清楚一些,StackFatory.New()所建立的不是一個普通對象,而是建立了一個閉包(Closure)。
閉包是什嗎?
那麼閉包究竟是什麼呢?目前有兩種流行的說法:一種說法是閉包是在其詞法上下文中引用了自由變數的函數;另一種說法認為閉包是由函數和與其相關的引用環境組合而成的實體。兩種說法都對,但我更傾向於第二種表述,因為它明確地將閉包定義為“實體”。從例子中我們可以看出,閉包可以表現出對象的特徵,而普通的lambda函數或delegate更像是某個方法。結合兩種定義,我認為可以把閉包理解為帶狀態的函數。
自由變數
我們先來看一個閉包的簡單例子:
static Func<int, int> AddX(int x) {
return (y) => x + y;
}
這裡的lambda函數(y) => x + y就是一個閉包,它引用了其詞法上下文之外的自由變數x。對AddX(8)求值將用8代換x,即(y)=>8 + y;若再繼續求值AddX(8)(100)將得到8 + 100 = 108。
狀態
下面我們將看到如何使閉包具有狀態:
static Func<int> NewCounter() {
int x = 0;
return () => ++x;
}
Func<int> counter1 = NewCounter();
Console.WriteLine(counter1());
Console.WriteLine(counter1());
Console.WriteLine(counter1());
Func<int> counter2 = NewCounter();
Console.WriteLine(counter2());
Console.WriteLine(counter2());
Console.WriteLine(counter2());
運行結果:
>>1
>>2
>>3
>>1
>>2
>>3
我們通過NewCounter建立了一個閉包,每次對閉包進行求值都會使其環境的局部變數x增1,這樣就體現了閉包的狀態。同時,我們注意到局部變數x對於不同的閉包是獨立的,counter1和counter2並不共用同一個x。
閉包 vs class
這裡我們可以和OOP程式做一個對比,如果要用類來實現Counter我們會怎麼做呢?
class Counter{ //對應NewCounter
private int x; //對應局部變數int x
public Counter() { x = 0; } //new Counter()對應NewCounter()
public int Count() { return ++x;} //對應() => ++x
}
和 上面的閉包程式相比,從結構上看是不是有些類似呢?Counter類與NewCounter函數對應;new Counter()與NewCounter()對應;Counter類的私人成員x和NewCounter的局部變數x對應;Counter類的 Count()方法與閉包對應。
行為
除了狀態,我們還需要讓閉包具備類似stack.Push()和stack.Pop()這樣的對象行為。由於閉包只擁有一個()運算子,需要怎樣做才能使其具有多個方法呢?答案是高階函數(Higher-Order Function)。看剛才Stack的例子:
public enum Method {
Push, Pop, Top
}
public static Func<Method, object> NewStack() {
LinkedList<int> aList = new LinkedList<int>();
Func<Method, object> func = (method) => {
switch (method) {
case Method.Push:
Action<int> push = (int aValue) => { aList.AddLast(aValue); };
return push;
case Method.Pop:
Func<int> pop = () => {
int value = aList.Last.Value;
aList.RemoveLast();
return value;
};
return pop;
case Method.Top:
Func<int> top = () => { return aList.Last.Value; };
return top;
default:
return null;
}
};
return func;
}
NewStack()返回的是一個Func<Method, object>類型的閉包,它的參數是enum Method類型的,傳回值是object。NewStack()(Method.Push)將得到:
Action<int> push = (int aValue) => { aList.AddLast(aValue); };
這就是實際的Push方法!不過,在調用之前還需要顯式轉換一下類型:
(NewStack()(Method.Push) as Action<int>)(1);
最後,我們利用C#3.0的擴充方法(Extension Method)封裝一下,讓這個調用更加簡潔:
public static void Push(this Func<Method, object> aStack, int aValue){
(aStack(Method.Push) as Action<int>)(aValue);
}
public static int Pop(this Func<Method, object> aStack){
return (int)(aStack(Method.Pop) as Func<int>)();
}
public static int Top(this Func<Method, object> aStack){
return (int)(aStack(Method.Top) as Func<int>)();
}
這樣,我們就能寫出stack.Push(1), stack.Pop()這樣很OO的代碼來了!通過這樣一步步地探索,不知道您是否對函數式編程的閉包以及它和OOP對象的關係有了更深的理解呢?
模式
我們可以把上面在C#3.0中用閉包建立對象的方法歸納為一種固定的模式,不妨稱其為閉包原廠模式(Closure Factory Pattern)。模式要點包括:
1. 定義一個工廠類XXFactory,提供建立閉包對象的靜態Factory 方法New;
2. 在New方法的內定義局部對象作為閉包對象的狀態m_States;
3. 定義enum Method表示對象所具有的方法;
4. 在New方法內建立並返回一個引用m_States的閉包closureObject,其類型為Func<Method, object>;
5. closureObject接受Method參數,返回表示該方法的閉包(或普通委託)的methodObject;
6. 通過擴充方法為Func<Method, object>定義擴充方法,為closureObject的方法調用加上文法糖衣。
參考
閉包的概念、形式與應用
The Beauty of Closures