闭包和对象的关系

下面的这段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,提供创建闭包对象的静态工厂方法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

posted on 2010-11-01 08:56  Todd Wei  阅读(5574)  评论(13编辑  收藏  举报