C#再识委托
C# 1
1.什么是委托
委托是一种定义方法签名的类型。当实例化委托时,您可以将其实例与任何具有兼容签名的方法相关联。 您可以通过委托实例调用方法。(MSDN)
- 委托类似于 C++函数指针,但它们是类型安全的
- 委托允许将方法作为参数进行传递
- 委托可用于定义回调方法
- 委托可以链接在一起
- 方法不必与委托签名完全匹配。(协变与逆变)
- C# 2.0 版引入了匿名方法的概念,此类方法允许将代码块作为参数传递,以代替单独定义的方法。 C#3.0引入了Lambda表达式,利用它们可以更简练地编写内联代码块。匿名方法和 Lambda表达式(在某些上下文中)都可编译为委托类型
2.如何使用委托
1. 定义委托类型
定义一个委托类型,实际上只有一个定义委托的关键字、一个类型名称、一个返回值和参数列表。如下所示:
在这里值得注意的是,Processor其实是一个类,只不过看起来像一个方法的签名,但它不是一个方法,你可以认为它是一个特殊的类,但你一定不要说是一个特殊的方法。还有,因为委托是一个类,当然可以有它的可访问性修饰符了。
2. 定义一个兼容委托类型签名的回调方法
现在,已经知道了委托类型的签名,就可以定义一个兼容于委托类型签名的回调方法了。
第4种情况比较特殊,这在C#1.0时代是不允许的,但在C#2.0后是允许的。将一个方法绑定到一个委托时,C#和CLR都允许引用类型的协变性和逆变性。
协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。
在委托类型签名中参数是string类型,根据逆变性,第4个方法的参数完成符合要求。
3.实例化委托类型
在前面,已经有了一个委托类型和一个正确签名的方法,接着就可以创建委托的一个实例了,通过委托实例来真正执行这个先前定义的回调方法。在C#中如何创建委托实例,取决于先前定义的方法是实例方法还是静态方法。
假定在StaticMethods类中的定义一个静态方法PrintString,在InstanceMethods类中定义一个实例方法PrintString。下面就演示了如何如何创建委托类型Processor实例的两个例子:
Processor proc1,proc2;
//静态方法,类直接调用
proc1 = new Processor(StaticMethods.PrintString)
InstanceMethods instance = new InstanceMethods();
//实例方法,通过类的实例调用
proc2 = new Processor (instance.PrintString)
如果需要真正执行的方法是静态方法,指定类型名称就可以了;如果是实例方法,就需要先创建该方法的类型的实例。这个和平时调用方法是一模一样的。当委托实例被调用时,就会调用需要真正执行的方法。
值得注意的是,C#2.0后,可以使用一种简洁语法,它仅有方法说明符构成,如下所示代码。使用快捷语法是因为在方法名称和其相应的委托类型之间有隐式转换。
Processor proc1,proc2;
proc1 = StaticMethods.PrintString; //快捷语法
InstanceMethods instance = new InstanceMethods();
proc2 = instance.PrintString //快捷语法
4.调用委托
调用委托实例指的是调用委托实例的一个方法来执行先前定义的回调方法,不过这显得非常简单。如下所示:
Processor proc1,proc2;
proc1 = new Processor(StaticMethods.PrintString) //静态方法,类直接调用
InstanceMethods instance = new InstanceMethods();
proc2 = new Processor (instance.PrintString) //实例方法,通过类的实例调用
proc1("PrintString方法执行了");
//proc1.Invoke("PrintString方法执行了");
//proc1("PrintString方法执行了"); 是对proc1.Invoke("PrintString方法执行了"); 的简化调用
proc2.Invoke("PrintString方法执行了");
值得注意的是,其中的调用委托实例的一个方法指的是Invoke方法,这个方法以委托类型的形式出现,并且具有与委托类型的声明中所指定的相同参数列表和返回类型。所以,在我们的例子中,有一个像下面这样的方法:
void Invoke(string input);
调用Invoke执行先前定义的回调方法,可以在这里向这个执行先前定义的回调方法指定相应参数。可以用下面这一张图来解释:
5.完整委托示例
namespace Program {
//定义委托
delegate void Processor(string input);
class InstanceMethods
{
//定义与委托签名相同的"实例方法"
public void PrintString(string message)
{
Console.WriteLine(message);
}
}
class StaticMethods
{
//定义与委托签名相同的"静态方法"
public static void PrintString(string message)
{
Console. WriteLine(message);
}
}
class Program
{
static void Main(string[] args)
{
Processor proc1,proc2;
proc1 = new Processor(StaticMethods. PrintString); //静态方法,类直接调用
InstanceMethods instance = new InstanceMethods();
proc2 = new Processor (instance. PrintString); //实例方法,通过类的实例调用
proc1("PrintString方法执行了");
//proc1.Invoke("PrintString方法执行了"); //proc1("PrintString方法执行了")是对proc1.Invoke("PrintString方法执行了")的简化调用
proc2.Invoke("PrintString方法执行了");
Console.ReadKey();
}
}
}
4.委托的用途
实际上,委托在某种程度上提供了间接的方法。换言之,不需要直接指定一个要执行的行为,而是将这个行为用某种方式“包含”在一个对象中。这个对象可以像其他任何对象那样使用。在对象中,可以执行封装的操作。可以选择将委托类型看做只定义了一个方法的接口,将委托的实例看做实现了这个接口的一个对象。
5.委托揭秘
先看下面一段代码,通过这段代码,逐步揭秘委托内部。
namespace Test
{
// 1.声明委托类型
internal delegate void Feedback(Int32 value);
internal class Program
{
private static void Main(string[] args)
{
StaticDelegateDemo();
InstanceDelegateDemo();
ChainDelegateDemo1(new Program());
ChainDelegateDemo2(new Program());
}
private static void StaticDelegateDemo()
{
Console.WriteLine("----- Static Delegate Demo -----");
Counter(1, 3, null);
// 3.创建委托实例
Counter(1, 3, new Feedback(Program.FeedbackToConsole));
Counter(1, 3, new Feedback(FeedbackToMsgBox));
Console.WriteLine();
}
private static void InstanceDelegateDemo()
{
Console.WriteLine("----- Instance Delegate Demo -----");
Program di = new Program();
// 3.创建委托实例
Counter(1, 3, new Feedback(di.FeedbackToFile));
Console.WriteLine();
}
private static void ChainDelegateDemo1(Program di)
{
Console.WriteLine("----- Chain Delegate Demo 1 -----");
// 3.创建委托实例
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile);
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Counter(1, 2, fbChain);
}
private static void ChainDelegateDemo2(Program di)
{
Console.WriteLine("----- Chain Delegate Demo 2 -----");
Feedback fb1 = new Feedback(FeedbackToConsole);
Feedback fb2 = new Feedback(FeedbackToMsgBox);
Feedback fb3 = new Feedback(di.FeedbackToFile);
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
Counter(1, 2, fbChain);
Console.WriteLine();
fbChain -= new Feedback(FeedbackToMsgBox);
Counter(1, 2, fbChain);
}
private static void Counter(Int32 from, Int32 to, Feedback fb)
{
for (Int32 val = from; val <= to; val++)
{
// 如果指定了任何回调,就可以调用它
if (fb != null)
// 4.调用委托
fb(val);
}
}
// 2.声明签名相同的方法
private static void FeedbackToConsole(Int32 value)
{
Console.WriteLine("Item=" + value);
}
// 2.声明签名相同的方法
private static void FeedbackToMsgBox(Int32 value)
{
Console.WriteLine("Item=" + value);
}
// 2.声明签名相同的方法
private void FeedbackToFile(Int32 value)
{
StreamWriter sw = new StreamWriter("Status", true);
sw.WriteLine("Item=" + value);
sw.Close();
}
}
}
从表面看起来,使用一个委托似乎很容易:先用C#的delegate关键字声明一个委托类型,再定义一个要执行的签名一致的方法,然后用熟悉的new操作符构造委托实例,最后用熟悉的方法调用语法来调用先前定义的方法。
事实上,编译器在幕后做了大量的工作来隐藏了不必要的复杂性。首先,让我们重新认识一下下面的委托类型定义代码:
internal delegate void Feedback(Int32 value);
当编译器看到这行代码时,实际上会生成像下面一个完整的类:
internal class Feedback: System.MulticastDelegate {
// 构造器
public Feedback(object @object, IntPtr method);
// 这个方法和源代码指定的原型一样
public virtual void Invoke(Int32 value);
// 以下方法实现了对回调方法的异步回调
public virtual IAsyncResult BeginInvoke(Int32 value, AsyncCallback callback, object @object);
// 以下方法获取了回调方法的返回值
public virtual void EndInvoke(IAsyncResult result);
}
编译器定义的类有4个方法:一个构造器、Invoke、BeginInvoke和EndInvoke。
现在重点解释构造器和Invoke,BeginInvoke和EndInvoke看留到后面讲解。
事实上,可用.Net Reflector查看生成的程序集,验证编译器是否真的会自动生成相关代码,如下图所示:
在这个例子中,编译器定义了一个名为Feedback的类,该类派生自FCL定义的System.MulticastDelegate类型(所有委托类型都派生自System.MulticastDelegate类型)。
提示:System.MulticastDelegate类派生自System.Delegate,后则又派生自System.Object。之所以有两个委托类,是有历史原因的。
从图中可知Feedback的可访问性是private,因为委托在源代码中声明为internal类。如果源代码改成使用public可见性,编译器生成的类也会是public类。要注意,委托类即可嵌套在一个类型中定义,也可以在全局范围中定义。简单地说,由于委托是类,所以凡是能够定义类的地方,都能定义委托。
由于所有委托类型都派生自MulticastDelegate,所以它们继承了MulticastDelegate的字段、属性和方法。在这些成员中,有三个非公共字段是最重要的。
字段 | 类型 | 说明 |
---|---|---|
_target | System.Object | 当委托对象包装一个静态方法时,这个字段为null。当委托对象包装一个实例方法时,这个字段引用的是回调方法要操作的对象。换言之,这个字段指出了要传给实例方法的隐式参数this的值 |
_methodPtr | System.IntPtr | 一个内部的整数值,CLR用它来标识要回调的方法 |
_invocationList | System.Object | 该字段通常为null。构造一个委托链时,它可以引用一个委托数组。 |
注意,所有委托都有一个构造器,它要获取两个参数:一个是对象引用,另一个是引用回调方法的一个整数。然而,如果仔细看下签名的源代码,会发现传递的是Program.FeedbackToConsole和p.FeedbackToFile这样的值,还少一个intPtr类型的参数,这似乎不可能通过编译吧?
然而,C#编译器知道要构造的是委托,所以会分析源代码来确定引用的是哪个对象和方法。对象引用被传给构造器的object参数,标识了方法的一个特殊IntPtr值(从MethodDef或MemberRef元数据token获得)被传给构造器的method参数。对于静态方法,会为object参数传递null值。在构造器内部,这两个实参分别保存在_target和_methodPtr私有字段中。除此之外,构造器还将_invocationList字段设为null,对这个字段的讨论推迟到后面。
所以,每个委托对象实际都是一个包装器,其中包装了一个方法和调用该方法时要操作的一个对象。例如,在执行以下两行代码之后:
Feedback fbStatic = new Feedback(Program.FeedbackToConsole);
Feedback fbInstance = new Feedback(new Program.FeedbackToFile());
fbStatic和fbInstance变量将引用两个独立的,初始化好的Feedback委托对象,如下图所示。
Delegate类定义了两个只读的公共实例属性:Target和Method。给定一个委托对象的引用,可查询这些属性。Target属性返回一个引用,它指向回调方法要操作的对象。简单的说,Target属性返回保存在私有字段_target中的值。如果委托对象包装的是一个静态方法,Target将返回null。Method属性返回一个System.Reflection.MethodInfo对象的引用,该对象标识了回调方法。简单地说,Method属性有一个内部转换机制,能将私有字段_methodPtr中的值转换为一个MethodInfo对象并返回它。
可通过多种方式利用这些属性。例如,可检查委托对象引用是不是一个特定类型中定义的实例方法:
Boolean DelegateRefersToInstanceMethodOfType(MulticastDelegate d ,Type type) {
return ((d.Target != null) && d.Target.GetType() == type);
}
还可以写代码检查回调方法是否有一个特定的名称(比如FeedbackToMsgBox):
Boolean DelegateRefersToInstanceMethodOfName(MulticastDelegate d ,String methodName) {
return (d.Method.Name == methodName);
}
知道了委托对象如何构造并了解其内部结构之后,在来看看回调方法是如何调用的。为方便讨论,下面重复了Counter方法的定义:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// 如果指定了任何回调,就调用它们
if(fb != null ){
fb(val); //调用委托
}
}
}
注意注释下方的那一行代码。if语句首先检查fb是否为null。如果不为null,下一行代码调用回调方法。
这段代码看上去是在调用一个名为fb的函数,并向它传递一个参数(val)。但事实上,这里没有名为fb的函数。再次提醒你注意,因为编译器知道fb是引用了一个委托对象的变量,所以会生成代码调用该委托对象的Invoke方法。也就是说,编译器看到以下代码时:
fb(val);
将生成以下代码,好像源代码本来就是这么写的:
fb.Invoke(val);
其实,完全可以修改Counter方法来显式调用Invoke方法,如下所示:
private static void Counter(Int32 from, Int32 to, Feedback fb) {
for (Int32 val = from; val <= to; val++) {
// 如果指定了任何回调,就调用它们
if(fb != null ){
fb.Invoke(val);
}
}
}
前面说过,编译器是在定义Feedback类时定义Invoke的。所以Invoke被调用时,它使用私有字段_target和_methodPtr在指定对象上调用包装好的回调方法。注意,Invoke方法的签名与委托的签名是匹配的。由于Feedback委托要获取一个Int32参数,并返回void,所以编译器生成的Invoke方法也要获取一个Int32参数,并返回void。
6.委托链
1. 委托链初印象
委托实例实际有一个操作列表与之关联。这称为委托实例的调用列表。System.Delegate类型的静态方法Combine和Remove负责创建新的委托实例。其中,Combine负责将两个委托实例的调用列表连接在一起,而Remove负责从一个委托实例中删除另一个的委托列表。
委托是不易变的。创建一个委托实例后,有关它的一切就不能改变。这样一来,就可以安全地传递委托实例,并把它们与其他委托实例合并,同时不必担心一致性、线程安全性或者是否其他人视图更改它的操作。这一点,委托实例和string是一样的。
但很少在C#中看到对Delegate.Combine的显式调用,一般都是使用+和+=操作符。
图中展示了转换过程,其中x和y都是兼容委托类型的变量。所有转换都是由C#编译器完成的。
可以看出,这是一个相当简单的转换过程,但它使得代码变得整洁多了。
除了能合并委托实例,还可以使用Delegate.Rmove方法从一个实例中删除另一个实例的调用列表。对应的C#简化操作为-和-=。Delegate.Remove(source,value)将创建一个新的委托实例,其调用列表来自source,value中的列表则被删除。如果结果有一个空的调用列表,就返回null。
一个委托实例调用时,它的所有操作都顺序执行。如果委托的签名具有一个非void的返回值类型,则Invoke的返回值是最后一个操作的返回值。
如果调用列表中的任何操作抛出一个异常,都会阻止执行后续的操作。
2. 深入委托链
委托本身就已经相当有用了,再加上对委托链的支持,它的用处就更大了!委托链是由委托对象构成的一个集合。利用委托链,可调用集合中的委托所代表的全部方法。为了理解这一点,请参考上面示例代码中的ChainDelegateDemo1方法。在这个方法中,在Console.WriteLine语句之后,构造了三个委托对象并让变量fb1、fb2和fb3引用每一个对象,如下图所示:
随后,我定义了指向Feedback委托对象的引用变量fbChain,并打算让它引用一个委托链或者一个委托对象集合,这些对象包装了可以回调的方法。fbChain被初始化为null,表明目前没有回调的方法。使用Delegate类的公共静态方法Combine,可以将一个委托添加到链中:
Feedback fbChain = null;
fbChain = (Feedback)Delegate.Combine(fbChain, fb1);
执行以上代码时,Combine方法会视图合并null和fb1。在内部,Combine直接返回fb1中的值,所以fbChain变量现在引用的就是fb1变量引用的那个委托对象。如下图所示:
为了在链中添加第二个委托,再次调用了Combine方法:
fbChain = (Feedback)Delegate.Combine(fbChain, fb2);
在内部,Combine方法发现fbChain已经引用了一个委托对象,所以Combine会构造一个新的委托对象。这个新的委托对象对它的私有字段_target和_methodPtr进行初始化,具体值对目前讨论的来说并不重要。重要的是,_invocationList字段被初始化为引用一个委托对象数组。这个数组的第一个元素(索引为0)被初始化为引用包装了FeedbackToConsole方法的委托。数组的第二个元素(索引为1)被初始化为引用包装了FeedbackToMsgBox方法的委托。最后,fnChain被设为引用新建的委托对象,如下图所示:
为了在链中添加第三个委托,再次调用了Combine方法:
fbChain = (Feedback)Delegate.Combine(fbChain, fb3);
同样的,Combine方法会发现fbChain已经引用了一个委托对象,于是又Combine会构造一个新的委托对象。这个新的委托对象对它的私有字段_target和_methodPtr进行初始化,具体值对目前讨论的来说并不重要。重要的是,_invocationList字段被初始化为引用一个委托对象数组。这个数组的第一个元素(索引为0)被初始化为引用包装了FeedbackToConsole方法的委托,数组的第二个元素(索引为1)被初始化为引用包装了FeedbackToMsgBox方法的委托,数组的第三个元素(索引为2)被初始化为引用包装了FeedbackToFile方法的委托。最后,fnChain被设为引用新建的委托对象。注意之前新建的委托以及_invocationList字段引用的数组已经被垃圾回收器回收了。如下图所示:
在ChainDelegateDemo1方法中,用于设置委托链的所有代码已经执行完毕,我将fnChain变量交给Counte方法:
Counter(1, 2, fbChain);
Counter方法内部的代码会在Feedback委托对象上隐式调用Invoke方法,这在前面已经讲过了。在fnChain引用的委托上调用Invoke时,该委托发现私有字段_invocationList不为null,所以会执行一个循环来遍历数组中的所有元素,并依次调用每个委托包装的方法。在本例中,首先调用的是FeedbackToConsole,然后是FeedbackToMsgBox,最后是FeedbackToFile。
以伪代码的方式,Feedback的Invoke的基本上是向下面这样实现的:
public void Invoke(Int32 value) {
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
foreach(var d in delegateSet)
d(value);// 调用委托
}else{//否则,不是委托链
_methodPtr.Invoke(value);
}
}
注意,还可以使用Delegate公共静态方法Remove从委托链中删除委托,如下所示。
fbChain = (Feedback)Delegate.Remove(fbChain, new Feedback(FeedbackToMsgBox));
Remove方法被调用时,它扫描的第一个实参(本例是fbChain)所引用的那个委托对象内部维护的委托数组(从末尾向索引0扫描)。Remove查找的是其_target和_methodPtr字段与第二个实参(本例是新建的Feedback委托)中的字段匹配的委托。如果找匹配的委托,并且(在删除之后)数组中只剩下一个数据项,就返回那个数据项。如果找到匹配的委托,并且数组中还剩余多个数据项,就新建一个委托对象——其中创建并初始化_invocationList数组将引用原始数组中的所有数据项(删除的数据项除外),并返回对这个新建委托对象的引用。如果从链中删除了仅有的一个元素,Remove会返回null。注意,每次Remove方法调用只能从链中删除一个委托,它不会删除有匹配的_target和_methodPtr字段的所有委托。
前面展示的例子中,委托返回值都是void。但是,完全可以向下面这样定义Feedback委托:
public delegate Int32 Feedback (Int32 value);
如果这样定义,那么该委托的Invoke方法就应该向下面这样(伪代码形式):
public Int32 Invoke(Int32 value) {
Int32 result;
Delegate[] delegateSet = _invocationList as Delegate[];
if (delegateSet != null) {
foreach(var d in delegateSet)
result = d(value);// 调用委托
}else{//否则,不是委托链
result = _methodPtr.Invoke(_target,value);
}
return result;
}
1.C#对委托链的支持
为方便C#开发人员,C#编译器自动为委托类型的实例重载了+=和-=操作符。这些操作符分别调用了Delegate.Combine和Delegate.Remove。使用这些操作符,可简化委托链的构造。
比如下面代码:
Feedback fbChain = null;
fbChain += fb1;
fbChain += fb2;
fbChain += fb3;
2.取得对委托链调用更多控制
现在我们已经理解了如何创建一个委托对象链,以及如何调用链中的所有对象。链中的所有项都会被调用,因为委托类型的Invoke方法包含了对数组中的所有项进行变量的代码。因为Invoke方法中的算法就是遍历,过于简单,显然,这有很大的局限性,除了最后一个返回值,其它所有回调方法的返回值都会被丢弃。还有吗如果被调用的委托中有一个抛出一个或阻塞相当长的时间,我们又无能为力。显然,这个算法还不够健壮。
由于这个算法的局限,所以MulticastDelegate类提供了一个GetInvocationList,用于显式调用链中的每一个委托,同时又可以自定义符合自己需要的任何算法:
public abstract class MulticastDelegate :Delegate {
// 创建一个委托数组,其中每个元素都引用链中的一个委托
public sealed override Delegate[] GetInvocationList();
}
GetInvocationList方法操作一个从MulticastDelegate派生的对象,返回一个有Delegate组成的数组,其中每一个引用都指向链中的一个委托对象。
下面是代码演示:
public static class GetInvocationList
{
// 定义一个 Light 组件
private sealed class Light
{
// 该方法返回 light 的状态
public String SwitchPosition()
{
return "The light is off";
}
}
// 定义一个 Fan 组件
private sealed class Fan
{
// 该方法返回 fan 的状态
public String Speed()
{
throw new InvalidOperationException("The fan broke due to overheating");
}
}
// 定义一个 Speaker 组件
private sealed class Speaker
{
// 该方法返回 speaker 的状态
public String Volume()
{
return "The volume is loud";
}
}
// 定义委托
private delegate String GetStatus();
public static void Go()
{
// 声明一个为null的委托
GetStatus getStatus = null;
// 构造三个组件,将它们的状态方法添加到委托链中
getStatus += new GetStatus(new Light().SwitchPosition);
getStatus += new GetStatus(new Fan().Speed);
getStatus += new GetStatus(new Speaker().Volume);
// 输出该委托链中,每个组件的状态
Console.WriteLine(GetComponentStatusReport(getStatus));
}
// 该方法用户查询几个组件的状态
private static String GetComponentStatusReport(GetStatus status)
{
// 如果委托链为null,则不进行任何操作
if (status == null) return null;
// 用StringBuilder来记录创建的状态报告
StringBuilder report = new StringBuilder();
// 获取委托链,其中的每个数据项都是一个委托
Delegate[] arrayOfDelegates = status.GetInvocationList();
// 遍历数组中的每一个委托
foreach (GetStatus getStatus in arrayOfDelegates)
{
try
{
// 获取一个组件的状态报告,将它添加到StringBuilder中
report.AppendFormat("{0}{1}{1}", getStatus(), Environment.NewLine);
}
catch (InvalidOperationException e)
{
// 在状态报告中生成一条错误记录
Object component = getStatus.Target;
report.AppendFormat(
"Failed to get status from {1}{2}{0} Error: {3}{0}{0}",
Environment.NewLine,
((component == null) ? "" : component.GetType() + "."),
getStatus.Method.Name, e.Message);
}
}
// 返回遍历后的报告
return report.ToString();
}
}
执行结果为:
The light is off
Failed to get status from ConsoleTest.GetInvocationList+Fan.Speed
Error: The fan broke due to overheating
The volume is loud
7.小结
- 委托封装了包含特殊返回类型和一组参数的行为,类似包含单一方法的接口。
- 委托类型声明中所描述的类型签名决定了哪个方法可用于创建委托实例,同时决定了调用的签名。
- 为了创建委托实例,需要一个方法以及(对于实例方法来说)调用方法的目标。
- 委托实例是不易变的。
- 每个委托实例都包含一个调用列表——一个操作列表。
- 委托实例可以合并到一起,也可以从一个委托实例中删除一个。
- 事件不是委托实例——只是成对的add/remove方法。
2.C# 2
2.1 方法组转换
在C#1中,如果要创建一个委托实例,就必须同时指定委托类型和要采取的操作。如下所示:
Processor proc1,proc2;
proc1 = new Processor(StaticMethods. PrintString) //静态方法,类直接调用
InstanceMethods instance = new InstanceMethods();
proc2 = new Processor (instance. PrintString) //方法,通过类的实例调用
为了简化编程,C#2支持从方法组到一个兼容委托类型的隐式转换。所谓"方法组"(method group),其实就是一个方法名。
现在我们可以使用如下代码,效果和上面的代码一模一样。
Processor proc1,proc2;
proc1 = StaticMethods.PrintString //静态方法,类直接调用
InstanceMethods instance = new InstanceMethods();
proc2 = instance.PrintString //方法,通过类的实例调用
2.2 协变性和逆变性
在前面已经说过C#2.0后,将一个方法绑定到一个委托时,C#和CLR都允许引用类型的协变性和逆变性。
协变性是指方法能返回从委托的返回类型派生的一个类型。逆变性是指方法获取的参数可以是委托的参数类型的基类。
2.3 使用匿名方法的内联委托
1.使用匿名方法
Action
在C#1中,可能一些参数不同,需要创建一个或多个很小的方法,而这些细粒度的方法管理起来又十分不便。在C#2中引入的匿名方法很好的解决了这个问题。
.NET2.0引入了一个泛型委托类型Action,它的签名非常简单:
public delegate void Action<T>
Action就是对T的一个实例执行某些操作。例如:
Action<string> printAction1 = delegate(string text){
char[] chars = text.ToCharArray();
Array.Reverse(chars);
Console.WriteLine(new string(chars));
};
Action<int> printAction2 = delegate(int s)
{
Console.WriteLine(Math.Sqrt(s));
};
private Action printAction3 = delegate
{
Console.WriteLine("没有参数");
};
printAction1("asd");
printAction2(4);
printAction3();
上述代码展示了匿名方法的几个不同特性。首先是匿名方法的语法:先是delegate关键字,再是参数(如果有的话),随后是一个代码块,其中包含了对委托实例的操作行定义的代码。值得注意的是,逆变性不适用于匿名方法:必须指定和委托类型完全匹配的参数类型。
说到实现,我们在IL中为源代码中的每个匿名方法都创建了一个方法:编译器将在已知类(匿名方法所在的类)的内部生成一个方法,并使用创建委托实例时的行为,就像它是一个普通的方法一样。如下图所示:
2.匿名方法的返回值
Predicate
Action委托的返回类型是void,所以不必从匿名方法返回任何东西。但在需要返回值的情况下怎么办呢,这就要使用.NET2.0中的Predicate委托类型。下面是它的签名:
public delegate bool Predicate<T>(T obj)
从签名中可以看到,这个委托返回的是bool类型,现在演示一下,创建一个Predicate的一个实例,其返回值指出传入的实参是奇数还是偶数。
Predicate<int> isEven = delegate (int x) { return x % 2 == 0;};
Console.WriteLine(isEven(1));
Console.WriteLine(isEven(4));
注意:从匿名方法返回一个值时,它始终从匿名函数中返回,而不是从委托实例的方法中返回。
Comparison
Comparison 委托,表示比较同一类型的两个对象的方法。下面是它的签名:
public delegate int Comparison(T x,T y)
从签名中可以看到,这个委托返回的是int 类型。Comparison是在.NET2.0中常见的委托类型,可用它来对集合排序,它是IComparer接口的委托版。通常,一种情况下只需要一个特定的排列顺序,所以采取内联的方式指定完全是合理的,不需要在其余类的内部添加一个独立的方法来指定该顺序。此委托由 Array 类的 Sort(T[], Comparison) 方法重载和 List 类的 Sort(Comparison)方法重载使用,用于对数组或列表中的元素进行排序。
internal class Program
{
private static void Main(string[] args)
{
Program p = new Program();
SortAndShowFiles("Sorted by name:",delegate (FileInfo f1,FileInfo f2)
{
return f1.Name.CompareTo(f2.Name);
});
SortAndShowFiles("Sorted by lenth:", delegate(FileInfo f1, FileInfo f2)
{
return f1.Length.CompareTo(f2.Length);
});
Console.Read();
}
static void SortAndShowFiles(string title, Comparison<FileInfo> sortOrder)
{
FileInfo[] files = new DirectoryInfo(@"C:\").GetFiles();
Array.Sort(files,sortOrder);
foreach (var fileInfo in files)
{
Console.WriteLine("{0} ({1} byte)",fileInfo.Name,fileInfo.Length);
}
}
}
3.忽略委托参数
在少数情况下,你实现的委托可能不依赖于它的参数值。你可能想写一个事件处理程序,它的行为只适用于一个事件,而不依赖事件的实际参数。如下面的例子中,可以完全省略参数列表,只需要使用一个delegate关键字,后跟作为方法的操作使用的代码块.
Button button = new Button();
button.Test = "Click me";
button.Click += delegate{ Console.WriteLine("LogClick");};
button.KeyParess+= delegate{ Console.WriteLine("LogKey");};
一般情况下,我们必须像下面这样写:
button.Click += delegate (object sender, EventArgs e){.....};
那样会无谓地浪费大量空间——因为我们根本不需要参数的值,所以编译器现在允许完全省略参数。
4.在匿名方法中捕捉变量
1.定义闭包和不同的变量类型
闭包的基本概念是:一个函数除了能通过提供给它的参数与环境互动之外,还能同环境进行更大程度的互动,这个定义过于抽象,为了真正理解它的应用情况,还需要理解另外两个术语:
外部变量:指其作用域包括一个函数方法的局部变量或参数(ref和out参数除外)。在可以使用匿名方法的地方,this引用也被认为是一个外部变量。
被捕捉的外部变量:通常简称为被捕获的变量,它在匿名方法内部使用的外部变量。
重新看一下"闭包"的定义,其中所说的"函数"是指匿名方法,而与之互动的"环境"是指由这个匿名方法捕捉到的变量集合。
它主要强调的是,匿名方法能使用在声明该匿名方法的方法内部定义的局部变量。
void EnclosingMethod()
{
int outervariable = 5; //外部变量 未捕获
string capturedVariable = "captured"; //被匿名方法捕获的外部变量
Action x = delegate()
{
string anonLocal = "local to anonymous method "; //匿名方法的局部变量
Console.WriteLine(anonLocal + capturedVariable); //捕获外部遍历
};
x();
}
下面描述了从最简单到最复杂的所有变量:
anonLocal:它是匿名方法的局部变量,但不是EnclosingMethod的局部变量
outervariable:它是外部变量,因为在它的作用域内声明了一个匿名方法。但是,匿名方法没有引用它,所以它未被捕获。
capturedVariable:它是一个外部变量,因为在它的作用域内声明了一个匿名方法。但是,匿名方法内部引用引用了它,所以它成为了一个被捕获的变量。
2.测试被捕获的变量的行为
void EnclosingMethod(){
string captured = "在x之前创建";
Action x = delegate{
Console.WriteLine(captured);
captured = "被x改变了";
};
captured = "在x第一次调用之前";
x();
Console.WriteLine(captured);
captured = "在x第二次调用之前";
x();
}
输出结果:
在x第一次调用之前
被x改变了
在x第二次调用之前
3.捕获变量有什么用
简单的说,捕获变量能简化编程,避免专门创建一些类来存储一个委托需要处理的信息(作为参数传递的信息除外)。
举个例子,假定有一个任务列表,并希望写一个方法来返回包含低于特定年龄的所有人的另一个列表。其中,我们知道List有一个方法能返回一个新列表,这个方法就是FindAll。但是,在匿名方法和捕获变量问世之前,List.FindAll的存在并没有多大意义,因为创建一个适合的委托是在太麻烦了。但是在C#2中,这个操作变量非常简单:
List<Person> Find(List<Person> people,int limit){
return people.FindAll(delegate(Person person){
return person.Age < limit; //limit是被捕获的外部变量
});
}
4.捕获变量的延长生命周期
对于一个被捕捉的变量,只要还有任何委托实例在引用它,它就会一直存在。
被捕捉的变量存在于编译器创建的一个额外的类中,相关的方法会引用该类的实例。
5.局部变量实例化
当一个变量被捕捉时,捕捉的变量的"实例"。如果在循环内捕捉变量,第一循环迭代的变量将有别于第二次循环时捕获的变量,以此类推。
6.捕获变量的使用规则和小结
使用规则
- 如果用或不用捕获变量时的代码同样简单,那就不用
- 捕捉由for或foreach语句声明的变量之前,思考你的委托是否需要在循环迭代结束之后延续,以及是否想让它看到那个变量的后续值。否则的话,就在循环内另建一个变量,用来复制你想要的值。
- 如果创建多个委托实例,而且捕获了变量,思考一下是否希望它们捕获同一个变量
- 如果捕获的变量不会发生改变,那就不要这么多担心。
小结
- 捕获的变量的生命周期变长了,至少和捕捉它的委托一样长。
- 多个委托可以捕获同一个变量
- 在循环内部,同一个变量声明实际会引用不同的变量"实例"
- 在for/foreach循环的声明中创建的变量仅在循环持续期间有效
-
必要时创建额外的类型来保存捕获的变量
5.小结
C# 2根本性地改变了委托的创建方式,这样我们就能在.NET Framework的基础上采取一种更函数化的编程风格。
C# 3
1. 作为委托的Lambda表达式
1.Func<T, TResult>
Func<T, TResult> 委托,封装一个具有一个参数并返回 TResult 参数指定的类型值的方法。下面是它的签名:
public delegate TResult Func<in T, out TResult>(T arg)
从签名中可以看到,这个委托返回的是TResult类型。可以使用此委托表示一种能以参数形式传递的方法,而不用显式声明自定义委托。
封装的方法必须与此委托定义的方法签名相对应。也就是说,封装的方法必须具有一个通过值传递给它的参数,并且必须返回值。
在使用 Func<T, TResult>委托时,不必显式定义一个封装只有一个参数的方法的委托。
例如,以下代码显式声明了一个名为 ConvertMethod 的委托,并将对UppercaseString方法的引用分配给其委托实例。
using System;
delegate string ConvertMethod(string inString);
public class DelegateExample
{
public static void Main()
{
// Instantiate delegate to reference UppercaseString method
ConvertMethod convertMeth = UppercaseString;
string name = "Dakota";
// Use delegate instance to call UppercaseString method
Console.WriteLine(convertMeth(name));
}
private static string UppercaseString(string inputString)
{
return inputString.ToUpper();
}
}
以下示例简化了此代码,它所用的方法是实例化 Func<T, TResult> 委托,而不是显式定义一个新委托并将命名方法分配给该委托。
public class GenericFunc
{
public static void Main()
{
// Instantiate delegate to reference UppercaseString method
Func<string, string> convertMethod = UppercaseString;
string name = "Dakota";
// Use delegate instance to call UppercaseString method
Console.WriteLine(convertMethod(name));
}
private static string UppercaseString(string inputString)
{
return inputString.ToUpper();
}
}
您也可以按照以下示例所演示的那样在 C# 中将 Func<T, TResult> 委托与匿名方法一起使用。
public class Anonymous
{
public static void Main()
{
Func<string, string> convert = delegate(string s)
{ return s.ToUpper();};
string name = "Dakota";
Console.WriteLine(convert(name));
}
}
2.第一次转换成Lambda表达式
用一个匿名方法来创建委托实例,如:
Func<string,int> returnLength;
returnLength = delegate (string text) { return text.Length; };
Console.WriteLine(returnLength("Hello"));
最终的结果为"5"这是意料之中的事。值得注意的是,returnLength的声明和赋值是分开的,否则一行可能放不下,这样还有利于代码的理解。
匿名方法是加粗的一部分,也是打算转换成Lambda表达式的部分。
Lambda表达式最冗长的形式是:
(显式类型参数列表) => {语句}
=>部分是C#3新增的,他告诉编译器我们正在使用一个Lambda表达式。Lambda表达式大多数时候都和一个返回非void的委托类型配合使用——如果不返回结果,语法就不像现在这样一目了然了。这标志着C#1和C#3在用法习惯上的另一个区别。在C#1中,委托一般用于事件,很少会返回什么。在LINQ中,它们通常被视为数据管道的一部分,接收输入并返回结果,或者判断某项是否符合当前的筛选器等等。
这个版本包含了显式参数列表,并将语句放到大括号中,他看起来和匿名方法非常相似,代码如下:
Func<string,int> returnLength;
returnLength = (string text) => { return text.Length; };
Console.WriteLine(returnLength("Hello"));
同样的,加粗的那一部分是用于创建委托实例的表达式。在阅读Lambda表达式时,可以将=>部分看错"goes to"。
匿名方法中控制返回语句的规则同意适用于lambda表达式:如果返回值是void,就不能从Lambda表达式返回一个值;如果有一个非void的返回值类型,那么每个代码路径都必须返回一个兼容的值。
3.用单一表达式作为主题
大多数时候,都可以用一个表达式来表示整个主体,该表达式的值是Lambda的结构。在这些情况下,可以只指定哪个表达式,不使用大括号,不使用return语句,也不添加分号。格式如下:
(显示类型的参数列表) => 表达式
在这个例子中,Lambda表达式变成了:
(string text) => text.Length
4.隐式类型的参数列表
编译器大多数情况下都能猜出参数类型,不需要你显式声明它们。在这些情况下,可以将Lambda表达式写成:
(隐式类型的参数列表) => 表达式
隐式类型的参数列表就是以一个逗号分隔的名称列表,没有类型。但隐式和显式类型的参数不能混合使用——要么全面是显式类型参数,要么全部是隐式类型参数。除此之外,如果有任何out或ref参数,就只能使用显式类型。在我们的例子中,还可以简化成:
(text) => text.Length
5.单一参数的快捷语法
如果Lambda表达式只需要一个参数,而且这个参数可以隐式指定类型,就可以省略小括号。这种格式的Lambda表达式是:
参数名 => 表达式
因此,我们例子中Lambda表达式最红形式是:
text => text.Length
值得注意的是,如果愿意,可以用小括号将整个Lambda表达式括起来。