委托与事件(上)


摘 要

委托与事件,这是一个老生常谈的话题,很多人在讲,很多人在用,但似乎它是一个永远也说不完道不尽的东西。那么,到底什么是委托?什么是事件?委托链又是怎么回事?为什么使用事件时常常用到+=/-=?委托又是如何支持协变和逆变的呢?你喜欢使用Action和Func<T,TResult>吗?由于内容比较多,这一章将分上、下两部分慢慢为你讲解。

第一节 委托

回调函数是Windows编程语言中一种常见而有用的编程实践,在C/C++中,它指的是函数调用的指针,通过这个指针可以方便地对函数进行调用,当然这个指针也是可以被传递给别的函数使用。在.NET Framework中,回调是通过委托来实现的,当然在这里,比起非托管的C/C++,.NET中的委托提供了更丰富的功能,比如同步和异常调用、委托链等等。

委托其实是一种类型,是一种定义方法签名的类型,它支持以new的方式来实例化。委托是使用关键字delegate进行定义的,它其实是对方法的包装和聚集。既然它也是一种类型,所以能定义类的地方都可以定义委托,如下是一个委托的声明:

public delegate void ShowMessage( string msg);

任何与委托签名匹配的方法都可以分配给委托,这就要求该方法的返回值类型与参数列表必须与委托的签名相匹配,方法可以是静态的,也可以是对象级的,通过委托可以对分配给委托的方法进行调用。我们来看一下编译器来干了什么事:

通过上图我们可以看出,编译器让这个委托类型继承了System.MulticastDelegate类,System.MuticastDelegate类又继承了Delegate类,如下:

