C#回调函数和闭包

来源:互联网 发布:身份证电子版制作软件 编辑:程序博客网 时间:2024/05/29 14:27

整理自http://bbs.csdn.net/topics/390622815


回调函数,这一般是在C语言中这么称呼,对于定义一个函数,但是并不由自己去调用,而是由被调用者间接调用,都可以叫回调函数。本质上,回调函数和一般的函数没有什么区别,也许只是因为我们定义了一个函数,却从来没有直接调用它,这一点很奇怪,所以有人发明了回调函数这个词来统称这种间接的调用关系。


在包括C#在内的很多高级语言中,我们有其它更具体的名词来称呼它,比如事件处理函数,委托,匿名委托,匿名函数,Lambda表达式,所以很少直接称呼某个函数为回调函数,除非是编写和C打交道的程序。

闭包一般是指函数的嵌套定义中,内部的函数可以超越作用于“看见”外侧的变量,反之则不行,它描述的就是这样一种关系,比如

C# code
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Action foo1 = () =>
{
    int i = 1;
    Action foo2 = () =>
    {
        int j = 2;
        Action foo3 = () =>
        {
            int k = 3;
            Console.WriteLine(i + j + k);
        };
        foo3();
        // error Console.WriteLine(k);
    };
    foo2();
    // error Console.WriteLine(j);
};
foo1();
// error Console.WriteLine(i);


闭包的作用是使方法内部变量的作用域延生到方法之外,相当于延长内部变量的生命周期。
这不是闭包的作用,而是闭包的副作用,好比汽车的作用绝对不是开得太快能把人撞死。

不恰当地使用匿名函数,会导致变量作用域不正确地延长,比如

C# code
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A
{
    static public Action action;
}
 
class Program
{
    void foo()
    {
        int i = 1;
        A.action = () => Console.WriteLine(i); //很明显,i这个变量按理说在foo()执行完了以后就没用了,但是当我们在匿名函数中访问它以后,它在函数退出之后仍然有效。
    }
    void bar()
    {
        A.action();
    }
    void Main()
    {
        foo();
        bar();
    }
}




回调函数最主要的作用,不是“获取调用者内部的对象,包括方法体,变量”,而是让一个函数可以实现总体流程的复用,同时让调用者“填空”,自定义某个细节。而一般的函数,则只能复用功能本身。

比如说我编写这么一个函数:
C# code
?
1
2
3
4
5
6
7
void PrintElementsInTheArray(int[] array)
{
    for (int i = 0; i < array.GetLength(0); i++)
    {
        Console.WriteLine(array[i]);
    }
}

我调用这个函数,就可以输出一个数组的元素,这很好,但是,如果我的需求变化下,我想输出以逗号分隔,而不是以行分隔,怎么办?
如果不用委托,我们只能直接修改它,或者再写一个函数,比如
C# code
?
1
2
3
4
5
6
7
8
void PrintElementsInTheArray2(int[] array)
{
    for (int i = 0; i < array.GetLength(0); i++)
    {
        if (i == 0) Console.Write(array[i]);
        else Console.Write("," + array[i]);
    }
}

当然,我们也可以把这两个函数写在一起,比如:
C# code
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void PrintElementsInTheArray(int[] array, bool SplitByComma)
{
    for (int i = 0; i < array.GetLength(0); i++)
    {
        if (SplitByComma)
        {
            if (i == 0) Console.Write(array[i]);
            else Console.Write(", " + array[i]);
        }
        else
        {
            Console.WriteLine(array[i]);
        }
    }
}

但是你看,写来写去,我们都只能由这个函数的定义者决定整个输出的细节,而遍历数组的流程无法真正复用,如果我们要再换一种输出方式,还得修改代码。有没有一劳永逸的办法呢?那就是使用委托,直接把foreach的循环空出来,让调用者自己填空:
C# code
?
1
2
3
4
5
6
7
void PrintElementsInTheArray(int[] array, Action<boolint> action)
{
    for (int i = 0; i < array.GetLength(0); i++)
    {
        action(i == 0, array[i]);
    }
}

好了,有了这个通用的函数,再实现上面两个需求就很简单了,比如
C# code
?
1
PrintElementsInTheArray(data, (isFirst, item) => Console.WriteLine(item));

这就是按行输出
C# code
?
1
2
3
PrintElementsInTheArray(data, (isFirst, item) => {
    if (isFirst) Console.Write(item); else Console.Write(", " + item);
});

这就是按照逗号输出

我们还可以不在控制台输出,而在Web页面上输出
C# code
?
1
PrintElementsInTheArray(data, (isFirst, item) => Response.WriteLine(item));


关于闭包我再多解释几句。

延长变量的生命周期,只是一种现象,并不是错误,也不是副作用。好比汽车可以开得很快只是一种现象而已。
导致错误的原因是因为“代码没有按照设计者的意图工作”,闭包只是使得代码变得复杂,导致更有可能使得它不按照编写者的意图工作,明白这个逻辑关系么?
好比
int i = arr[3];
这行代码对不对?
不好说,如果arr长度是3,那么这样写,越界了。如果arr长度是100,但是你想访问的是第三个元素,你这么写访问了第四个,还是不对,因此,导致代码不对的原因不是代码本身(否则编译器就可以拦下来了),而是它和你的意图不一致。
我们看这样两段代码:
C# code
?
1
2
3
4
5
//在另一个很远的地方
public const int MaxLength = 2;
...
int[] arr = new int[MaxLength];
int i = arr[3];


C# code
?
1
2
int[] arr = new int[2];
int i = arr[3];

我问你,哪个代码中的错误容易被发现?
显然是后者。
一样的道理,闭包只是使得错误更隐蔽,它本身不是一种错误或者问题。
好比汽车开得快会撞死人,但是开多快会撞死人?这个说不准。你在闹市区开70码是作死,但是在高速上开120一点问题没有。

言归正传,闭包延长了变量的生命周期会导致哪些原本很容易发现的问题不容易发现,我可以举两个例子,
第一个是导致原本应该释放的大量内存没有及时释放,造成性能问题。这个很好理解。
再有,我们都知道,如果一个对象拥有非托管资源,并且用Dispose释放过,那么继续访问或者调用它是错误的,比如
using (obj)
{
   ...
}
obj.foo();
这个明显就是错误的,using之后,这个对象就不要再调用了。
但是如果有闭包,问题就可能很隐蔽:
Action a;
using (obj)
{
     a = () => obj.foo(); 
}
a();
这段代码粗略一看,我没有在using之外再访问它的foo()方法啊。
其实因为闭包的缘故,obj被带了出来。
那么这么写,可能就会有问题。并且不易发现。所以要小心。
注意,我用了“可能”这个词。

原创粉丝点击