首页 > 代码库 > 【C#】delegate 和 event

【C#】delegate 和 event

在开始之前,先说一下文章的表达习惯。

Object a = new Object();

在上面的例子里,Object 是一种类型,a 是一个引用,new Object() 构造了一个对象,有时 ”构造对象“ 也被称为 ”创建实例“。有的文章习惯把 a 也称作实例,请根据上下文理解不要混淆。接下来你会经常看到类似 ”Object 类型“ ”Object 引用“ ”Object 对象“ 这样的表达。

另外,我会尽量使用 ”方法“ 而不使用 ”函数“ 的称呼。习惯使用 C/C++ 的读者接下来看到 ”方法“ 时不要感到困惑。

 

delegate 简介

delegate 的中文意思是委托,在 c# 里也是一个关键字。在 System 命名空间里,有 Delegate 类 和 MulticastDelegate 类,后者继承前者。编译器和其他工具可以从 MulticastDelegate 派生,但是我们无法显式的去继承这两个类。为了使用 delegate,我们需要使用 delegate 关键字来声明一个 delegate 类型:

public delegate int MyDelegate(int arg);

和其他类型一样,这个声明可以放在一个命名空间内部或其他类的内部。我们只需要写这样一行代码,编译器为我们实现细节。注意 delegate 关键字后面的部分,可以是任何合理的方法签名(方法签名由方法的参数列表和返回值组成,这两者都相同的方法具有相同的签名)。

现在我们已经声明了一个 delegate 类型 MyDelegate,接下来用这个类型创建一个实例:

MyDelegate sample = new MyDelegate(Method);

看起来和创建其他类的实例没有什么区别,但是构造函数的参数 Method 是什么呢?Method 必须是与 MyDelegate 声明时的签名一致的方法名称,也就是说 Method 的签名必须含有一个 int 参数和 int 返回值(这样描述并不准确,由于C#的协变和逆变的特性,详细参考:委托中的协变和逆变)。delegate 是引用类型,所以如果没有为 sample 初始化引用实例,那么 sample 的值是 null。

现在有一个问题。如果 Method 是静态方法,那么这样传递参数完全没有问题;如果 Method 不是静态方法,这样就不一定正确了。我们知道普通方法必须由具体的对象来发起调用,所以传递一个普通方法给 delegate 的构造函数而不指定所属对象是没有道理的。如果上面这段代码在类的普通方法里,由于 this 关键字可以省略,所以 Method 被编译器理解为 this.Method,所属对象是明确的,所以仍是正确的;如果上面这段代码在类的静态方法里,那么会因为 Method 没有明确的所属对象而错误。这个细节需要留意。

最后我们需要了解如何调用 delegate 实例里存储的方法。

int a = sample(3);

看起来与直接调用 Method 没有什么区别。的确如此,仅需要注意 delegate 引用如果为 null 则会引起空引用异常。delegate 引用与其他类型的引用一样,还可以执行 = 赋值运算、作为方法的参数传递、使用点运算符 . 访问对象成员。

目前为止,您可能觉得 delegate 与 C/C++ 里的函数指针很相似。但是,delegate 类型还可以执行 +/+= 和 -/-= 操作,因为 delegate 对象并不只是保存一个方法的引用,而是保存了一个方法列表。

sample -= Method;sample += Method;

- 用于从方法列表里移除一个方法,+ 用于增加一个方法。+ 操作不会排除完全相同的方法引用。现在,sample(3) 的意义是依次调用方法列表里的每个方法,最后一个方法的返回值作为 sample(3) 的返回值。

请注意类型转换问题。在上面的代码里,我们把方法与 delegate 相加。其实,还可以直接把方法赋值给 delegate 引用。这些做法之所以合理,是因为方法可以隐式转化为 delegate。

为了简化编程,System 命名空间里预声明了一系列的泛型 delegate。使用这些声明我们几乎不再需要做额外的声明工作了。

public delegate void Action();public delegate void Action<T>(T arg);public delegate void Action<T1, T2>(T1 arg1, T2 arg2);// 还有更多参数版本的 Actionpublic delegate TResult Func<TResult>();public delegate TResult Func<T, TResult>(T arg);public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);// 还有更多参数版本的 Func

 

匿名方法和 Lambda 表达式

在上文中,我们必须事先声明 Method 方法才能初始化 delegate 实例。C# 提供了匿名方法来简化 delegate 的初始化工作。

MyDelegate sample = delegate(int arg){    return arg * arg;};

看起来好像在运行代码内部声明了一个新方法并使用 sample 保存。这样做使得代码更简洁。注意匿名方法的返回类型,必须是或可以隐式转换为 delegate 声明时签名的返回类型。匿名方法还有一个好处是可以访问外部变量,匿名方法捕获外部变量的引用而不是值,这一点需要留意,请看这段代码:

int n = 0;sample = delegate(int arg) { return n * arg; };n = 1;print(sample(1));

结果将输出 1 而不是 0。另外匿名方法里无法访问外部的 ref 或 out 参数。

Lambda 表达式是一种匿名函数。在 lambda 运算符 => 的左边指定输入参数,在右边指定执行表达式或语句。

sample = x => x * x;

看起来与上文的匿名方法非常相似。实际上匿名方法的所有限制和特征在 lambda 表达式中同样适用。

先看 Lambda 表达式的输入参数部分:

// 无参数使用()() => DoSomeThing();// 一个参数直接写出参数名arg => arg * arg;// 多个参数使用,分割(arg1, arg2) => arg1 == arg2;// 参数类型无法推断时可以显式指定(string arg1, char arg2) => arg1.Split(arg2);

在这些例子里,=> 的右侧都是单一表达式,如果需要返回值,那么表达式的值就是返回值。使用大括号可以书写多条语句,但必须使用 return 明确指定返回值。

(arg1, arg2) =>{    if(arg1 == arg2) return 0;    return arg1 > arg2 ? 1 : -1;}

 

event 简介

event 中文意思是事件,在 c# 里也是一个关键字。如下是 event 的声明方式:

public event MyDelegate myEvent{    add { print("add: " + value); }    remove { print("remove: " + value); }}

看起来与属性的声明相似。event 具有两种操作:+= 和 -=,右操作数必须为与 event 类型一致的 delegate(或 null)。+= 调用的是 add,-= 调用的是 remove。下面是调用的示例:

myEvent += null;myEvent -= null;

结果打印出 "add: " 和 "remove: "。

有一个更简单的声明方法:

public event MyDelegate myEvent;

这种声明类似于(实际并不相同):

private MyDelegate _myEvent;public event MyDelegate myEvent{    add { _myEvent += value; }    remove { _myEvent -= value; }}

使用这种简单的声明方法,你可以在 myEvent 所在的类中像对 delegate 一样对 event 进行操作,包括 = 赋值运算和调用方法;但是在其他地方,你只能使用 += 和 -= 运算。这样做的目的是,把所有可能不安全的操作封装在类内部。对就是这样,没有更多了。

 

最后,文章的目的是尽可能用简单的语言把委托和事件描述清楚,因此忽略了所有的异步操作等相关细节。更细节的资料请参考 msdn 或这篇文章:C# in Depth: Delegates and Events。
 
 

【C#】delegate 和 event