public abstract class
MulticastDelegate : Delegate
{
// ...}

同时还生成了三个方法,其中BeginInvoke()和EndInvoke()两个方法是供异步调用,Invoke()方法是供同步调用。其实通过IL更能明确的看出委托最终经过编译器生成的是一个类:

在Delegate类中有两个非常重要的字段:

internal object _target; 当委托包装的是一个静态方法时,该字段为null;当委托包装的是一个对象方法时,该字段引用的是该对象。该字段可以通过属性Target获取。

internal IntPtr _methodPtr; 保存着一个方法的IntPtr值,属性Method 获取一个标识了该回调方法的对象(MethodInfo)的引用,在类的内部,这个MethodInfo对象是通过方法GetMethodImpl()运算生成来的。

我们分别定义一个实例级方法ShowString()和一个静态方法StaticShowString(),代码:

复制代码
void
ShowString(String str)
{
Console.WriteLine(
" ShowString: " +
str);
}
public class
Code_05_01
{
public static void StaticShowString( string
str)
{
Console.WriteLine(
" StaticShowString: " +
str);
}
}
复制代码

现在我们来看一下运行时的这两个属性,如下图:

上图中显示了Target指向的是对象,下图中由于绑定了一个静态方法,所以Target是null。

MulticastDelegate类有两个私有字段:

private IntPtr _invocationCount; 保存了委托链中方法个数。

private object _invocationList; 保存的即是委托链(方法集合)

MulticastDelegate类重写了Delegate的一个虚方法public virtual Delegate[] GetInvocationList(),来获取委托的调用列表。

对委托的调用也有两种方式,可以同步调用也可以异步调用,同步调用有两种方法:直接调用和使用委托对象的Invoke方法,如下:

复制代码
void
TestCall()
{
ShowMessage callShow 
new
ShowMessage(ShowString);
callShow(
" abc "
);
}
void
TestInvoke()
{
ShowMessage invokeShow 
new
ShowMessage(ShowString);
invokeShow.Invoke(
" abc "
);
}
复制代码

这两种调用有什么区别呢?我们来看一下它们的IL。

TestCall.IL:

复制代码
.method private hidebysig instance void TestCall()  cil managed
{
//  代码大小 27 (0x1b) .maxstack 3 .locals init ([ 0class
ConsoleApp.Example05.ShowMessage callShow)
IL_0000: nop IL_0001: ldarg.0 IL_0002: ldftn instance void ConsoleApp.Example05.Code_05::ShowString( string
)
IL_0008: newobj instance void ConsoleApp.Example05.ShowMessage::.ctor( object
,
native
int)
IL_000d: stloc.0 IL_000e: ldloc.0 IL_000f: ldstr " abc " IL_0014: callvirt instance void ConsoleApp.Example05.ShowMessage::Invoke( string
)
IL_0019: nop IL_001a: ret //  end of method Code_05::TestCall
复制代码

TestInvoke.IL:

复制代码
.method private hidebysig instance void TestInvoke()  cil managed
{
//  代码大小 27 (0x1b) .maxstack 3 .locals init ([ 0class
ConsoleApp.Example05.ShowMessage invokeShow)
IL_0000: nop IL_0001: ldarg.0 IL_0002: ldftn instance void ConsoleApp.Example05.Code_05::ShowString( string
)
IL_0008: newobj instance void ConsoleApp.Example05.ShowMessage::.ctor( object
,
native
int)
IL_000d: stloc.0 IL_000e: ldloc.0 IL_000f: ldstr " abc " IL_0014: callvirt instance void ConsoleApp.Example05.ShowMessage::Invoke( string
)
IL_0019: nop IL_001a: ret //  end of method Code_05::TestInvoke
复制代码

其实两者的内部调用实现基本一样,都是对委托对象的方法Invoke进行调用。

委托链 也叫多路广播委托,是在委托内部由委托对象构成的一个委托对象集合,可以通过委托来调用委托链内的所有委托包装的方法。Delegate有两个静态方法,Combine()用于创建委托链和委托链添加新的委托,我们假设有一个委托委托A,来模拟向委托链追加委托的过程:

(1) 委托A对象在实例化的时候已经包装了一个方法,

(2) 当调用Delegate.Combine()方法向委托A追加新委托B对象时,在内部会重新创建一个委托对象C,并且用新追加的委托(方法)初始化_target和_methodPtr字段;

(3) 将委托C的_invocationList初始化为一个委托对象数组,并将委托A放到这个数组的第1项(索引为0)位置,然后将新的委托B对象放到数据的第2项位置(这里会根据委托的个数依次递增,新增加的那个委托对象总是在这个数组的最后位置),最后返回这个新创建的委托对象C。

当再次向新委托C追加委托成员时,会重复(1)-(3)的步骤,每次最终都会返回一个新创建的委托,并且字段_target和_methodPtr总是根据新增加的委托对象来实例化,不过此时的这两个字段好像用处已经不大了,它只是保存了是最后进来的一个委托对象的部分数据。很显然,如果一个委托只包装了一个方法,并没有因追加新的委托而创建委托链,那么在这种情况下,这两个字段_target和_methodPtr是非常有意义的。

与方法Combine()对应的有一个方法public static Delegate Remove(Delegate source, Delegate value);很显然它是从委托链中移除一个委托对象。为了方便书写,C#为委托类型的实例重载了两个操作符+=和-=分别对应于方法Combine和方法Remove。通过下图我们可以看一下追加委托的过程。继续对上面的代码进行改造,增加一个对象级方法:

void
ShowString2(String str)
{
Console.WriteLine(
" ShowString2: " +
str);
}

然后实例化一个委托ShowMessage show3 = new ShowMessage(ShowString);,如下图:

可以看到此时show3的Method指向的方法是ShowString(System.String),并且字段_invocationCount的值为0,_invocationList是null。

接下来我们向show3追加一个委托对象(事实上是向委托链追加),show3 += new ShowMessage(ShowString2);,也可以使用简写:show3 += ShowString2;这样不用手写代码来创建委托对象,但在编译的过程中,编译器还是会识别出这是一个创建委托对象的过程并向IL中写入创建委托对象的代码。如下图:

此时,我们已经看到Method指向的是新的方法ShowString2(System.String),_invocationCount的值为2,_invocationList已经是一个拥有两个元素的集合。

对委托对象有委托链且不为空的时候,又是如何调用委托链内的各个回调函数的呢?通过上面对Invoke的讨论,我们知道当调用一个委托对象的回调函数时,在内部CLR实际上是调用了Invoke方法,而在调用invoke方法时,该委托会发现字段_invocationList不为null,接着就会遍历该数组中的所有委托对象依次对委托方法进行调用。

协变性与逆变性

委托的协变性 是指委托方法能返回从对应委托的返回类型派生的一个类型。

委托的逆变性 是指方法获取的参数类型可以是委托的参数类型的基类。

这里的描述的有点绕口,我们来年如下代码:

复制代码
public class
Code_05_02
{
public string Name {  getset
; }
}
public class
Code_05_03 : Code_05_02
{
public int Age {  getset
; }
}
// 定义一个委托MyDel public delegate  Code_05_02 MyDel(Code_05_03 para); // 定义一个与委托MyDel 相匹配的方法 private
Code_05_03 GetData(Code_05_02 para)
{
return new
Code_05_03();
}
// 以下的实例化及调用是可行的。 MyDel del =  new
MyDel(GetData);
del(
new Code_05_03());
复制代码

Code_05_03类继承于Code_05_02类,委托MyDel的返回类型是Code_05_02,方法GetData的返回类型是Code_05_03,这体现的协变性;方法GetData的参数类型是Code_05_02,委托的参数类型是Code_05_03,这体现了逆变性。

需要说明一点的是:协变性和逆变性不能用于值类型(包括void)。

在我们开发的过程中,可能经常要使用委托,但委托的定义都大同小异,很幸运的是.NET Framework为我们预定义了很多的常用泛型委托。

无返回值的Action系列:

public delegate void Action< in T> (T obj) public delegate void Action< in T1,  in T2> (T1 arg1, T2 arg2) // 共有16个,另外还有一个无参的非泛型委托: public delegate void Action()

使用非常简单,代码示例:

void
TestAction()
{
Action
< string> act =  new Action< string>
(ShowString);
act(
" Action "
);
}

有返回值的Func<T,TResult>系列:

public delegate TResult Func< in T,  out TResult> (T arg) public delegate TResult Func< in T1,  in T2,  out TResult> (T1 arg1, T2 arg2) // 共有16个,另外还有一个无参的泛型委托: public delegate TResult Func< out TResult>()

一般的时候,我们使用这些委托已经足够了。详细内容可以查询MSDN: Action<T>系列 和 Func<T,TResult>系列 。

C#的lambda表达式为委托的简化使用显示出了很不错的编程体验。可以参考MSDN的相关章节:Lambda 表达式(C# 编程指南) 。

这一章的上半部分,我们主要讲解了与委托相关的内容,后一篇的下半部分将主要讲解什么是事件及委托如何与事件共事。

 

你可能感兴趣的:(C#,委托与事件)