C#委托浅析与漫谈委托浅析与漫谈
1. 概述
委托是C#区别于其他语言的一个特色,用它我们能写出简洁优雅的代码、能很方便的实现对象间的交互。
初学者可能会觉得委托体系很复杂:lambda表达式、语句lambda、匿名方法、委托、事件,光名词一堆。其实这些只是
C#编译器为我们提供的语法糖,在编译后它们都是MulticastDelegate类型的对象。而且从用途上讲主要也两方面:将“方法对
象化”和实现“观察者模式”,本文围绕这两方面,分享本人对委托中相关概念的理解,顺便介绍一些相关的其它东西。
2. 闭包
闭包似乎在javascript里谈得比较多,其实只要支持定义”局部函数”的语言都会涉及到”闭包”的概念,像C++11的lambda、
java的匿名内部类、Smalltalk的Block等。
在C#中也有闭包,比如下面这个简单的例子:
例1
int[] scores = { 100, 80, 60, 40, 20 };
var min = 60;
var passed = scores.Where((int i) => { return i >= min; });
这里向Count这个扩展方法传入了一个匿名方法,注意这里变量min对于这个匿名方法很特殊,它对匿名方法来说叫”自由
变量(free variable)”(与之相对的叫”bound variable”),因为它不是该匿名方法的参数,也不是它的局部变量,这段代码
能成功编译是因为从词法作用域的角度看,min这个变量对于匿名方法内部是可见的。
2.1 词法作用域(lexical scoping)
首先我们搞清一些概念:
scope:英语中有“视野”之义,表示符号名的可见范围。
extent:或叫lifetime,表示变量的一生。scope有时会影响extent。
JavaScript中的变量提升
关于这两者区别的一个例子是javascript中的“变量提升”:
例2
text = 'global variable';
function test(){
alert(text);
//猜猜这里会弹出什么?
var text = "local variable";
};
test();
答案是弹出”undefined”。为什么?因为首先javascript是函数作用域,第二javascript中的局部变量名的scope贯穿整个函
数,即函数中变量名在函数起始到结束范围内都是有效的,所以调用alert时text这个变量名被解析为局部变量,而这时还未执
行到赋值语句,也是局部text的extent还未开始,text只是个有效的变量名,并没有指向一个有效的对象,因此会弹
出”undefined”。
好,我们继续
什么是词法作用域?
“词法作用域”也叫静态作用域(static scoping),”词法”表示源代码级别,”静态”指发生在解释时/编译时(与之相对是运行
时),根据字面可理解为“变量在源代码中的可见范围”。
因为是基于源代码的,所以看上去很符合我们的直观感受,大多数的编程语言,包括许多动态语言,都使用词法作用域的
规则来进行标识符解析的。C#也不例外。
我们通常所称的“闭包”全称叫“词法闭包”,它是指存储了一个函数和创建该函数时所处词法环境的对象。在闭包中,它访
问的外部变量叫做“被捕捉的变量(captured variable)”。比如上面例1中,第3行,匿名方法内部使用了min这个符号,但min
既不是匿名方法的参数,也不是它的局部变量,所以编译器开始从定义这个匿名方法的词法环境开始向上不断搜索min这个符
号,成功找到后,将它“捕捉”进匿名方法里,形成“闭包”,传递给Count方法的其实是这个“闭包”。
“捕捉”到底是什么意思?把它复制一份?还是保存它的引用?带着疑问猜猜下面C#代码的执行结果:
例3
class Program
{
static void Main()
{
foreach(var i in MakeClosures(3))
{
i();
//每次执行的结果是什么
}
Console.ReadKey();
}
static IEnumerableMakeClosures(int count)
{
var closures = new List();
for (var i = 0; i Add(() => Console.WriteLine(++i));
return closures;
}
}
我们来分析一下。首先,C#中的捕捉是“按引用捕捉”,所以: