您的位置:首页 > 职场人生

.NET面试题系列[7] - 委托与事件

2016-08-09 16:54 459 查看

委托和事件

委托在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) ar.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.DelegateSystem.MulticastDelegate类中有一个重要字段_invocationList,它令委托可以挂接多于一个函数(即一个函数List)。它维护一个Invocation List(委托链)。你可以为这个链自由的添加或删除Handler函数。一个委托链可以没有函数。

由于委托可以代表一类函数,你可以随心所欲的为委托链绑定合法的函数。此时如果执行委托,将会顺序的执行委托链上所有的函数。如果某个函数出现了异常,则其后所有的函数都不会执行。

如果你的委托的委托链含有很多委托的话,你只会收到最后一个含有返回值的委托的返回值。假如你的委托是有输出值的,而且你想得到委托链上所有方法的输出值,你只能通过GetInvocationList方法得到委托链上的所有方法,然后一一执行。

委托的本质

本节大部分都是概念,如果你正在准备面试,而且已经没有多少时间了,可以考虑将它们背下来。

委托的本质是一个密封类。这个类继承自System.MultiDelegate,其再继承自System.Delegate[b][b]。[b]这个密封类包括三个核心函数,Invoke方法赋予其同步访问的能力,BeginInvoke,EndInvoke赋予其异步访问的能力。[/b][/b][/b]例如public delegate int ADelegate(out z,int x,int y)的三个核心函数:

int Invoke (out z,int x,int y)

IAsyncResult BeginInvoke (out z,int x,int y,AsyncCallback cb,object ob)

int EndInvoke (out z,IAsyncResult result)

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的基石之一。



我们看一个LINQ的例子:Where方法。



通过阅读VS的解释,我们可以获得以下信息:

Where是IEnumerable<T>的一个扩展方法

这个方法的输入是一个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();
}


我们可以看到,对不同类型都有着统一的比较大小的方式。可以参考:/article/7002047.html 

什么是事件?

简单的看,事件的定义就是通知(给订阅者)。事件由三部分组成:事件的触发者(sender),事件的处理者(Event Handler,一个和委托类型相同的函数)和事件的数据传送通道delegatedelegate负责传输事件的触发者对象sender和自定义的数据EventArgs。要实现事件,必须实现中间的委托(的标的函数),并为事件提供一个处理者。处理者函数的签名和委托必须相同。

所以,事件必须基于一个委托。

使用事件的步骤:

声明委托(指出当事件发生时要执行的方法的方法类型)。委托要传递的数据可能是自定义类型的

声明一个事件处理者(一个方法),其签名和委托签名相同

声明一个事件(这需要第一步的委托)

为事件+=事件处理者(委托对象即是订阅者/消费者)

在事件符合条件之后,调用事件



委托和事件有何关系?

委托是事件传输消息的管道。事件必须基于一个委托。下图中小女孩是事件的发起者(拥有者),她通过委托(即图上的“电话线”)传递若干消息给她的爸爸(事件的处理者/订阅者)。和委托一样,事件可以有多个订阅者,这也是多路广播的一个体现。

可以借助事件实现观察者模式。观察者模式刻画了一个一对多的依赖关系,其中,当一对多中的“一”发生变化时,“多”的那头会收到信息。



经典例子: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. 张子阳的经典例子: /article/4671509.html

可以自行编写一个热水器的例子,测试自己是否掌握了基本的事件用法。

/article/4671539.html 这是续篇。

2. 委托本质论,不过说的比较简单。这个水平也基本可以应付面试了(很少有人问这么深入),更难更全面的解释可以参考clr via c#:/article/4747007.html

3. 一个生动的事件例子:/article/5364924.html

4. 常见委托面试题目:/article/5244167.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: