15.1 什么是委托
委托(delegate)可以认为是这样的对象,它包含具有相同签名和返回值类型的有序方法列表。
- 方法的列表称为调用列表(invocation list)。
- 当委托被调用时,它调用列表中的每一个方法。
包含单个方法的委托和C++的函数指针相似。然而,与函数指针不同的是,委托是面向对象的并且是类型安全(type-safe)的。
调用列表中的方法
15.2 声明委托类型由委托保存的方法可以来自任何类或结构,只要它们同时匹配委托的如下两点:
- 返回值
- 签名(包括ref和out修饰符)
调用列表中的方法可以是实例方法或是静态方法。
委托是类型,就好像类是类型一样。与类一样,委托类型必须在被用来创建变量以及类型的对象之前声明。如下示例代码声明了委托类型。委托类型声明和所有类型声明一样,不需要在类内部声明。
delegate void MyDel(int x);
委托类型的声明看上去与方法的声明很相似,有返回类型和答名。返回类型和签名指定了委托接受的方法形式。
委托类型声明在两个方面与方法声明不同。委托类型声明:
- 以delegate关键词开头。
- 没有方法主体。
15.3 创建委托对象
委托是引用类型,因此有引用和对象。在委托类型声明之后,我们可以声明变量并创建类型的对象。如下代码演示了委托类型的变量的声明:
MyDel delVar;
有两种创建委托对象的方式,第一种是使用带new运算符的对象创建表达式,如下面代码所示。new运算符的操作数的组成如下:
- 委托类型名
- 一组圆括号,其中包含作为调用列表中的第一个成员的方法的名字。方法可以是实例方法或静态方法。
例:
delVar = new MyDel(myInstObj.Mym1); dVar=new MyDel(Sclass.TherM2);
还可以使用快捷语法,它仅由方法说明符构成,如下面代码所示。这段代码和之前的代码是等价的。使用快捷语法是因为在方法名称和其相应的委托类型之间有隐式转换。
例:
delVar = myInstObj.MyM1; dVar=SClass.OtherM2;
15.4 赋值委托
15.5 组合委托由于委托是引用类型,我们可以通过给它赋值来改变包含在委托变量中的引用。旧的委托对象会被垃圾回收器回收。
delVar=SClass.OtherM2;
委托可以使用额外的运算符来“组合”。这个运算最终会创建一个新的委托,其调用列表是两个操作数的委托调用列表的副本的连接。
例:
MyDel delA = myInstObj.MyM1;
MyDel delB = SClass.OtherM2;
MyDel delC = delA + delB;
尽管术语组合委托(combining delegate)让我们觉得好像操作数委托被修改了,其实它们并没有被修改,委托是恒定的。委托对象被创建后不会再被改变。
15.6 为委托增加方法
尽管通过之前的内容我们知道了委托其实是不变的,C#提供了看上去可以为委托增加方法的语法,以这种方式考虑,它非常棒。我们可以通过使用+=运算符来为委托增加方法或另一个委托。
当然,在使用+=去处符时,实际发生的是创建了一个新的委托,其调用列表是左边的委托加上右边方法的组合。
15.7 从委托移除方法
还可以使用-=去处符从委托移除方法。与为委托增加方法一样,其实是创建了一个新的委托。新的委托是旧委托的副本—只是没有了已经被移除方法的引用。如下是移除委托时需要记住的一些事项:
- 如果在调用列表中的方法有多个实例,-=运算符将从列表最后开始搜索,并且移除第一个与方法匹配的实例。
- 试图删除委托中不存在的方法没有效果。
- 试图调用空委托会抛出异常。
- 我们可以通过把委托和null进行比较来判断委托的调用列表是否为空。如果调用列表为空,则委托是null。
15.8 调用委托
可以像调用方法一样简单地调用委托。用于调用委托的参数将会用于调用调用列表中的每一个方法(除非有一个参数是输出参数)。
一个方法可以在调用列表中出现多次。如果这样,当委托被调用时,每次在列表中遇到这个方法时它都会被调用一次。
15.9 委托的示例
//定义一个没有返回值和参数的委托类型 delegate void PrintFunction(); class Test { public void Print1() { Console.WriteLine("Print1 -- instance"); } public static void Print2() { Console.WriteLine("Print2 -- static"); } } class Program { static void Main() { Test t= new Test(); //创建一个测试类实例 PrintFunction pf; //创建一个空委托 pf = t.Print1; //实例化并初始化该委托 //Add three more methods to the delegate. pf+=Test.Print2; pf+=t.print1; pf+=Test.Print2; // The delegate now contains four methods. if (null !=pf) pf(); else Console.WriteLine("Delegate is empty"); } }
15.10 调用带返回值的委托
如果委托有返回值并且在调用列表中有一个以上的方法,会发生下面的情况:
- 调用列表中最后一个方法返回的值就是委托调用返回的值。
- 调用列表中所有其他方法的返回值都会被忽略。
15.11 调用带引用参数的委托
15.12 匿名方法如果委托有引用的参数,参数值会根据调用列表中的一个或多个方法的返回值而改变。
在调用委托列表中的下一个方法时,参数的新值(不是初始值)会传给下一个方法。
我们已经见过了使用静态方法或实例方法来初始化委托。对于这种情况,方法本身可以被代码的其他部分显式调用,当然,也就必须是某个类或结构的成员。然而,如果方法只会被使用一次—用来初始化委托会怎么样呢?在这种情况下,除了创建委托语法的需要,没有必须创建独立的具名方法。匿名方法允许我们避免使用独立的具名方法。匿名方法(anonymous method)是在初始化委托时内联(inline)声明的方法。
使用匿名方法我们可以在如下地方使用匿名方法:
- 声明委托变量时作为初始化表达式。
- 组合委托时在赋值语句的右边。
- 为委托增加事件时在赋值语句的右边。
匿名方法的语法
匿名方法表达式的语法包含如下组成部分:
- delegate类型关键字。
- 参数列表,如果语句块没有任何参数则可以省略。
- 语句块,它包含了匿名方法的代码。
delegate (parameters){ImplementationCode}1.返回类型
匿名方法不会显式声明返回值。然而,实现代码本身的行为必须通过返回一个在类型上与委托的返回类型相同的值来匹配委托的返回类型。如果委托有void类型的返回值,匿名方法就不能返回值。
2.参数
除了数组参数,匿名方法的参数列表必须在如下三方面匹配委托:
- 参数数量
- 参数类型
- 修饰符
我们可以通过使圆括号为空或省略圆括号来简化匿名方法的参数列表,但是仅在下面两项都为真的情况下才可以这样做。
- 委托的列表不包含任何out参数。
- 匿名方法不使用任何参数。
3.params参数
如果委托声明的参数列表包含了params参数,那么params关键字就会被匿名方法的参数列表忽略。
例如,在如下代码中:
- 委托 类型声明指定最后一个参数为params类型的参数。
- 然而,匿名方法参数列表忽略了params关键字。
delegate void SomeDel(int x,params int[] y);SomeDel mDel = delegate (int x,int[] y){};
变量和参数的作用域参数以及声明在匿名方法内部的局部变量的作用域限制在实现方法的主体之内。
1.外部变量
与委托的命名方法不同,匿名方法可以访问它们外围的作用域的局部变量和环境。
- 外围作用域的变量叫做外部变量(outer variable)。
- 用在匿名方法实现代码中的外部变量称为被方法捕获(captured)。
2.被捕获变量的生命周期的扩展
只要捕获方法还是委托的一部分,即使变量已经离开了作用域,被捕获的外部变量也会一直有效。
15.13 Lambda表达式
C# 2.0引入了匿名方法,它允许我们在创建或为委托增加方法时包含小段内联代码。然而,匿名方法的语法有一点麻烦,而且需要一些编译器已经知道的信息。C# 3.0引入了lambda表达式,简化了匿名方法的语法,从而避免包含这些多余的消息。我们可能会希望使用lambda表达式来替代匿名方法。其实,如果lambda表达式被先引入,那么就不会有匿名方法。
在匿名方法的语法中,delegate关键字是有点多余,因为编译器已经知道我们在将方法赋值给委托。我们可以通过如下步骤把匿名方法转换成lambda表达式:
- 删除delegate关键字。
- 在参数列表和匿名方法主体之间放lambda运算符 =>。lambda运算符读作“goes to”。
如下代码演示了这种转换。第一行演示了将匿名方法赋值给变量del。第二行演示了同样的匿名在被转换成lambda表达式之后,被赋值给了变量le1。
MyDel del = delegate(int x) {return x+1;}; //匿名方法MyDel le1 = (int x) =>{return x+1;}; //表达式
说明:术语lambda表达式来源于数学家Alonzo Church等人在1920年到1930期间发明的lambda积分。lambda积分是用于表示函数的一套系统。它使用希腊字母lambda(λ)来表示无名函数。近来,诸如Lisp和其方言的函数式编程语言使用这个术语来表示可以直接用于描述函数定义的表达式,表达式不再需要有名字了。
这种简单的转换少了一些多余的东西,看上去也更简洁了,但是只省了6个字符。然而,编译器可以通过推断,允许我们更进一步简化lambda表达式,如下代码所示:
- 编译器还可以从委托的声明中知道委托参数的类型,因此lambda表达式允许我们活力类型参数,如le2的赋值代码所示。
- 带有类型的参数列表称为显式类型。
- 省略类型的参数列表称为隐式类型。
- 如果只有一个隐式类型参数,我们可以省略周围的圆括号,如le3的赋值代码所示。
- 最后,lambda表达式允许表达式的主体是语句块和表达式。如果语句块包含了一个返回语句,我们可以将语句块替换为return关键字后的表达式,如le4的赋值代码所示。
MyDel del = delegate(int x) {return x + 1; }; //匿名方法MyDel le1 = (int x) => {return x + 1; }; //Lambda表达式MyDel le2= (x) => {return x + 1; };MyDel le3= x => {return x + 1; };MyDel le4= x => x+1;
有关lambda表达式的参数列表的要点如下:
- lambda表达式参数列表中的参数必须在参数数量、类型和位置上与委托相匹配。
- 表达式的参数列表中的参数不一定需要包含类型(如隐式类型),除非委托有ref或out参数——此时类型是必须的(如显式类型)。
- 如果只有一个参数,并且是隐式类型的,周围的圆括号可以被活力,否则它就是必须的。
- 如果没有参数,必须使用一组空的圆括号。