(转).NET面试题系列[7] - 委托与事件
委托和事件
委托在C#中具有无比重要的地位。
C#中的委托可以说俯拾即是,从LINQ中的lambda表达式到(包括但不限于)winform,wpf中的各种事件都有着委托的身影。C#中如果没有了事件,那绝对是一场灾难,令开发者寸步难行。而委托又是事件的基础,可以说是C#的精髓,个人认为,其地位如同指针之于C语言。
很多开发者并不清楚最原始版本的委托的写法,但是这并不妨碍他们熟练的运用LINQ进行查询。对于这点我只能说是微软封装的太好了,导致我们竟可以完全不了解一件事物的根本,也能正确无误的使用。而泛型委托出现之后,我们也不再需要使用原始的委托声明方式。
CLR via C#关于委托的内容在第17章。委托不是类型的成员之一,但事件是。委托是一个密封类,可以看成是一个函数指针,它可以随情况变化为相同签名的不同函数。我们可以通过这个特点,将不同较为相似的函数中相同的部分封装起来,达到复用的目的。
回调函数
回调函数是当一个函数运行完之后立即运行的另一个函数,这个函数需要之前函数的运行结果,所以不能简单的将他放在之前的函数的最后一句。回调函数在C#问世之前就已经存在了。在C中,可以定义一个指针,指向某个函数的地址。但是这个地址不携带任何额外的信息,比如函数期望的输入输出类型,所以C中的回调函数指针不是类型安全的。
如果类型定义了事件成员,那么其就可以利用事件,通知其他对象发生了特定的事情。你可能知道,也可能不知道事件什么时候会发生。例如,Button类提供了一个名为Click的事件,该事件只有在用户点击了位于特定位置的按钮才会发生。想象一下如果不是使用事件,而是while轮询(每隔固定的一段时间判断一次)的方式监听用户的点击,将是多么的扯淡。事件通过委托来传递信息,可以看成是一个回调的过程,其中事件的发起者将信息通过委托传递给事件的处理者,后者可以看成是一个回调函数。
委托的简单调用 – 代表一个相同签名的方法
委托可以接受一个和它的签名相同的方法。对于签名相同,实现不同的若干方法,可以利用委托实现在不同情况下调用不同方法。
使用委托分为三步:
1. 定义委托
2. 创建委托的一个实例,并指向一个合法的方法(其输入和输出和委托本身相同)
3. 同步或异步调用方法
在下面的例子中,委托指向Select方法,该方法会返回输入list中,所有大于threshold的成员。
//1.Define
public delegate List<int> SelectDelegate(List<int> aList, int threshold);
class Program
{
static void Main(string[] args)
{
var list = new List<int>();
//Add numbers from -5 to 4
list.AddRange(Enumerable.Range(-5, 10));
//2.Initialize delegate, now delegate points to function 'Predicate'
SelectDelegate sd = Select;
//3.Invoke
list = sd.Invoke(list, 1);
//Only member > 1 are selected
Console.WriteLine("Now list has {0} members.", list.Count);
}
public static List<int> Select(List<int> aList, int threshold)
{
List<int> ret = new List<int>();
foreach (var i in aList)
{
if (i > threshold)
{
ret.Add(i);
}
}
return ret;
}
}
委托的作用 – 将方法作为方法的参数
在看完上面的例子之后,可能我们仍然会有疑惑,我们直接调用Select方法不就可以了,为什么搞出来一个委托的?下面就看看委托的特殊作用。我个人的理解,委托有三大重要的作用,提高扩展性,异步调用和作为回调。
首先来看委托如何实现提高扩展性。我们知道委托只能变身为和其签名相同的函数,所以我们也只能对相同签名的函数谈提高扩展性。假设我们要写一个类似计算器功能的类,其拥有四个方法,它们的签名都相同,都接受两个double输入,并输出一个double。此时常规的方法是:
public enum Operator
{
Add, Subtract, Multiply, Divide
}
public class Program
{
static void Main(string[] args)
{
double a = 1;
double b = 2;
Console.WriteLine("Result: {0}", Calculate(a, b, Operator.Divide));
}
public static double Calculate(double a, double b, Operator o)
{
switch (o)
{
case Operator.Add:
return Add(a, b);
case Operator.Subtract:
return Subtract(a, b);
case Operator.Multiply:
return Multiply(a, b);
case Operator.Divide:
return Divide(a, b);
default:
return 0;
}
}
public static double Add(double a, double b)
{
return a + b;
}
public static double Subtract(double a, double b)
{
return a - b;
}
public static double Multiply(double a, double b)
{
return a * b;
}
public static double Divide(double a, double b)
{
if (b == 0) throw new DivideByZeroException();
return a / b;
}
}
我们通过switch分支判断输入的运算符号,并调用对应的方法输出结果。不过,这样做有一个不好的地方,就是如果日后我们再增加其他的运算方法(具有相同的签名),我们就需要修改Calculate方法,为switch增加更多的分支。我们不禁想问,可以拿掉这个switch吗?
如何做到去掉switch呢?我们必须要判断运算类型,所以自然的想法就是将运算类型作为参数传进去,然而传入了运算类型,就得通过switch判断,思维似乎陷入了死循环。但是如果我们脑洞开大一点呢?如果我们通过某种方式,传入add,subtract等方法(而不是运算类型),此时我们就不需要判断了吧。
也就是说代码就是如下的样子:
double a = 1;
double b = 2;
//Parse function as parameter
Console.WriteLine("Result: {0}", Calculate(a, b, Add));
Console.WriteLine("Result: {0}", Calculate(a, b, Subtract));
我们假设电脑十分聪明,看到我们传入Add,就自动做加法,看到传入Subtract就做减法,最后输出3和-1。这种情况下我们当然不需要switch了。那么现在问题来了,这个 Calculate方法的签名是怎么样的?我们知道a和b都是double,那么第三个参数是什么类型?什么样的类型既可以代表Add又可以代表Subtract?我想答案已经呼之欲出了吧。
第三个参数当然就是一个委托类型。首先委托本身由于要和方法签名相同,故委托的定义只能是:
public delegate double CalculateDelegate(double a, double b);
第三个参数的签名也只能是:
public static double Calculate(double a, double b, CalculateDelegate cd)
完整的实现:
static void Main(string[] args)
{
double a = 1;
double b = 2;
//Parse function as parameter
Console.WriteLine("Result: {0}", Calculate(a, b, Add));
Console.WriteLine("Result: {0}", Calculate(a, b, Subtract));
}
//Invoke delegate and return corresponding result
public static double Calculate(double a, double b, CalculateDelegate cd)
{
return cd.Invoke(a, b);
}
public static double Add(double a, double b)
{
return a + b;
}
public static double Subtract(double a, double b)
{
return a - b;
}
public static double Multiply(double a, double b)
{
return a * b;
}
public static double Divide(double a, double b)
{
if (b == 0) throw new DivideByZeroException();
return a / b;
}
我们看到,我们彻底摈弃了switch这个顽疾,使得代码的扩展性大大增强了。假设哪天又来了第五种运算,我们只需要增加一个签名相同的方法:
public static double AnotherOperation(double a, double b)
{
//TODO
}
然后调用即可:
Console.WriteLine("Result: {0}", Calculate(a, b, AnotherOperation));
扩展阅读:函数式编程
许多人初学委托无法理解的一个重要原因是,总是把变量和方法看成不同的东西。方法必须输入若干变量,然后对它们进行操作,最后输出结果。但是实际上,方法本身也可以看成是一种特殊类型的变量。
相同签名的方法具有相同的类型,在C#****中,这个特殊的类型有一个名字,就叫做委托。如果说double****代表了(几乎)所有的小数,那么输入为double****,输出为double****的委托,代表了所有签名为****输入为double****,输出为double****的方法。所以,方法是变量的一种形式,方法既然可以接受变量,当然也可以接受另一个方法。
函数式编程是继面向对象之后未来的发展方向之一。简单来说,就是在函数式编程的环境下,你是在写函数,将一个集合通过函数映射到另一个集合。例如f(x)=x+1就是一个这样的映射,它将输入集合中所有的元素都加1,并将结果作为输出集合。由于你所有的函数都是吃进去集合,吐出来集合,所以你当然可以pipeline式的进行调用,从而实现一连串操作,既简单又优雅。
许多语言,例如javascript,C#都有函数式编程的性质。在以后的文章中,我们可以看到LINQ有很多函数式编程的特点:pipeline,currying等。有关函数式编程的内容可以参考:http://coolshell.cn/articles/10822.html以及http://www.ruanyifeng.com/blog/2012/04/functional_programming.html
委托的作用 – 异步调用和作为回调函数,委托的异步编程模型(APM)
通过委托的BeginInvoke方法可以实现异步调用。由于委托可以代表任意一类方法,所以你可以通过委托异步调用任何方法。对于各种各样的异步实现方式,委托是其中最早出现的一个,在C#1.0就出现了,和Thread的历史一样长。
异步调用有几个关键点需要注意:
- 如何取消一个异步操作?
- 如何获得异步调用的结果?
- 如何实现一个回调函数,当异步调用结束时立刻执行?
- 对于各种异步实现方式,都要留心上面的几个问题。异步是一个非常巨大的话题,我现在也没有学到熟练的地步。
实现一个简单的异步调用首先我们需要一个比较耗时的任务。在这里我打算通过某种算法,判断某个大数是否为质数。
public static bool IsPrimeNumber(long number)
{
if (number == 1) throw new Exception("1 is neither prime nor composite number");
if (number % 2 == 0) return false;
//int sqrt = (int) Math.Floor(Math.Sqrt(number));
for (int i = 2; i < number; i++)
{
if (number%i == 0) return false;
}
return true;
}
上面的算法中我故意撤去了计算平方根这步,使得算法的性能大大变差了,达到耗时的目的。为了拖慢时间,我们找一个巨大的质数1073676287,这样,整个for循环要全部运行一次才会结束,而不会提早break。
为了异步调用,要先声明一个和方法签名相同的委托才行:
public delegate void ClongBigFileDelegate(string path);
然后,我们就在主程序中简单的异步调用。我们发现BeginInvoke的参数数目比Invoke多了两个,不过现在我们先不管它,将它们都设置为null:
IsPrimeNumberDelegate d = new IsPrimeNumberDelegate(IsPrimeNumber);
d.BeginInvoke(1073676287, null, null);
Console.WriteLine("I am doing something else.");
Console.ReadKey();
这样虽然实现了异步调用(主程序会马上离开BeginInvoke打印下面的话),但也有很多问题:
- 如果不加上Console.ReadKey,主程序会直接关闭,因为唯一的前台线程结束运行了(winform则不存在这个问题,除非你终止程序,前台线程永远不会结束运行)
- 异步调用具体什么时候结束工作不知道。可能很快就结束了,可能刚进行了5%,总之就是看不出来(但如果你手贱敲了任意一个键,程序立马结束),也不能实现“当异步调用结束之后,主程序继续运行某些代码”
- 算了半天,不知道结果...
你可能也想到了,BeginInvoke后两个神秘的输入参数可能能帮你解决上面的问题。
通过EndInvoke获得异步委托的执行结果
我们可以通过EndInvoke获得委托标的函数的返回值:
IAsyncResult ia = d.BeginInvoke(1073676287, null, null);
Console.WriteLine("I am doing something else.");
var ret = d.EndInvoke(ia);
Console.WriteLine("Calculation finished. Number is prime number : {0}", ret == true ? "Yes" : "No");
Console.ReadKey();
这解决了第一个问题和第三个问题。现在你再运行程序,程序会阻塞在EndInvoke,你手贱敲了任意一个键,程序也不会结束。另外,我们还获得了异步委托的结果,即该大数是质数。
但这个解决方法又衍生出了一个新的问题:即程序会阻塞在EndInvoke,如果这是一个GUI程序,主线程将会卡死,给用户带来不好的体验。如何解决这个问题?
通过回调函数获得异步委托的执行结果
回调函数的用处是当委托完成时,可以主动通知主线程自己已经完成。我们可以在BeginInvoke中定义回调函数,这将会在委托完成时自动执行。
回调函数的类型是AsyncCallback,其也是一个委托,它的签名:传入参数必须是IAsyncResult,而且没有返回值。所以我们的回调函数必须长成这样子:
public static void IsPrimeNumberCallback(IAsyncResult iar)
{
}
在主函数中加入回调函数:
AsyncCallback acb = new AsyncCallback(IsPrimeNumberCallback);
d.BeginInvoke(1073676287, acb, null);
IAsyncResult中并不包括委托的返回值。利用AsyncCallback可以被转换成AsyncResult类型的特点,我们可以利用AsyncResult中的AsyncDelegate“克隆”一个当前正在运行的委托,然后调用克隆委托的EndInvoke。因为这时委托已经执行完了所以EndInvoke不会阻塞:
public static void IsPrimeNumberCallback(IAsyncResult iar)
{
AsyncResult ar = (AsyncResult) iar;
var anotherDelegate = (IsPrimeNumberDelegate) iar.AsyncDelegate;
var ret = anotherDelegate.EndInvoke(iar);
Console.WriteLine("Calculation finished, Number is prime number : {0}", ret == true ? "Yes" : "No");
}
看到这里读者大概要感慨了,使用委托异步调用获得结果怎么这么复杂。确实是比较复杂,所以之后微软就在后续版本的C#中加入了任务这个工具,它大大简化了异步调用的编写方式。
总结
使用委托的异步编程模型(APM):
- 通过建立一个委托和使用BeginInvoke调用委托来实现异步,通过EndInvoke来获得结果,但要注意的是,EndInvoke会令主线程进入阻塞状态,卡死主线程,所以我们通常使用回调函数
- BeginInvoke方法拥有委托全部的输入,以及额外的两个输入
- 第一个输入为委托的回调函数,它是AsyncCallback类型,这个类型是一个委托,其输入必须是IAsyncResult类型,且没有返回值,如果需要获得返回值,需要在回调函数中,再次呼叫EndInvoke,并传入IAsyncResult
- 委托的回调函数在次线程任务结束时自动执行,并替代EndInvoke
- 第二个输入为object类型,允许你为异步线程传入自定义数据
- 因为使用委托的异步调用本质上也是通过线程来实现异步编程的,所以也可以使用同Threading相同的取消方法,但这实在是太过麻烦(你需要手写一个CancellationToken,这部分到说到线程的时候再说)
- 关于进度条的问题,要等到更高级的BackgroundWorker来解决
- 我们看到获取异步结果这一步还是比较麻烦,所以在任务和BackgroundWorker等大杀器出现之后,这个模型就基本不会使用了
多路广播
委托的本质是一个密封类。这个类继承自System.MultiDelegate,其再继承自System.Delegate。System.MulticastDelegate类中有一个重要字段_invocationList,它令委托可以挂接多于一个函数(即一个函数List)。它维护一个Invocation List(委托链)。你可以为这个链自由的添加或删除Handler函数。一个委托链可以没有函数。
由于委托可以代表一类函数,你可以随心所欲的为委托链绑定合法的函数。此时如果执行委托,将会顺序的执行委托链上所有的函数。如果某个函数出现了异常,则其后所有的函数都不会执行。
如果你的委托的委托链含有很多委托的话,你只会收到最后一个含有返回值的委托的返回值。假如你的委托是有输出值的,而且你想得到委托链上所有方法的输出值,你只能通过GetInvocationList方法得到委托链上的所有方法,然后一一执行。
委托的本质
本节大部分都是概念,如果你正在准备面试,而且已经没有多少时间了,可以考虑将它们背下来。
-
委托的本质是一个密封类。这个类继承自System.MultiDelegate,其再继承自System.Delegate。这个密封类包括三个核心函数,Invoke方法赋予其同步访问的能力,BeginInvoke,EndInvoke赋予其异步访问的能力。例如public delegate int ADelegate(out z,int x,int y)的三个核心函数:
1.int Invoke (out z,int x,int y)
2.IAsyncResult BeginInvoke (out z,int x,int y,AsyncCallback cb,object ob)
3.int EndInvoke (out z,IAsyncResult result)
4.Invoke方法的参数和返回值同委托本身相同,BeginInvoke的返回值总是IAsyncResult,输入则除了委托本身的输入之外还包括了AsyncCallback(回调函数)和一个object。EndInvoke的输入总是IAsyncResult,加上委托中的out和ref(如果有的话)类型的输入,输出类型则是委托的输出类型。 -
在事件中,委托是事件的发起者sender将EventArgs传递给处理者的管道。所以委托是一个密封类,没有继承的意义。
-
委托可以看成是函数指针,它接受与其签名相同的任何函数。委托允许你把方法作为参数。
-
相比C的函数指针,C#的委托是类型安全的,可以方便的获得回调函数的返回值,并且可以通过委托链支持多路广播。
-
EventHandler委托类型是.NET自带的一个委托。其不返回任何值,输入为object类型的sender和EventArgs类型的e。如果你想返回自定义的数据,你必须继承EventArgs类型。这个委托十分适合处理不需要返回值的事件,例如点击按钮事件。
-
System.MulticastDelegate类中有一个重要字段_invocationList,它令委托可以挂接多于一个函数(即一个函数List)。它维护一个Invocation List(委托链)。你可以为这个链自由的添加或删除Handler函数。一个委托链可以没有函数。添加或删除实质上是调用了Delegate.Combine / Delegate.Remove。
-
当你为一个没有任何函数的委托链删除方法时,不会发生异常,仅仅是没有产生任何效果。
-
假设委托可以返回值,那么如果你的委托的委托链含有很多委托的话,你只会收到最后一个委托的返回值。
-
如果在委托链中的某个操作出现了异常,则其后任何的操作都不会执行。如果你想要让所有委托挂接的函数至少执行一次,你需要使用GetInvocationList方法,从委托链中获得方法,然后手动执行他们。
泛型委托
泛型委托Action和Func是两个委托,Action<T>接受一个T类型的输入,没有输出。Func则有一个输出,16个重载分别对应1-16个T类型的输入(这使得它更像数学中函数的概念,故名Func)。Func委托的最后一个参数是返回值的类型,前面的参数都是输入值的类型。
在它们出现之后,你就不需要使用delegate关键字声明委托了(即你可以忘记它了),你可以使用泛型委托代替之。
static void Main(string[] args)
{
Action<int, int> a = new Action<int, int>(add);
a(1, 2);
//Func委托的最后一个参数是返回值的类型
Func<int, int, int> b = new Func<int, int, int>(add2);
Console.WriteLine(b(1, 2));
Console.ReadLine();
}
//这个EventHandler不返回值
public static void add(int a, int b)
{
Console.WriteLine(a + b);
}
//这个EventHandler返回一个整数
public static int add2(int a, int b)
{
return a+b;
}
我们可以看到使用Action对代码的简化。我们不用再自定义一个委托,并为其取名了。这两个泛型委托构成了LINQ的基石之一。
image
我们看一个LINQ的例子:Where方法。
image
通过阅读VS的解释,我们可以获得以下信息:
1.Where是IEnumerable<T>的一个扩展方法
2.这个方法的输入是一个Func<T,bool>,形如Func<T,bool>的泛型委托又有别名Predicate,因其是返回一个布尔型的输出,故有判断之意。
泛型委托使用一例
下面这个问题是某著名公司的一个面试题目。其主要的问题就是,如何对两个对象比较大小,这里面的对象可以是任意的东西。这个题目主要考察的是如何使用泛型和委托结合,实现代码复用的目的。
假设我们有若干个表示形状的结构体,我们要比较它们的大小。
public struct Rectangle
{
public double Length { get; set; }
public double Width { get; set; }
//By calling this() to initialize all valuetype members
public Rectangle(double l, double w) : this()
{
Length = l;
Width = w;
}
}
public struct Circle
{
public double Radius { get; set; }
public Circle(double r) : this()
{
Radius = r;
}
}
我们规定谁面积大就算谁大,此时,因为结构体不能比较大小,只能比较是否相等,我们就需要自己制定一个规则。对不同的形状,求面积的公式也不一样:
public static int CompareRectangle(Rectangle r1, Rectangle r2)
{
double r1Area = r1.Length*r1.Width;
double r2Area = r2.Length*r2.Width;
if (r1Area > r2Area) return 1;
if (r1Area < r2Area) return -1;
return 0;
}
public static int CompareCircle(Circle c1, Circle c2)
{
if (c1.Radius > c2.Radius) return 1;
if (c1.Radius < c2.Radius) return -1;
return 0;
}
当然,在比较大小的时候,可以直接调用这些函数。但如果这么做,你将再次陷入“委托的作用-将方法作为方法的参数”一节中的switch泥潭。注意到这些函数的签名都相同,我们现在已经熟悉委托了,当然就可以用委托来简化代码。
我们可以把规则看作一个函数,其输入为两个同类型的对象,输出一个整数,当地一个对象较大时输出1,相等输出0,第二个对象较大输出-1。那么,这个规则函数的签名应当为:
Func<T, T, int>
它可以变身为任意类型的比较函数。我们在外部再包装一下,将这个规则传入进去。那么这个外部包装函数的签名应当为:
public static void Compare<T>(T o1, T o2, Func<T, T, int> rule)
{
}
当然这里的返回值也可以是int。由于是演示的缘故,我就简单的打印一些信息:
public static void Compare<T>(T o1, T o2, Func<T, T, int> rule)
{
var ret = rule.Invoke(o1, o2);
if (ret == 1) Console.WriteLine("First object is bigger.");
if (ret == -1) Console.WriteLine("Second object is bigger.");
if (ret == 0) Console.WriteLine("They are the same.");
}
主程序调用:
static void Main(string[] args)
{
var r1 = new Rectangle(1, 6);
var r2 = new Rectangle(2, 4);
Compare(r1, r2, CompareRectangle);
var c1 = new Circle(3);
var c2 = new Circle(2);
Compare(c1, c2, CompareCircle);
Console.ReadKey();
}
我们可以看到,对不同类型都有着统一的比较大小的方式。可以参考:http://www.cnblogs.com/onepiece_wang/archive/2012/11/28/2793530.html
什么是事件?
简单的看,事件的定义就是通知(给订阅者)。事件由三部分组成:事件的触发者(sender),事件的处理者(Event Handler,一个和委托类型相同的函数)和事件的数据传送通道delegate。delegate负责传输事件的触发者对象sender和自定义的数据EventArgs。要实现事件,必须实现中间的委托(的标的函数),并为事件提供一个处理者。处理者函数的签名和委托必须相同。
所以,事件必须基于一个委托。
使用事件的步骤:
- 声明委托(指出当事件发生时要执行的方法的方法类型)。委托要传递的数据可能是自定义类型的
- 声明一个事件处理者(一个方法),其签名和委托签名相同
- 声明一个事件(这需要第一步的委托)
- 为事件+=事件处理者(委托对象即是订阅者/消费者)
-
在事件符合条件之后,调用事件
image
委托和事件有何关系?
委托是事件传输消息的管道。事件必须基于一个委托。下图中小女孩是事件的发起者(拥有者),她通过委托(即图上的“电话线”)传递若干消息给她的爸爸(事件的处理者/订阅者)。和委托一样,事件可以有多个订阅者,这也是多路广播的一个体现。
可以借助事件实现观察者模式。观察者模式刻画了一个一对多的依赖关系,其中,当一对多中的“一”发生变化时,“多”的那头会收到信息。
image
经典例子:this.button1.Click += new System.EventHandler(this.StartButton_Click);
- Click是一个事件,它的定义为public event EventHandler Click,它基于的委托类型是EventHandler类型。
- Click事件挂接了一个新的委托,委托传递object类型的sender和EventArgs类型的e给事件的处理者StartButton_Click。StartButton_Click是一个和EventHandler委托类型签名相同的函数。
- EventHandler是.NET自带的一个委托。其不返回任何值,输入为object类型的sender和EventArgs类型的e。EventArgs类型本身没有任何成员,如果你想传递自定义的数据,你必须继承EventArgs类型。
使用事件
使用事件需要至少一个订阅者。订阅者需要一个事件处理函数,该处理函数通常要具备两个参数:输入为object类型的sender和一个继承了EventArgs类型的e(有时候第一个参数是不必要的)。你需要继承EventArgs类型来传递自定义数据。
public class Subscriber
{
public string Name { get; set; }
public Subscriber(string name)
{
Name = name;
}
public void ReceiveMessage(object sender, MessageArgs e)
{
Console.WriteLine("I am {0} and I know {1}!", Name, e.Message);
}
}
public class MessageArgs : EventArgs
{
public string Message { get; set; }
}
当有订阅者订阅事件之后,Invoke事件会顺序激发所有订阅者的事件处理函数。其激发顺序视订阅顺序而定。
首先要定义委托和事件。委托的命名惯例是以Handler结尾:
//1. Base delegate
public delegate void SendMessageHandler(object sender, MessageArgs e);
//2. Event based on the delegate
public static event SendMessageHandler SendMessage;
事件的执行演示:
static void Main(string[] args)
{
//Subscribers
Subscriber s1 = new Subscriber("Adam");
Subscriber s2 = new Subscriber("Betty");
Subscriber s3 = new Subscriber("Clara");
//Subscribe
SendMessage += s1.ReceiveMessage;
SendMessage += s2.ReceiveMessage;
SendMessage += s3.ReceiveMessage;
//Simulate a message transfer
Console.WriteLine("Simulate initializing...");
Thread.Sleep(new Random(1).Next(0, 1000));
var data = new MessageArgs {Message = "Class begins"};
if (SendMessage != null) SendMessage(null, data);
//Unsubscribe
SendMessage -= s1.ReceiveMessage;
Thread.Sleep(new Random(1).Next(0, 1000));
data.Message = "Calling from main function";
if (SendMessage != null) SendMessage(null, data);
Console.WriteLine("Class is over!");
Console.ReadKey();
}
事件的本质
-
如果你查看事件属性的对应IL,你会发现它实质上是一个私有的字段,包含两个方法add_[事件名]和remove_[事件名]。
-
事件是私有的,它和委托的关系类似属性和字段的关系。它封装了委托,用户只能通过add_[事件名]和remove_[事件名](也就是+=和-=)进行访问。
-
如果订阅事件的多个订阅者在事件触发时,有一个订阅者的事件处理函数引发了异常,则它将会影响后面的订阅者,后面的订阅者的事件处理函数不会运行。
-
如果你希望事件只能被一个客户订阅,则你可以将事件本身私有,然后暴露一个注册的方法。在注册时,直接使用等号而不是+=就可以了,后来的客户会将前面的客户覆盖掉。
委托的协变和逆变
协变和逆变实际上是属于泛型的语法特性,由于有泛型委托的存在,故委托也具备这个特性。我将在讨论泛型的时候再深入讨论这个特性。
经典文章,参考资料
有关委托和事件的文章多如牛毛。熟悉了委托和事件,将会对你理解linq有很大的帮助。
1. 张子阳的经典例子: http://www.cnblogs.com/JimmyZhang/archive/2007/09/23/903360.html
可以自行编写一个热水器的例子,测试自己是否掌握了基本的事件用法。
http://www.cnblogs.com/JimmyZhang/archive/2008/08/22/1274342.html 这是续篇。
2. 委托本质论,不过说的比较简单。这个水平也基本可以应付面试了(很少有人问这么深入),更难更全面的解释可以参考clr via c#:http://www.cnblogs.com/zhili/archive/2012/10/25/DeepDelegate.html
3. 一个生动的事件例子:http://www.cnblogs.com/yinqixin/p/5056307.html
4. 常见委托面试题目:http://www.cnblogs.com/jackson0714/p/5111347.html