美文网首页
(转).NET面试题系列[7] - 委托与事件

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

作者: aslbutton | 来源:发表于2019-02-25 14:37 被阅读0次

    委托和事件

    委托在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

    相关文章

      网友评论

          本文标题:(转).NET面试题系列[7] - 委托与事件

          本文链接:https://www.haomeiwen.com/subject/ffcgyqtx.html