zoukankan      html  css  js  c++  java
  • 托付

    我们先无论这个标题怎样的绕口。也无论托付到底是个什么东西,来看以下这两个最简单的方法,它们只是是在屏幕上输出一句问候的话语:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void GreetPeople(string name)
    {
        // 做某些额外的事情。比方初始化之类。此处略
        EnglishGreeting(name);
    }
    public void EnglishGreeting(string name)
    {
        Console.WriteLine("Morning, " + name);
    }
    暂且无论这两个方法有没有什么实际意义。GreetPeople用于向某人问好。当我们传递代表某人姓名的name參数,比方说“Jimmy”,进去的时候。在这种方法中,将调用EnglishGreeting方法,再次传递name參数,EnglishGreeting则用于向屏幕输出 “Morning, Jimmy”。
    如果这个程序须要进行全球化。哎呀,不好了,我是中国人,我不明确“Morning”是什么意思,怎么办呢?好吧,我们再加个中文版的问候方法:
    1
    2
    3
    4
    public void ChineseGreeting(string name)
    {
        Console.WriteLine("早上好, " + name);
    }
    这时候。GreetPeople也须要改一改了,不然怎样推断究竟用哪个版本号的Greeting问候方法合适呢?在进行这个之前,我们最好再定义一个枚举作为推断的根据:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public enum Language
    {
        English, Chinese
    }
    public void GreetPeople(string name, Language lang)
    {
        //做某些额外的事情,比方初始化之类,此处略
        switch(lang)
        {
            case Language.English:
            EnglishGreeting(name);
            break;
            case Language.Chinese:
            ChineseGreeting(name);
            break;
         }
    }
    OK。虽然这样攻克了问题。但我不说大家也非常easy想到,这个解决方式的可扩展性非常差,假设日后我们须要再加入韩文版、日文版,就不得不重复改动枚举和GreetPeople()方法,以适应新的需求。
    在考虑新的解决方式之前,我们先看看 GreetPeople的方法签名:
    public void GreetPeople(string name, Language lang)
    我们仅看 string name。在这里,string 是參数类型,name 是參数变量,当我们赋给name字符串“jimmy”时,它就代表“jimmy”这个值;当我们赋给它“张子阳”时。它又代表着“张子阳”这个值。

    然后,我们能够在方法体内对这个name进行其它操作。哎。这简直是废话么,刚学程序就知道了。

    假设你再细致想想,假如GreetPeople()方法能够接受一个參数变量,这个变量能够代表还有一个方法,当我们给这个变量赋值 EnglishGreeting的时候,它代表着 EnglishGreeting() 这种方法;当我们给它赋值ChineseGreeting 的时候,它又代表着ChineseGreeting()方法。

    我们将这个參数变量命名为 MakeGreeting,那么不是能够如同给name赋值时一样。在调用 GreetPeople()方法的时候。给这个MakeGreeting 參数也赋上值么(ChineseGreeting或者EnglishGreeting等)?然后,我们在方法体内。也能够像使用别的參数一样使用MakeGreeting。

    可是,因为MakeGreeting代表着一个方法。它的使用方式应该和它被赋的方法(比方ChineseGreeting)是一样的。比方:

    MakeGreeting(name);
    好了,有了思路了,我们就来改改GreetPeople()方法。那么它应该是这个样子了:
    1
    2
    3
    4
    public void GreetPeople(string name, *** MakeGreeting)
    {
        MakeGreeting(name);
    }
    注意到 *** 。这个位置通常放置的应该是參数的类型,但到眼下为止。我们不过想到应该有个能够代表方法的參数,并按这个思路去改写GreetPeople方法,就出现了一个大问题:这个代表着方法的MakeGreeting參数应该是什么类型的?
    NOTE:这里已不再须要枚举了,由于在给MakeGreeting赋值的时候动态地决定使用哪个方法,是ChineseGreeting还是 EnglishGreeting,而在这个两个方法内部。已经对使用“morning”还是“早上好”作了区分。
    聪明的你应该已经想到了,是托付该出场的时候了。但讲述托付之前,我们再看看MakeGreeting參数所能代表的 ChineseGreeting()和EnglishGreeting()方法的签名:
    1
    2
    public void EnglishGreeting(string name)
    public void ChineseGreeting(string name)
    如同name能够接受String类型的“true”和“1”。但不能接受bool类型的true和int类型的1一样。MakeGreeting的 參数类型定义 应该能够确定 MakeGreeting能够代表的方法种类。再进一步讲,就是MakeGreeting能够代表的方法 的 參数类型和返回类型。

    于是,托付出现了:它定义了MakeGreeting參数所能代表的方法的种类,也就是MakeGreeting參数的类型。
    NOTE:假设上面这句话比較绕口。我把它翻译成这样:string 定义了name參数所能代表的值的种类,也就是name參数的类型。
    本例中托付的定义:
    public delegate void GreetingDelegate(string name);
    能够与上面EnglishGreeting()方法的签名对照一下,除了增加了delegatekeyword以外,其余的是不是全然一样?
    让我们再次修改GreetPeople()方法,例如以下所看到的:
    1
    2
    3
    4
    public void GreetPeople(string name, GreetingDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
    如你所见,托付GreetingDelegate出现的位置与 string同样,string是一个类型。那么GreetingDelegate应该也是一个类型,或者叫类(Class)。

    可是托付的声明方式和类却全然不同,这是怎么一回事?实际上。托付在编译的时候确实会编译成类。由于Delegate是一个类。所以在不论什么能够声明类的地方都能够声明托付。很多其它的内容将在以下讲述。请看看这个范例的完整代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    using System;
    using System.Collections.Generic;
    using System.Text;
    namespace Delegate
    {
        //定义托付。它定义了能够代表的方法的类型
        public delegate void GreetingDelegate(string name);
        class Program
        {
            private static void EnglishGreeting(string name)
            {
                Console.WriteLine("Morning, " + name);
            }
            private static void ChineseGreeting(string name)
            {
                Console.WriteLine("早上好, " + name);
            }
            //注意此方法。它接受一个GreetingDelegate类型的參数。该參数是返回值为空,參数为string类型的方法
            private static void GreetPeople(string name, GreetingDelegate MakeGreeting)
            {
                MakeGreeting(name);
            }
            static void Main(string[] args)
            {
                GreetPeople("Jimmy Zhang", EnglishGreeting);
                GreetPeople("张子阳", ChineseGreeting);
                Console.ReadKey();
            }
        }
    }
    输出例如以下:
    Morning, Jimmy Zhang
    早上好, 张子阳
    我们对托付做一个总结:
    托付是一个类。它定义了方法的类型。使得能够将方法当作还有一个方法的參数来进行传递,这样的将方法动态地赋给參数的做法,能够避免在程序中大量使用If-Else(Switch)语句,同一时候使得程序具有更好的可扩展性。

    托付

    编辑
    看到这里。是不是有那么点如梦初醒的感觉?于是,你是不是在想:在上面的样例中,我不一定要直接在GreetPeople()方法中给 name參数赋值,我能够像这样使用变量:
    1
    2
    3
    4
    5
    6
    7
    8
    static void Main(string[] args) {
    string name1, name2;
    name1 = "Jimmy Zhang";
    name2 = "张子阳";
    GreetPeople(name1, EnglishGreeting);
    GreetPeople(name2, ChineseGreeting);
    Console.ReadKey();
    }
    而既然托付GreetingDelegate 和 类型 string 的地位一样,都是定义了一种參数类型,那么,我是不是也能够这么使用托付?
    1
    2
    3
    4
    5
    6
    7
    8
    static void Main(string[] args) {
    GreetingDelegate delegate1, delegate2;
    delegate1 = EnglishGreeting;
    delegate2 = ChineseGreeting;
    GreetPeople("Jimmy Zhang", delegate1);
    GreetPeople("张子阳", delegate2);
    Console.ReadKey();
    }
    如你所料。这样是没有问题的,程序一如预料的那样输出。这里,我想说的是托付不同于string的一个特性:能够将多个方法赋给同一个托付,或者叫将多个方法绑定到同一个托付。当调用这个托付的时候。将依次调用其所绑定的方法。

    在这个样例中,语法例如以下:

    1
    2
    3
    4
    5
    6
    7
    8
    static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // 先给托付类型的变量赋值
    delegate1 += ChineseGreeting; // 给此托付变量再绑定一个方法
    // 将先后调用 EnglishGreeting 与 ChineseGreeting 方法
    GreetPeople("Jimmy Zhang", delegate1);
    Console.ReadKey();
    }
    输出为:
    Morning, Jimmy Zhang
    早上好, Jimmy Zhang
    实际上。我们也能够绕过GreetPeople方法。通过托付来直接调用EnglishGreeting和ChineseGreeting:
    1
    2
    3
    4
    5
    6
    7
    8
    static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // 先给托付类型的变量赋值
    delegate1 += ChineseGreeting; // 给此托付变量再绑定一个方法
    // 将先后调用 EnglishGreeting 与 ChineseGreeting 方法
    delegate1 ("Jimmy Zhang");
    Console.ReadKey();
    }
    NOTE:这在本例中是没有问题的,但回头看下上面GreetPeople()的定义,在它之中能够做一些对于EnglishGreeting和ChineseGreeting来说都须要进行的工作,为了简便我做了省略。
    注意这里,第一次用的“=”,是赋值的语法。第二次。用的是“+=”,是绑定的语法。假设第一次就使用“+=”。将出现“使用了未赋值的局部变量”的编译错误。
    我们也能够使用以下的代码来这样简化这一过程:
    GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
    delegate1 += ChineseGreeting; // 给此托付变量再绑定一个方法
    看到这里,应该注意到,这段代码第一条语句与实例化一个类是何其的相似。你不禁想到:上面第一次绑定托付时不能够使用“+=”的编译错误,也许能够用这个方案来避免:
    GreetingDelegate delegate1 = new GreetingDelegate();
    delegate1 += EnglishGreeting; // 这次用的是 “+=”,绑定语法。
    delegate1 += ChineseGreeting; // 给此托付变量再绑定一个方法
    但实际上,这样会出现编译错误: “GreetingDelegate”方法没有採用“0”个參数的重载。

    虽然这种结果让我们认为有点沮丧,可是编译的提示:“没有0个參数的重载”再次让我们联想到了类的构造函数。

    我知道你一定按捺不住想探个到底,但在此之前。我们须要先把基础知识和应用介绍完。

    既然给托付能够绑定一个方法,那么也应该有办法取消对方法的绑定,非常easy想到。这个语法是“-=”:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static void Main(string[] args) {
    GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
    delegate1 += ChineseGreeting; // 给此托付变量再绑定一个方法
    // 将先后调用 EnglishGreeting 与 ChineseGreeting 方法
    GreetPeople("Jimmy Zhang", delegate1);
    Console.WriteLine();
    delegate1 -= EnglishGreeting; //取消对EnglishGreeting方法的绑定
    // 将仅调用 ChineseGreeting
    GreetPeople("张子阳", delegate1);
    Console.ReadKey();
    }
    输出为:
    Morning, Jimmy Zhang
    早上好, Jimmy Zhang
    早上好, 张子阳
    让我们再次对托付作个总结:
    使用托付能够将多个方法绑定到同一个托付变量,当调用此变量时(这里用“调用”这个词,是由于此变量代表一个方法),能够依次调用全部绑定的方法。

    由来

    编辑
    我们继续思考上面的程序:上面的三个方法都定义在Program类中,这样做是为了理解的方便,实际应用中。通常都是 GreetPeople 在一个类中,ChineseGreeting和 EnglishGreeting 在另外的类中。你已经对托付有了初步了解,是时候对上面的样例做个改进了。如果我们将GreetingPeople()放在一个叫GreetingManager的类中,那么新程序应该是这个样子的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace Delegate {
    //定义托付,它定义了能够代表的方法的类型
    public delegate void GreetingDelegate(string name);
    //新建的GreetingManager类
    public class GreetingManager{
    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
    }
    }
    class Program {
    private static void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
    }
    private static void ChineseGreeting(string name) {
    Console.WriteLine("早上好, " + name);
    }
    static void Main(string[] args) {
    // ... ...
    }
    }
    }
    这个时候,假设要实现前面演示的输出效果,Main方法我想应该是这种:
    1
    2
    3
    4
    5
    static void Main(string[] args) {
    GreetingManager gm = new GreetingManager();
    gm.GreetPeople("Jimmy Zhang", EnglishGreeting);
    gm.GreetPeople("张子阳", ChineseGreeting);
    }
    我们执行这段代码。嗯,没有不论什么问题。

    程序一如预料地那样输出了:

    Morning, Jimmy Zhang
    早上好, 张子阳
    如果我们须要使用上一节学到的知识,将多个方法绑定到同一个托付变量,该怎样做呢?让我们再次改写代码:
    1
    2
    3
    4
    5
    6
    7
    static void Main(string[] args) {
    GreetingManager gm = new GreetingManager();
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting;
    gm.GreetPeople("Jimmy Zhang", delegate1);
    }
    输出:
    Morning, Jimmy Zhang
    早上好, Jimmy Zhang
    到了这里。我们不禁想到:面向对象设计。讲究的是对象的封装,既然能够声明托付类型的变量(在上例中是delegate1)。我们何不将这个变量封装到 GreetManager类中?在这个类的client中使用不是更方便么?于是,我们改写GreetManager类。像这样:
    1
    2
    3
    4
    5
    6
    7
    public class GreetingManager{
    //在GreetingManager类的内部声明delegate1变量
    public GreetingDelegate delegate1;
    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
    MakeGreeting(name);
    }
    }
    我们能够这样使用这个托付变量:
    1
    2
    3
    4
    5
    6
    static void Main(string[] args) {
    GreetingManager gm = new GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;
    gm.GreetPeople("Jimmy Zhang", gm.delegate1);
    }
    输出为:
    Morning, Jimmy Zhang
    早上好, Jimmy Zhang
    虽然这样做没有不论什么问题,但我们发现这条语句非常奇怪。在调用gm.GreetPeople方法的时候,再次传递了gm的delegate1字段:
    gm.GreetPeople("Jimmy Zhang", gm.delegate1);
    既然如此,我们何不改动 GreetingManager 类成这样:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class GreetingManager{
    //在GreetingManager类的内部声明delegate1变量
    public GreetingDelegate delegate1;
    public void GreetPeople(string name) {
    if(delegate1!=null){ //假设有方法注冊托付变量
    delegate1(name); //通过托付调用方法
    }
    }
    }
    在client,调用看上去更简洁一些:
    1
    2
    3
    4
    5
    6
    static void Main(string[] args) {
    GreetingManager gm = new GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;
    gm.GreetPeople("Jimmy Zhang"); //注意,这次不须要再传递 delegate1变量
    }
    输出为:
    Morning, Jimmy Zhang
    早上好, Jimmy Zhang
    虽然这样达到了我们要的效果,可是还是存在着问题:
    在这里,delegate1和我们平时用的string类型的变量没有什么分别,而我们知道,并非全部的字段都应该声明成public,合适的做法是应该public的时候public,应该private的时候private。
    我们先看看假设把 delegate1 声明为 private会如何?结果就是:这简直就是在搞笑。由于声明托付的目的就是为了把它暴露在类的client进行方法的注冊。你把它声明为private了,client对它根本就不可见。那它还有什么用?
    再看看把delegate1 声明为 public 会如何?结果就是:client能够对它进行任意的赋值等操作,严重破坏对象的封装性。
    最后。第一个方法注冊用“=”。是赋值语法。由于要进行实例化,第二个方法注冊则用的是“+=”。可是。不管是赋值还是注冊,都是将方法绑定到托付上。除了调用时先后顺序不同,再没有不论什么的分别。这样不是让人认为非常别扭么?
    我们想想。假设delegate1不是一个托付类型。而是一个string类型,你会怎么做?答案是使用属性对字段进行封装。
    于是,Event出场了,它封装了托付类型的变量,使得:在类的内部,无论你声明它是public还是protected。它总是private的。

    在类的外部,注冊“+=”和注销“-=”的訪问限定符与你在声明事件时使用的訪问符同样。

    我们改写GreetingManager类,它变成了这个样子:
    1
    2
    3
    4
    5
    6
    7
    public class GreetingManager{
    //这一次我们在这里声明一个事件
    public event GreetingDelegate MakeGreet;
    public void GreetPeople(string name) {
    MakeGreet(name);
    }
    }
    非常easy注意到:MakeGreet 事件的声明与之前托付变量delegate1的声明唯一的差别是多了一个eventkeyword。看到这里,在结合上面的解说,你应该明确到:事件事实上没什么不好理解的,声明一个事件只是类似于声明一个进行了封装的托付类型的变量而已。

    为了证明上面的推论。假设我们像以下这样改写Main方法:
    1
    2
    3
    4
    5
    6
    static void Main(string[] args) {
    GreetingManager gm = new GreetingManager();
    gm.MakeGreet = EnglishGreeting; // 编译错误1
    gm.MakeGreet += ChineseGreeting;
    gm.GreetPeople("Jimmy Zhang");
    }
    会得到编译错误:事件“Delegate.GreetingManager.MakeGreet”仅仅能出如今 += 或 -= 的左边(从类型“Delegate.GreetingManager”中使用时除外)。

    编译代码

    编辑
    这时候,我们凝视掉编译错误的行。然后又一次进行编译,再借助Reflector来对 event的声明语句做一探究,看看为什么会发生这种错误:
    public event GreetingDelegate MakeGreet;
    能够看到。实际上虽然我们在GreetingManager里将 MakeGreet 声明为public,可是,实际上MakeGreet会被编译成 私有字段。难怪会发生上面的编译错误了。由于它根本就不同意在GreetingManager类的外面以赋值的方式訪问,从而验证了我们上面所做的推论。
    我们再进一步看下MakeGreet所产生的代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    private GreetingDelegate MakeGreet; //对事件的声明 实际是 声明一个私有的托付变量
    [MethodImpl(MethodImplOptions.Synchronized)]
    public void add_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
    }
    [MethodImpl(MethodImplOptions.Synchronized)]
    public void remove_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
    }
    已经非常明白了:MakeGreet事件确实是一个GreetingDelegate类型的托付。仅仅只是无论是不是声明为public,它总是被声明为private。

    另外。它还有两个方法,各自是add_MakeGreet和remove_MakeGreet。这两个方法分别用于注冊托付类型的方法和取消注冊。实际上也就是: “+= ”相应 add_MakeGreet。“-=”相应remove_MakeGreet。而这两个方法的訪问限制取决于声明事件时的訪问限制符。

    在add_MakeGreet()方法内部,实际上调用了System.Delegate的Combine()静态方法,这种方法用于将当前的变量加入到托付链表中。我们前面提到过两次,说托付实际上是一个类,在我们定义托付的时候:
    1
    2
    3
    4
    5
    6
    7
    8
    public delegate void GreetingDelegate(string name);
    当编译器遇到这段代码的时候,会生成以下这样一个完整的类:
    public sealed class GreetingDelegate:System.MulticastDelegate{
    public GreetingDelegate(object @object, IntPtr method);
    public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
    public virtual void EndInvoke(IAsyncResult result);
    public virtual void Invoke(string name);
    }
    关于这个类的更深入内容,能够參阅《CLR Via C#》等相关书籍,这里就不再讨论了。

    设计模式

    编辑
    我们之前已经对托付和事件介绍非常多了。写代码应该非常easy了,在这里直接给出代码,并在凝视中加以说明。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    using System;
    using System.Collections.Generic;
    using System.Text;
    namespace Delegate {
    // 热水器
    public class Heater {
    private int temperature;
    public delegate void BoilHandler(int param); //声明托付
    public event BoilHandler BoilEvent; //声明事件
    // 烧水
    public void BoilWater() {
    for (int i = 0; i <= 100; i++) {
    temperature = i;
    if (temperature > 95) {
    if (BoilEvent != null) { //假设有对象注冊
    BoilEvent(temperature); //调用全部注冊对象的方法
    }
    }
    }
    }
    }
    // 警报器
    public class Alarm {
    public void MakeAlert(int param) {
    Console.WriteLine("Alarm:嘀嘀嘀。水已经 {0} 度了:", param);
    }
    }
    // 显示器
    public class Display {
    public static void ShowMsg(int param) { //静态方法
    Console.WriteLine("Display:水快烧开了,当前温度:{0}度。

    ", param);

    }
    }
    class Program {
    static void Main() {
    Heater heater = new Heater();
    Alarm alarm = new Alarm();
    heater.BoilEvent += alarm.MakeAlert; //注冊方法
    heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注冊方法
    heater.BoilEvent += Display.ShowMsg; //注冊静态方法
    heater.BoilWater(); //烧水。会自己主动调用注冊过对象的方法
    }
    }
    }
    输出为:
    Alarm:嘀嘀嘀,水已经 96 度了:
    Alarm:嘀嘀嘀,水已经 96 度了:
    Display:水快烧开了,当前温度:96度。
    // 省略...

    范例说明

    如果我们有个高档的热水器,我们给它通上电。当水温超过95度的时候:1、扬声器会開始发出语音。告诉你水的温度;2、液晶屏也会改变水温的显示,来提示水已经快烧开了。
    我们须要写个程序来模拟这个烧水的过程,我们将定义一个类来代表热水器,我们管它叫:Heater,它有代表水温的字段。叫做temperature。当然。还有不可缺少的给水加热方法BoilWater(),一个发出语音警报的方法MakeAlert(),一个显示水温的方法。ShowMsg()。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    namespace Delegate {
    class Heater {
    private int temperature; // 水温
    // 烧水
    public void BoilWater() {
    for (int i = 0; i <= 100; i++) {
    temperature = i;
    if (temperature > 95) {
    MakeAlert(temperature);
    ShowMsg(temperature);
    }
    }
    }
    // 发出语音警报
    private void MakeAlert(int param) {
    Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);
    }
    // 显示水温
    private void ShowMsg(int param) {
    Console.WriteLine("Display:水快开了,当前温度:{0}度。

    " , param);

    }
    }
    class Program {
    static void Main() {
    Heater ht = new Heater();
    ht.BoilWater();
    }
    }
    }

    模式简单介绍

    上面的样例显然能完毕我们之前描写叙述的工作,可是却并不够好。如果热水器由三部分组成:热水器、警报器、显示器。它们来自于不同厂商并进行了组装。那么。应该是热水器只负责烧水。它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。
    这时候。上面的样例就应该变成这个样子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 热水器
    public class Heater {
    private int temperature;
    // 烧水
    private void BoilWater() {
    for (int i = 0; i <= 100; i++) {
    temperature = i;
    }
    }
    }
    // 警报器
    public class Alarm{
    private void MakeAlert(int param) {
    Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);
    }
    }
    // 显示器
    public class Display{
    private void ShowMsg(int param) {
    Console.WriteLine("Display:水已烧开,当前温度:{0}度。

    " , param);

    }
    }
    这里就出现了一个问题:怎样在水烧开的时候通知报警器和显示器?在继续进行之前,我们先了解一下Observer设计模式,Observer设计模式中主要包含例如以下两类对象:
    Subject:监视对象,它往往包括着其它对象所感兴趣的内容。在本范例中。热水器就是一个监视对象,它包括的其它对象所感兴趣的内容,就是temprature字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。

    Observer:监视者,它监视Subject,当Subject中的某件事发生的时候。会告知Observer,而Observer则会採取对应的行动。

    在本范例中,Observer有警报器和显示器,它们採取的行动各自是发出警报和显示水温。 在本例中。事情发生的顺序应该是这种:

    警报器和显示器告诉热水器,它对它的温度比較感兴趣(注冊)。 热水器知道后保留对警报器和显示器的引用。 热水器进行烧水这一动作。当水温超过95度时。通过对警报器和显示器的引用,自己主动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。

    类似这种样例是非常多的。GOF对它进行了抽象,称为Observer设计模式:Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时。其它依赖于它的对象会被自己主动告知并更新。Observer模式是一种松耦合的设计模式。

    托付事件

    编辑
    虽然上面的范例非常好地完毕了我们想要完毕的工作,可是我们不仅疑惑:为什么.Net Framework[1] 中的事件模型和上面的不同?为什么有非常多的EventArgs參数?
    在回答上面的问题之前,我们先搞懂 .Net Framework的编码规范:
    托付类型的名称都应该以EventHandler结束。

    托付的原型定义:有一个void返回值,并接受两个输入參数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。 事件的命名为 托付去掉 EventHandler之后剩余的部分。

    继承自EventArgs的类型应该以EventArgs结尾。 再做一下说明:

    托付声明原型中的Object类型的參数代表了Subject。也就是被监视对象,在本例中是 Heater(热水器)。

    回调函数(比方Alarm的MakeAlert)能够通过它訪问触发事件的对象(Heater)。 EventArgs 对象包括了Observer所感兴趣的数据,在本例中是temperature。上面这些事实上不不过为了编码规范而已。这样也使得程序有更大的灵活性。比方说,假设我们不光想获得热水器的温度,还想在Observer端(警报器或者显示器)方法中获得它的生产日期、型号、价格,那么托付和方法的声明都会变得非常麻烦。而假设我们将热水器的引用传给警报器的方法。就能够在方法中直接訪问热水器了。

    我们改写之前的范例。让它符合 .Net Framework[1] 的规范:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    using System;
    using System.Collections.Generic;
    using System.Text;
    namespace Delegate {
    // 热水器
    public class Heater {
    private int temperature;
    public string type = "RealFire 001"// 加入型号作为演示
    public string area = "China Xi'an"// 加入产地作为演示
    //声明托付
    public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
    public event BoiledEventHandler Boiled; //声明事件
    // 定义BoiledEventArgs类。传递给Observer所感兴趣的信息
    //继承EventArgs
    public class BoiledEventArgs : EventArgs {
    public readonly int temperature;//
    public BoiledEventArgs(int temperature) {
    this.temperature = temperature;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    // 能够供继承自 Heater 的类重写,以便继承类拒绝其它对象对它的监视
    protected virtual void OnBoiled(BoiledEventArgs e) {
    if (Boiled != null) { // 假设有对象注冊
    Boiled(this, e); // 调用全部注冊对象的方法
    }
    }
    // 烧水。

    public void BoilWater() {
    for (int i = 0; i <= 100; i++) {
    temperature = i;
    if (temperature > 95) {
    //建立BoiledEventArgs 对象。
    BoiledEventArgs e = new BoiledEventArgs(temperature);
    OnBoiled(e); // 调用 OnBoiled方法
    }
    }
    }
    }
    // 警报器
    public class Alarm {
    public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {
    Heater heater = (Heater)sender; //这里是不是非常熟悉呢?
    //訪问 sender 中的公共字段
    Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
    Console.WriteLine("Alarm: 嘀嘀嘀。水已经 {0} 度了:", e.temperature);
    Console.WriteLine();
    }
    }
    // 显示器
    public class Display {
    public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) { //静态方法
    Heater heater = (Heater)sender;
    Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
    Console.WriteLine("Display:水快烧开了。当前温度:{0}度。

    ", e.temperature);

    Console.WriteLine();
    }
    }
    class Program {
    static void Main() {
    Heater heater = new Heater();
    Alarm alarm = new Alarm();
    heater.Boiled += alarm.MakeAlert; //注冊方法
    heater.Boiled += (new Alarm()).MakeAlert; //给匿名对象注冊方法
    heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert); //也能够这么注冊
    heater.Boiled += Display.ShowMsg; //注冊静态方法
    heater.BoilWater(); //烧水,会自己主动调用注冊过对象的方法
    }
    }
    }
    输出为:
    Alarm:China Xian - RealFire 001:
    Alarm: 嘀嘀嘀,水已经 96 度了:
    Alarm:China Xian - RealFire 001:
    Alarm: 嘀嘀嘀。水已经 96 度了:
    Alarm:China Xian - RealFire 001:
    Alarm: 嘀嘀嘀。水已经 96 度了:
    Display:China Xian - RealFire 001:
    Display:水快烧开了,当前温度:96度。

    // 省略 ...
    在本文中我首先通过一个GreetingPeople的小程序向大家介绍了托付的概念、托付用来做什么,随后又引出了事件,接着对托付与事件所产生的中间代码做了粗略的讲述。
    在第二个略微复杂点的热水器的范例中。我向大家简要介绍了 Observer设计模式。并通过实现这个范例完毕了该模式,随后讲述了.Net Framework[1] 中托付、事件的实现方式。
    希望这篇文章能给你带来帮助。
  • 相关阅读:
    代码规范总结
    git记住提交密码的技巧
    php foreach遍历
    flight学习笔记
    the resource is not on the build path of a php project
    Google安装postman插件
    PHP开发框架CodeIgniter
    eclipse中php项目开发的环境配置说明
    MyBatis入门篇
    mybatis学习(十二)——mybatis逆向工程
  • 原文地址:https://www.cnblogs.com/cynchanpin/p/7403780.html
Copyright © 2011-2022 走看看