• C# in Depth-委托


    2.1 委托

    委托在某种程度上提供了间接的方法。换言之,不需要直接指定一个要执行的行为,而是将这个行为用某种方式“包含”在一个对象中。

    这个对象可以像其他任何对象那样使用。在该对象中,可以执行封装的操作。

    可以选择将委托类型看做只定义了一个方法的接口,将委托的实例看做实现了那个接口的一个对象。

    让我们以遗嘱为例。遗嘱由一系列指令组成,比如:“付账单,捐善款,其余财产留给猫。”

    遗嘱一般是在某人去世之前写好,然后把它放到一个安全的地方。去世后,(希望)律师会执行这些指令。

    C#中的委托和现实世界的遗嘱一样,也是要在恰当的时间执行一系列操作。

    如果代码想要执行操作,但不知道操作细节,一般可以使用委托。

    例如, Thread 类之所以知道要在一个新线程里运行什么,唯一的原因就是在启动新线程时,向它提供了一个 ThreadStart 或 ParameterizedThreadStart 委托实例。


    2.1.1 简单委托的构成

    为了让委托做某事,必须满足4个条件:

    ① 声明委托类型

    ②必须有一个方法包含了要执行的代码

    ③必须创建一个委托实例

    ④必须调用(invoke)委托实例

    下面依次讨论上述每一步。


    1. 声明委托类型

    委托类型实际上只是参数类型的一个列表以及一个返回类型。它规定了类型的实例能表示的操作。

    例如,以如下方式声明一个委托类型:

    delegate void StringProcessor(string input);

    上述代码指出,如果要创建 StringProcessor 的一个实例,需要只带一个参数(string)的方法,而且这个方法要有一个 void 返回类型(什么都不返回)。

    这里的重点在于, StringProcessor 其实是一个从 System.MulticastDelegate 派生的类型,后者又派生自 System.Delegate 。

    它有方法,可以创建它的实例,并将引用传递给实例,所有这些都没有问题。

    虽然它有一些自己的“特性”,但假如你对特定情况下发生的事情感到困惑,那么首先想一想使用“普通”的引用类型时发生的事情。

    讨论委托的下一个基本元素时,会用到 StringProcessor 委托类型。

    混乱的根源:容易产生歧义的“委托”

    委托经常被人误解,这是由于大家喜欢用委托这个词来描述委托类型和委托实例。

    这两者的区别其实就是任何一个类型和该类型的实例的区别。

    例如, string 类型本身和一组特定的字符肯定不同。委托类型和委托实例这两个词会贯穿本章始终,从而让你明白我具体说的是什么。


    2. 为委托实例的操作找到一个恰当的方法

    我们的下一个基本元素是找到(或者写)一个方法,它能做我们想做的事情,同时具有和委托类型相同的签名。

    基本的思路是,要确保在调用(invoke)一个委托实例时,使用的参数完全匹配,而且能以我们希望的方式(就像普通的方法调用)使用返回值(如果有的话)。

    看看以下 StringProcessor 实例的5个备选方法签名:

    void PrintString(string x)
    void PrintInteger(int x)
    void PrintTwoStrings(string x,string y)
    int GetStringLength(string x)
    void PrintObject(object x)

    第1个方法完全符合要求,所以可以用它创建一个委托实例。

    第2个方法虽然也有一个参数,但不是 string 类型,所以不兼容 StringProcessor 。

    第3个方法第1个参数的类型匹配,但参数数量不匹配,所以也不兼容。

    第4个方法有正确的参数列表,但返回类型不是 void 。(如果委托类型有返回类型,方法的返回类型也必须与之匹配。)

    第5个方法比较有趣,任何时候调用一个 StringProcessor 实例,都可以调用具有相同的参数的 PrintObject 方法,这是由于 string 是从 object 派生的。

    把这个方法作为 StringProcessor 的一个实例来使用是合情合理的,但C# 1要求委托必须具有完全相同的参数类型。C# 2改善了这个状况——详见第5章。

    在某些方面,第4个方法也是相似的,因为总是可以忽略不需要的返回值。

    然而, void 和非 void 返回类型目前一直被认为是不兼容的。

    部分原因是因为系统的其他方面(特别是JIT)需要知道,在执行方法时返回值是否会留在栈上。

    假定有一个针对兼容的签名( PrintString )的方法体。接着,讨论下一个基本元素——委托实例本身。


    3. 创建委托实例

    有了一个委托类型和一个有正确签名的方法后,接着可以创建委托类型的一个实例,指定在调用委托实例时就执行该方法。作者将该方法称为委托实例的操作。

    至于具体用什么形式的表达式来创建委托实例,取决于操作使用实例方法还是静态方法。

    假定 PrintString 是 StaticMethods 类型中的一个静态方法,在 InstanceMethods 类型中是一个实例方法。

    下面是创建一个 StringProcessor 实例的两个例子:

    //静态方法
    StringProcessor pro1,proc2;
    proc1 = new StringProcessor(StaticMethods.PrintString);
    //实例方法
    InstanceMethods instance = new InstanceMethods();
    proc2 = new StringProcessor(instance.PrintString);

    如果操作是静态方法,指定类型名称就可以了。如果操作是实例方法,就需要先创建类型(或者它的派生类型)的一个实例。这和平时调用方法是一样的。

    这个对象称为操作的目标。调用委托实例时,就会为这个对象调用(invoke)方法。

    如果操作在同一个类中(这种情况经常发生,尤其是在UI代码中写事件处理程序时),那么两种限定方式都不需要——实例方法隐式将 this 引用作为前缀。

    同样,这些规则和你直接调用方法时没什么两样。单纯创建一个委托实例却不在某一时刻调用它是没有什么意义的。看看最后一步——调用。

    最终的垃圾(或者不是,视情况而定)

    必须注意,假如委托实例本身不能被回收,委托实例会阻止它的目标被作为垃圾回收。这可能造成明显的内存泄漏(leak),尤其是假如某“短命”对象调用了一个“长命”对象中的事件,并用它自身作为目标。“长命”对象间接容纳了对“短命”对象的一个引用,延长了“短命”对象的寿命。


    4. 调用委托实例

    这是很容易的一件事儿,调用一个委托实例的方法就可以了。这个方法本身被称为 Invoke 。

    在委托类型中,这个方法以委托类型的形式出现,并且具有委托类型声明中指定的相同参数列表和返回类型。

    所以,在我们的例子中,有一个像下面这样的方法:

    调用 Invoke 会执行委托实例的操作,向它传递在调用 Invoke 时指定的任何参数。另外,如果返回类型不是 void ,还要返回操作的返回值。

    C#将这个过程变得更简单——如果有一个委托类型的变量,就可以把它视为方法本身。

    观察由不同时间发生的事件构成的一个事件链,很容易就可以理解这一点,如图2-1所示。

    就是这么简单。所有原料都已齐备,接着将CLR预热到200℃,将所有东西都搅拌到一起,看看会发生什么。


    5. 一个完整的例子和一些动机

    //代码清单2-1 以各种简单的方式使用委托
    delegate void StringProcessor(string input);//声明委托类型
    class Person
    {
        string name;
        public Person(string name) { this.name = name; }
        //声明兼容的实例方法
        public void Say(string message)
        {
            Console.WriteLine("{0}说:{1}",name,message);
        }
    }
    class BackGround
    {
        //声明兼容的静态方法
        public static void Note(string note)
        {
            Console.WriteLine("({0})",note);
        }
    }
    class Program
    {
        static void Main()
        {
            //声明人物实例
            Person jon = new Person("铁子");
            Person tom = new Person("杨树");
            //声明委托实例,并添加对应的实例方法和静态方法
            StringProcessor jonsVoice, tomsVoice, background;
            jonsVoice = new StringProcessor(jon.Say);
            tomsVoice = new StringProcessor(tom.Say);
            background = new StringProcessor(BackGround.Note);
            //调用委托实例
            jonsVoice("你瞅啥?");
            tomsVoice.Invoke("瞅你咋地?");
            background("烧烤摊又开始洋溢着欢快的声音了");
            Console.Read();
        }
    }

    首先声明委托类型 ,接着创建两个方法,它们都与委托类型兼容。

    一个是实例方法( Person.Say ),另一个是静态方法( Background.Note ),这样就可以看到在创建委托实例时 ,它们在使用方式上的区别。

    代码清单2-1创建了 Person 类的两个实例,便于观察委托目标所造成的差异。

    jonsVoice 被调用时 它会调用 name 为 Jon 的那个 Person 对象的 Say 方法。

    同样,tomsVoice 被调用时使用的是 name 为 Tom 的对象。

    然后展示了调用委托实例的两种方式,显式调用 Invoke 和使用C#的简化形式。一般情况下只需使用简化形式。以下是代码清单2-1的输出:

    铁子说:你瞅啥?
    杨树说:瞅你咋地?
    (烧烤摊又开始洋溢着欢快的声音了)

    使用委托的意义

    如果仅仅是为了显示上述3行输出,代码清单2-1的代码未免太多了。即使想要使用Person 类和 Background 类,也没有必要使用委托。

    但是我们不能仅仅由于你希望某事发生,就意味着你始终会在正确的时间和地点出现,并亲自使之发生。有时,你需要给出一些指令,将职责委托给别人。

    应该强调的一点是,在软件世界中,没有对象“留遗嘱”这样的事情发生。经常都会发现这种情况:委托实例被调用时,最初创建委托实例的对象仍然是“活蹦乱跳”的。

    相反,委托相当于指定一些代码在特定的时间执行,那时,你也许已经无法(或者不想)更改要执行的代码。

    如果我希望在单击一个按钮后发生某事,但不想对按钮的代码进行修改,我只是希望按钮调用我的某个方法,那个方法能执行恰当的操作。

    委托的实质是间接完成某种操作,事实上,许多面向对象编程技术都在做同样的事情。我们看到,这增大了复杂性,但同时也增加了灵活性。

    现在已经对简单委托有了更多的理解,接着看看如何将委托合并到一起,以便成批地执行操作,而不是只执行一个。


    2.1.2 合并和删除委托

    委托实例实际有一个操作列表与之关联。这称为委托实例的调用列表(invocation list)。

    System.Delegate 类型的静态方法 Combine 和 Remove 负责创建新的委托实例。

    其中,Combine 负责将两个委托实例的调用列表连接到一起,而 Remove 负责从一个委托实例中删除另一个实例的调用列表。

    委托是不易变的

    创建了委托实例后,有关它的一切就不能改变。这样一来,就可以安全地传递委托实例的引用,并把它们与其他委托实例合并,同时不必担心一致性、线程安全性或者是否有其他人试图更改它。

    在这一点上,委托实例和 string 是一样的。string的 实 例 也 是 不 易 变 的 。 之 所 以 提 到 string , 是 因 为 Delegate.Combine 和String.Concat 很像——都是合并现有的实例来形成一个新实例,同时根本不更改原始对象。对于委托实例,原始调用列表被连接到一起。注意,如果试图将 null 和委托实例合并到一起, null 将被视为带有空调用列表的一个委托。

    使用操作符代替两个方法

    很少在C#代码中看到对 Delegate.Combine 的显式调用,一般都是使用+和+=操作符。

    图2-2展示了转换过程,其中 x 和 y 都是相同(或兼容)委托类型的变量。所有转换都由C#编译器完成。

     

    可以看出,这是一个相当简单的转换过程,但它使代码变得整洁多了。

    Delegate.Remove 方法从一个实例中删除另一个实例的调用列表。C#使用-和-=运算符简写形式的方法非常简单,一看便知。

    Delegate.Remove(source, value) 将创建一个新的委托实例,其调用列表来自 source , value 中的列表则被删除。如果结果有一个空的调用列表,就返回 null 。

    委托返回非void操作中最后一个操作的返回值

    调用委托实例时,它的所有操作都顺序执行。如果委托的签名具有一个非 void 的返回类型,则 Invoke 的返回值是最后一个操作的返回值。

    很少有非 void 的委托实例在它的调用列表中指定多个操作,因为这意味着其他所有操作的返回值永远都看不见。除非每次调用代码使用Delegate.GetInvocationList 获取操作列表时,都显式调用某个委托。

    任何操作异常立都会即终止委托

    如果调用列表中的任何操作抛出一个异常,都会阻止执行后续的操作。例如,假定调用一个委托实例,它的操作列表是 [a, b, c] ,但操作 b 抛出了一个异常,这个异常会立即“传播”,操作 c 不会执行。

    进行事件处理时,委托实例的合并与删除会特别有用。既然我们已经理解了合并与删除涉及的操作,就很容易理解事件。


    2.1.3 对事件的简单讨论

    事件的基本思想是让代码在发生某事时作出响应,如在正确单击一个按钮后保存一个文件。在这个例子中,事件是“单击按钮”,操作是“保存文件”。

    开发者经常将事件和委托实例,或者将事件和委托类型的字段混为一谈。

    但它们之间的差异十分大:事件不是委托类型的字段。之所以产生混淆,原因和以前相同,因为C#提供了一种简写方式,允许使用字段风格的事件(field-like event)。

    先从C#编译器的角度看看事件到底由什么组成。


    将事件看成属性

    将事件看做类似于属性(property)的东西是很有好处的。

    声明为具有一种特定的类型

    两者都声明为具有一种特定的类型。对于事件来说,必须是一个委托类型。

    能获取或设置字段/方法

    使用属性时,实际是在调用方法,也就是取值方法和赋值方法。实现属性时,可以在那些方法中做任何事情。

    同样,在订阅或取消订阅一个事件时,看起来就像是在通过 += 和 -= 运算符使用委托类型的字段。

    但和属性的情况一样,这个过程实际是在调用方法。对于一个纯粹的事件,你所能做的事情就是订阅或者取消订阅。

    最终是由事件方法来做真正有用的事情,如找到你试图添加和删除的事件处理程序,并使它们在类中的其他地方可用。

    使用封装实现发布/订阅

    事件存在的首要理由和属性差不多,它们都添加了一个封装层以实现发布/订阅模式。

    通常,我们不希望其他代码能直接设置字段值。最起码也要先由所有者(owner)对新值进行证。

    同样,我们通常不希望类外部的代码随意更改(或调用)一个事件的处理程序。

    类能通过添加方法的方式来提供额外的访问。例如,可以重置事件的处理程序列表,或者引发事件。但是如果只对外揭示事件本身,类外部的代码就只能添加和删除事件处理程序。

    字段风格的事件使所有这些的实现变得更易阅读,只需一个声明就可以了。

    编译器会将声明转换成一个具有默认 add / remove 实现的事件和一个私有委托类型的字段。类内的代码能看见字段;类外的代码只能看见事件。

    这样一来,表面上似乎能调用一个事件,但为了调用事件处理程序,实际做的事情是调用存储在字段中的委托实例。

  • 相关阅读:
    Asp.NetCore3.1 WebApi 获取配置json文件中的数据
    Dapper同时操作任意多张表的实现
    将视图批量新增到PowerDesigner中并以model图表的形式展示
    .NetCore3.1获取文件并重新命名以及大批量更新及写入数据
    .NetCore 简单的使用中间件
    比较复杂的SQL转Linq
    Asp.NetCore3.1版本的CodeFirst与经典的三层架构与AutoFac批量注入
    Git与GitLab的分支合并等简单的测试操作
    Winform的控件以及DataGridView的一般使用
    在Linux系统中运行并简单的测试RabbitMq容器
  • 原文地址:https://www.cnblogs.com/errornull/p/10019653.html
Copyright © 2020-2023  润新知