.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) ar.AsyncDelegate;
            var ret = anotherDelegate.EndInvoke(iar);
            Console.WriteLine("Calculation finished, Number is prime number : {0}", ret == true ? "Yes" : "No");
        }

看到这里读者大概要感慨了,使用委托异步调用获得结果怎么这么复杂。确实是比较复杂,所以之后微软就在后续版本的C#中加入了任务这个工具,它大大简化了异步调用的编写方式。

总结

使用委托的异步编程模型(APM):

  1. 通过建立一个委托和使用BeginInvoke调用委托来实现异步,通过EndInvoke来获得结果,但要注意的是,EndInvoke会令主线程进入阻塞状态,卡死主线程,所以我们通常使用回调函数
  2. BeginInvoke方法拥有委托全部的输入,以及额外的两个输入
  3. 第一个输入为委托的回调函数,它是AsyncCallback类型,这个类型是一个委托,其输入必须是IAsyncResult类型,且没有返回值,如果需要获得返回值,需要在回调函数中,再次呼叫EndInvoke,并传入IAsyncResult
  4. 委托的回调函数在次线程任务结束时自动执行,并替代EndInvoke
  5. 第二个输入为object类型,允许你为异步线程传入自定义数据
  6. 因为使用委托的异步调用本质上也是通过线程来实现异步编程的,所以也可以使用同Threading相同的取消方法,但这实在是太过麻烦(你需要手写一个CancellationToken,这部分到说到线程的时候再说)
  7. 关于进度条的问题,要等到更高级的BackgroundWorker来解决
  8. 我们看到获取异步结果这一步还是比较麻烦,所以在任务和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)的三个核心函数:
    • 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的解释,我们可以获得以下信息:

  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。要实现事件,必须实现中间的委托(的标的函数),并为事件提供一个处理者。处理者函数的签名和委托必须相同。

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

使用事件的步骤:

  1. 声明委托(指出当事件发生时要执行的方法的方法类型)。委托要传递的数据可能是自定义类型的
  2. 声明一个事件处理者(一个方法),其签名和委托签名相同
  3. 声明一个事件(这需要第一步的委托)
  4. 为事件+=事件处理者(委托对象即是订阅者/消费者)
  5. 在事件符合条件之后,调用事件

委托和事件有何关系?

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

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

经典例子: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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏写代码的海盗

scala与java之间的那些事

  scala与java之间的关系,我认为可以用一句话来开头:scala来源于java,但又高于java。   scala的设计者Martin Odersky就...

37450
来自专栏软件开发

C语言 第六章 多重循环

一、概要 在c语言中,if,switch,for,while,do-while可以相互间多次嵌套。 if(){   for()   {     for()   ...

25250
来自专栏Code_iOS

数据结构:栈与队列

工程代码 Github: Data_Structures_C_Implemention -- Stack & Queue

14130
来自专栏极客猴

内容提取神器 beautiful Soup 的用法

上篇文章只是简单讲述正则表达式如何读懂以及 re 常见的函数的用法。我们可能读懂别人的正则表达式,但是要自己写起正则表达式的话,可能会陷入如何写的困境。正则表达...

10630
来自专栏向治洪

Swift 4.0 新特性

WWDC 2017 带来了很多惊喜,在这次大会上,Swift 4 也伴随着 Xcode 9 测试版来到了我们的面前,虽然正式版要8月底9月初才会公布,但很多强大...

23190
来自专栏Create Sun

利用委托与Lambada创建和调用webapi接口

前言   现在项目中用的是webapi,其中有以下问题:       1.接口随着开发的增多逐渐增加相当庞大。     2.接口调用时不好管理。   以上是主要...

37290
来自专栏大内老A

通过实例模拟ASP.NET MVC的Model绑定机制:简单类型+复杂类型

总的来说,针对目标Action方法参数的Model绑定完全由组件ModelBinder来实现,在默认情况下使用的ModelBinder类型为DefaultMod...

26980
来自专栏林德熙的博客

C# 很少人知道的科技

本文来告诉大家在C#很少有人会发现的科技。即使是工作了好多年的老司机也不一定会知道,如果觉得我在骗你,那么请看看下面。

13620
来自专栏水击三千

ARCGIS接口详细说明

ArcGIS接口详细说明 目录 ArcGIS接口详细说明... 1 1.      IField接口(esriGeoDatabase)... 2 2.     ...

43960
来自专栏恰童鞋骚年

数据结构基础温故-6.查找(下):哈希表

哈希(散列)技术既是一种存储方法,也是一种查找方法。然而它与线性表、树、图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图示表示出来,...

14910

扫码关注云+社区

领取腾讯云代金券