委托是一种定义方法签名的类型。当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联。 您可以通过委托实例调用方法。(MSDN)
- 委托类似于 C++函数指针,但它们是类型安全的
- 委托允许将方法作为参数进行传递
- 委托可用于定义回调方法
- 委托可以链接在一起
- 方法不必与委托签名完全匹配。(协变与逆变)
- C# 2.0 版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法。 C#3.0引入了Lambda表达式,利用它们可以更简练地编写内联代码块。匿名方法和 Lambda表达式(在某些上下文中)都可编译为委托类型
1. 定义委托类型
2. 定义一个兼容委托类型签名的回调方法
在委托类型签名中参数是string类型,根据逆变性,第4个方法的参数完成符合要求。
3.实例化委托类型
在前面,已经有了一个委托类型和一个正确签名的方法,接着就可以创建委托的一个实例了,通过委托实例来真正执行这个先前定义的回调方法。在C#中如何创建委托实例,取决于先前定义的方法是实例方法还是静态方法。
假定在StaticMethods类中的定义一个静态方法PrintString,在InstanceMethods类中定义一个实例方法PrintString。下面就演示了如何如何创建委托类型Processor实例的两个例子:
如果需要真正执行的方法是静态方法,指定类型名称就可以了;如果是实例方法,就需要先创建该方法的类型的实例。这个和平时调用方法是一模一样的。当委托实例被调用时,就会调用需要真正执行的方法。
值得注意的是,C#2.0后,可以使用一种简洁语法,它仅有方法说明符构成,如下所示代码。使用快捷语法是因为在方法名称和其相应的委托类型之间有隐式转换。
4.调用委托
调用委托实例指的是调用委托实例的一个方法来执行先前定义的回调方法,不过这显得非常简单。如下所示:
值得注意的是,其中的调用委托实例的一个方法指的是Invoke方法,这个方法以委托类型的形式出现,并且具有与委托类型的声明中所指定的相同参数列表和返回类型。所以,在我们的例子中,有一个像下面这样的方法:
5.完整委托示例
实际上,委托在某种程度上提供了间接的方法。换言之,不需要直接指定一个要执行的行为,而是将这个行为用某种方式“包含”在一个对象中。这个对象可以像其他任何对象那样使用。在对象中,可以执行封装的操作。可以选择将委托类型看做只定义了一个方法的接口,将委托的实例看做实现了这个接口的一个对象。
先看下面一段代码,通过这段代码,逐步揭秘委托内部。
从表面看起来,使用一个委托似乎很容易:先用C#的delegate关键字声明一个委托类型,再定义一个要执行的签名一致的方法,然后用熟悉的new操作符构造委托实例,最后用熟悉的方法调用语法来调用先前定义的方法。
事实上,编译器在幕后做了大量的工作来隐藏了不必要的复杂性。首先,让我们重新认识一下下面的委托类型定义代码:
当编译器看到这行代码时,实际上会生成像下面一个完整的类:
编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。
现在重点解释构造器和Invoke,BeginInvoke和EndInvoke看留到后面讲解。
从图中可知Feedback的可访问性是private,因为委托在源代码中声明为internal类。如果源代码改成使用public可见性,编译器生成的类也会是public类。要注意,委托类即可嵌套在一个类型中定义,也可以在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。
由于所有委托类型都派生自MulticastDelegate,所以它们继承了MulticastDelegate的字段、属性和方法。在这些成员中,有三个非公共字段是最重要的。
注意,所有委托都有一个构造器,它要获取两个参数:一个是对象引用,另一个是引用回调方法的一个整数。然而,如果仔细看下签名的源代码,会发现传递的是Program.FeedbackToConsole和p.FeedbackToFile这样的值,还少一个intPtr类型的参数,这似乎不可能通过编译吧?
然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target和_methodPtr私有字段中。除此之外,构造器还将_invocationList字段设为null,对这个字段的讨论推迟到后面。
所以,每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的一个对象。例如,在执行以下两行代码之后:
Delegate类定义了两个只读的公共实例属性:Target和Method。给定一个委托对象的引用,可查询这些属性。Target属性返回一个引用,它指向回调方法要操作的对象。简单的说,Target属性返回保存在私有字段_target中的值。如果委托对象包装的是一个静态方法,Target将返回null。Method属性返回一个System.Reflection.MethodInfo对象的引用,该对象标识了回调方法。简单地说,Method属性有一个内部转换机制,能将私有字段_methodPtr中的值转换为一个MethodInfo对象并返回它。
可通过多种方式利用这些属性。例如,可检查委托对象引用是不是一个特定类型中定义的实例方法:
还可以写代码检查回调方法是否有一个特定的名称(比如FeedbackToMsgBox):
知道了委托对象如何构造并了解其内部结构之后,在来看看回调方法是如何调用的。为方便讨论,下面重复了Counter方法的定义:
注意注释下方的那一行代码。if语句首先检查fb是否为null。如果不为null,下一行代码调用回调方法。
这段代码看上去是在调用一个名为fb的函数,并向它传递一个参数(val)。但事实上,这里没有名为fb的函数。再次提醒你注意,因为编译器知道fb是引用了一个委托对象的变量,所以会生成代码调用该委托对象的Invoke方法。也就是说,编译器看到以下代码时:
将生成以下代码,好像源代码本来就是这么写的:
其实,完全可以修改Counter方法来显式调用Invoke方法,如下所示:
前面说过,编译器是在定义Feedback类时定义Invoke的。所以Invoke被调用时,它使用私有字段_target和_methodPtr在指定对象上调用包装好的回调方法。注意,Invoke方法的签名与委托的签名是匹配的。由于Feedback委托要获取一个Int32参数,并返回void,所以编译器生成的Invoke方法也要获取一个Int32参数,并返回void。
1. 委托链初印象
委托实例实际有一个操作列表与之关联。这称为委托实例的调用列表。System.Delegate类型的静态方法Combine和Remove负责创建新的委托实例。其中,Combine负责将两个委托实例的调用列表连接在一起,而Remove负责从一个委托实例中删除另一个的委托列表。
委托是不易变的。创建一个委托实例后,有关它的一切就不能改变。这样一来,就可以安全地传递委托实例,并把它们与其他委托实例合并,同时不必担心一致性、线程安全性或者是否其他人视图更改它的操作。这一点,委托实例和string是一样的。
除了能合并委托实例,还可以使用Delegate.Rmove方法从一个实例中删除另一个实例的调用列表。对应的C#简化操作为-和-=。Delegate.Remove(source,value)将创建一个新的委托实例,其调用列表来自source,value中的列表则被删除。如果结果有一个空的调用列表,就返回null。
一个委托实例调用时,它的所有操作都顺序执行。如果委托的签名具有一个非void的返回值类型,则Invoke的返回值是最后一个操作的返回值。
如果调用列表中的任何操作抛出一个异常,都会阻止执行后续的操作。
Counter方法内部的代码会在Feedback委托对象上隐式调用Invoke方法,这在前面已经讲过了。在fnChain引用的委托上调用Invoke时,该委托发现私有字段_invocationList不为null,所以会执行一个循环来遍历数组中的所有元素,并依次调用每个委托包装的方法。在本例中,首先调用的是FeedbackToConsole,然后是FeedbackToMsgBox,最后是FeedbackToFile。
以伪代码的方式,Feedback的Invoke的基本上是向下面这样实现的:
注意,还可以使用Delegate公共静态方法Remove从委托链中删除委托,如下所示。
Remove方法被调用时,它扫描的第一个实参(本例是fbChain)所引用的那个委托对象内部维护的委托数组(从末尾向索引0扫描)。Remove查找的是其_target和_methodPtr字段与第二个实参(本例是新建的Feedback委托)中的字段匹配的委托。如果找匹配的委托,并且(在删除之后)数组中只剩下一个数据项,就返回那个数据项。如果找到匹配的委托,并且数组中还剩余多个数据项,就新建一个委托对象——其中创建并初始化_invocationList数组将引用原始数组中的所有数据项(删除的数据项除外),并返回对这个新建委托对象的引用。如果从链中删除了仅有的一个元素,Remove会返回null。注意,每次Remove方法调用只能从链中删除一个委托,它不会删除有匹配的_target和_methodPtr字段的所有委托。
前面展示的例子中,委托返回值都是void。但是,完全可以向下面这样定义Feedback委托:
如果这样定义,那么该委托的Invoke方法就应该向下面这样(伪代码形式):
1.C#对委托链的支持
为方便C#开发人员,C#编译器自动为委托类型的实例重载了+=和-=操作符。这些操作符分别调用了Delegate.Combine和Delegate.Remove。使用这些操作符,可简化委托链的构造。
比如下面代码:
2.取得对委托链调用更多控制
现在我们已经理解了如何创建一个委托对象链,以及如何调用链中的所有对象。链中的所有项都会被调用,因为委托类型的Invoke方法包含了对数组中的所有项进行变量的代码。因为Invoke方法中的算法就是遍历,过于简单,显然,这有很大的局限性,除了最后一个返回值,其它所有回调方法的返回值都会被丢弃。还有吗如果被调用的委托中有一个抛出一个或阻塞相当长的时间,我们又无能为力。显然,这个算法还不够健壮。
由于这个算法的局限,所以MulticastDelegate类提供了一个GetInvocationList,用于显式调用链中的每一个委托,同时又可以自定义符合自己需要的任何算法:
GetInvocationList方法操作一个从MulticastDelegate派生的对象,返回一个有Delegate组成的数组,其中每一个引用都指向链中的一个委托对象。
下面是代码演示:
执行结果为:
The light is off
Failed to get status from ConsoleTest.GetInvocationList+Fan.Speed
Error: The fan broke due to overheating
The volume is loud
- 委托封装了包含特殊返回类型和一组参数的行为,类似包含单一方法的接口。
- 委托类型声明中所描述的类型签名决定了哪个方法可用于创建委托实例,同时决定了调用的签名。
- 为了创建委托实例,需要一个方法以及(对于实例方法来说)调用方法的目标。
- 委托实例是不易变的。
- 每个委托实例都包含一个调用列表——一个操作列表。
- 委托实例可以合并到一起,也可以从一个委托实例中删除一个。
- 事件不是委托实例——只是成对的add/remove方法。
在C#1中,如果要创建一个委托实例,就必须同时指定委托类型和要采取的操作。如下所示:
为了简化编程,C#2支持从方法组到一个兼容委托类型的隐式转换。所谓"方法组"(method group),其实就是一个方法名。
现在我们可以使用如下代码,效果和上面的代码一模一样。
在前面已经说过C#2.0后,将一个方法绑定到一个委托时,C#和CLR都允许引用类型的协变性和逆变性。
1.使用匿名方法
Action
在C#1中,可能一些参数不同,需要创建一个或多个很小的方法,而这些细粒度的方法管理起来又十分不便。在C#2中引入的匿名方法很好的解决了这个问题。
.NET2.0引入了一个泛型委托类型Action,它的签名非常简单:
Action就是对T的一个实例执行某些操作。例如:
上述代码展示了匿名方法的几个不同特性。首先是匿名方法的语法:先是delegate关键字,再是参数(如果有的话),随后是一个代码块,其中包含了对委托实例的操作行定义的代码。值得注意的是,逆变性不适用于匿名方法:必须指定和委托类型完全匹配的参数类型。
2.匿名方法的返回值
Predicate
Action委托的返回类型是void,所以不必从匿名方法返回任何东西。但在需要返回值的情况下怎么办呢,这就要使用.NET2.0中的Predicate委托类型。下面是它的签名:
从签名中可以看到,这个委托返回的是bool类型,现在演示一下,创建一个Predicate的一个实例,其返回值指出传入的实参是奇数还是偶数。
注意:从匿名方法返回一个值时,它始终从匿名函数中返回,而不是从委托实例的方法中返回。
Comparison
Comparison 委托,表示比较同一类型的两个对象的方法。下面是它的签名:
public delegate int Comparison(T x,T y)
从签名中可以看到,这个委托返回的是int 类型。Comparison是在.NET2.0中常见的委托类型,可用它来对集合排序,它是IComparer接口的委托版。通常,一种情况下只需要一个特定的排列顺序,所以采取内联的方式指定完全是合理的,不需要在其余类的内部添加一个独立的方法来指定该顺序。此委托由 Array 类的 Sort(T[], Comparison) 方法重载和 List 类的 Sort(Comparison)方法重载使用,用于对数组或列表中的元素进行排序。
在少数情况下,你实现的委托可能不依赖于它的参数值。你可能想写一个事件处理程序,它的行为只适用于一个事件,而不依赖事件的实际参数。如下面的例子中,可以完全省略参数列表,只需要使用一个delegate关键字,后跟作为方法的操作使用的代码块.
一般情况下,我们必须像下面这样写:
那样会无谓地浪费大量空间——因为我们根本不需要参数的值,所以编译器现在允许完全省略参数。
1.定义闭包和不同的变量类型
闭包的基本概念是:一个函数除了能通过提供给它的参数与环境互动之外,还能同环境进行更大程度的互动,这个定义过于抽象,为了真正理解它的应用情况,还需要理解另外两个术语:
外部变量:指其作用域包括一个函数方法的局部变量或参数(ref和out参数除外)。在可以使用匿名方法的地方,this引用也被认为是一个外部变量。
被捕捉的外部变量:通常简称为被捕获的变量,它在匿名方法内部使用的外部变量。
重新看一下"闭包"的定义,其中所说的"函数"是指匿名方法,而与之互动的"环境"是指由这个匿名方法捕捉到的变量集合。
它主要强调的是,匿名方法能使用在声明该匿名方法的方法内部定义的局部变量。
下面描述了从最简单到最复杂的所有变量:
anonLocal:它是匿名方法的局部变量,但不是EnclosingMethod的局部变量
outervariable:它是外部变量,因为在它的作用域内声明了一个匿名方法。但是,匿名方法没有引用它,所以它未被捕获。
capturedVariable:它是一个外部变量,因为在它的作用域内声明了一个匿名方法。但是,匿名方法内部引用引用了它,所以它成为了一个被捕获的变量。
2.测试被捕获的变量的行为
输出结果:
在x第一次调用之前
被x改变了
在x第二次调用之前
3.捕获变量有什么用
简单的说,捕获变量能简化编程,避免专门创建一些类来存储一个委托需要处理的信息(作为参数传递的信息除外)。
举个例子,假定有一个任务列表,并希望写一个方法来返回包含低于特定年龄的所有人的另一个列表。其中,我们知道List有一个方法能返回一个新列表,这个方法就是FindAll。但是,在匿名方法和捕获变量问世之前,List.FindAll的存在并没有多大意义,因为创建一个适合的委托是在太麻烦了。但是在C#2中,这个操作变量非常简单:
4.捕获变量的延长生命周期
对于一个被捕捉的变量,只要还有任何委托实例在引用它,它就会一直存在。
被捕捉的变量存在于编译器创建的一个额外的类中,相关的方法会引用该类的实例。
5.局部变量实例化
当一个变量被捕捉时,捕捉的变量的"实例"。如果在循环内捕捉变量,第一循环迭代的变量将有别于第二次循环时捕获的变量,以此类推。
6.捕获变量的使用规则和小结
使用规则
- 如果用或不用捕获变量时的代码同样简单,那就不用
- 捕捉由for或foreach语句声明的变量之前,思考你的委托是否需要在循环迭代结束之后延续,以及是否想让它看到那个变量的后续值。否则的话,就在循环内另建一个变量,用来复制你想要的值。
- 如果创建多个委托实例,而且捕获了变量,思考一下是否希望它们捕获同一个变量
- 如果捕获的变量不会发生改变,那就不要这么多担心。
小结
- 捕获的变量的生命周期变长了,至少和捕捉它的委托一样长。
- 多个委托可以捕获同一个变量
- 在循环内部,同一个变量声明实际会引用不同的变量"实例"
- 在for/foreach循环的声明中创建的变量仅在循环持续期间有效
-
必要时创建额外的类型来保存捕获的变量
C# 2根本性地改变了委托的创建方式,这样我们就能在.NET framework的基础上采取一种更函数化的编程风格。
1.Func<T, TResult>
Func<T, TResult> 委托,封装一个具有一个参数并返回 TResult 参数指定的类型值的方法。下面是它的签名:
从签名中可以看到,这个委托返回的是TResult类型。可以使用此委托表示一种能以参数形式传递的方法,而不用显式声明自定义委托。
封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法必须具有一个通过值传递给它的参数,并且必须返回值。
在使用 Func<T, TResult>委托时,不必显式定义一个封装只有一个参数的方法的委托。
例如,以下代码显式声明了一个名为 ConvertMethod 的委托,并将对UppercaseString方法的引用分配给其委托实例。
以下示例简化了此代码,它所用的方法是实例化 Func<T, TResult> 委托,而不是显式定义一个新委托并将命名方法分配给该委托。
您也可以按照以下示例所演示的那样在 C# 中将 Func<T, TResult> 委托与匿名方法一起使用。
用一个匿名方法来创建委托实例,如:
最终的结果为"5"这是意料之中的事。值得注意的是,returnLength的声明和赋值是分开的,否则一行可能放不下,这样还有利于代码的理解。
匿名方法是加粗的一部分,也是打算转换成Lambda表达式的部分。
Lambda表达式最冗长的形式是:
(显式类型参数列表) => {语句}
=>部分是C#3新增的,他告诉编译器我们正在使用一个Lambda表达式。Lambda表达式大多数时候都和一个返回非void的委托类型配合使用——如果不返回结果,语法就不像现在这样一目了然了。这标志着C#1和C#3在用法习惯上的另一个区别。在C#1中,委托一般用于事件,很少会返回什么。在LINQ中,它们通常被视为数据管道的一部分,接收输入并返回结果,或者判断某项是否符合当前的筛选器等等。
这个版本包含了显式参数列表,并将语句放到大括号中,他看起来和匿名方法非常相似,代码如下:
同样的,加粗的那一部分是用于创建委托实例的表达式。在阅读Lambda表达式时,可以将=>部分看错"goes to"。
匿名方法中控制返回语句的规则同意适用于lambda表达式:如果返回值是void,就不能从Lambda表达式返回一个值;如果有一个非void的返回值类型,那么每个代码路径都必须返回一个兼容的值。
大多数时候,都可以用一个表达式来表示整个主体,该表达式的值是Lambda的结构。在这些情况下,可以只指定哪个表达式,不使用大括号,不使用return语句,也不添加分号。格式如下:
(显示类型的参数列表) => 表达式
在这个例子中,Lambda表达式变成了:
编译器大多数情况下都能猜出参数类型,不需要你显式声明它们。在这些情况下,可以将Lambda表达式写成:
(隐式类型的参数列表) => 表达式
隐式类型的参数列表就是以一个逗号分隔的名称列表,没有类型。但隐式和显式类型的参数不能混合使用——要么全面是显式类型参数,要么全部是隐式类型参数。除此之外,如果有任何out或ref参数,就只能使用显式类型。在我们的例子中,还可以简化成:
如果Lambda表达式只需要一个参数,而且这个参数可以隐式指定类型,就可以省略小括号。这种格式的Lambda表达式是:
参数名 => 表达式
因此,我们例子中Lambda表达式最红形式是:
值得注意的是,如果愿意,可以用小括号将整个Lambda表达式括起来。