zoukankan      html  css  js  c++  java
  • C# 事件

    C# 事件1、多播委托

    2、事件
    3、自定义事件
     
    在上一章中,所有委托都只支持单一回调。
    然而,一个委托变量可以引用一系列委托,在这一系列委托中,每个委托都顺序指向一个后续的委托,
    从而形成了一个委托链,或者称为多播委托*multicast delegate)。
    使用多播委托,可以通过一个方法对象来调用一个方法链,创建变量来引用方法链,并将那些数据类型用
    作参数传递给方法。
    在C#中,多播委托的实现是一个通用的模式,目的是避免大量的手工编码。这个模式称为
    observer(观察者)或者publish-subscribe模式,它要应对的是这样一种情形:你需要将单一事件的通知
    (比如对象状态发生的一个变化)广播给多个订阅者(subscriber)。
     
    一、使用多播委托来编码Observer模式
     
    来考虑一个温度控制的例子。
    假设:一个加热器和一个冷却器连接到同一个自动调温器。
     
    为了控制加热器和冷却器的打开和关闭,要向它们通知温度的变化。
    自动调温器将温度的变化发布给多个订阅者---也就是加热器和冷却器。
     
    复制代码
      1     class Program
      2     {
      3         static void Main(string[] args)
      4         {
      5             //连接发布者和订阅者
      6             Thermostat tm = new Thermostat();
      7             Cooler cl = new Cooler(40);
      8             Heater ht = new Heater(60);
      9             //设置委托变量关联的方法。+=可以存储多个方法,这些方法称为订阅者。
     10             tm.OnTemperatureChange += cl.OnTemperatureChanged;
     11             tm.OnTemperatureChange += ht.OnTemperatureChanged;
     12             string temperature = Console.ReadLine();
     13  
     14             //将数据发布给订阅者(本质是依次运行那些方法)
     15             tm.OnTemperatureChange(float.Parse(temperature));
     16  
     17             Console.ReadLine();
     18  
     19  
     20  
     21         }
     22     }
     23     //两个订阅者类
     24     class Cooler
     25     {
     26         public Cooler(float temperature)
     27         {
     28             _Temperature = temperature;
     29         }
     30         private float _Temperature;
     31         public float Temperature
     32         {
     33             set
     34             {
     35                 _Temperature = value;
     36             }
     37             get
     38             {
     39                 return _Temperature;
     40             }
     41         }
     42  
     43         //将来会用作委托变量使用,也称为订阅者方法
     44         public void OnTemperatureChanged(float newTemperature)
     45         {
     46             if (newTemperature > _Temperature)
     47             {
     48                 Console.WriteLine("Cooler:on ! ");
     49             }
     50             else
     51             {
     52                 Console.WriteLine("Cooler:off ! ");
     53             }
     54         }
     55     }
     56     class Heater
     57     {
     58         public Heater(float temperature)
     59         {
     60             _Temperature = temperature;
     61         }
     62         private float _Temperature;
     63         public float Temperature
     64         {
     65             set
     66             {
     67                 _Temperature = value;
     68             }
     69             get
     70             {
     71                 return _Temperature;
     72             }
     73         }
     74         public void OnTemperatureChanged(float newTemperature)
     75         {
     76             if (newTemperature < _Temperature)
     77             {
     78                 Console.WriteLine("Heater:on ! ");
     79             }
     80             else
     81             {
     82                 Console.WriteLine("Heater:off ! ");
     83             }
     84         }
     85     }
     86  
     87  
     88     //发布者
     89     class Thermostat
     90     {
     91  
     92         //定义一个委托类型
     93         public delegate void TemperatureChangeHanlder(float newTemperature);
     94         //定义一个委托类型变量,用来存储订阅者列表。注:只需一个委托字段就可以存储所有订阅者。
     95         private TemperatureChangeHanlder _OnTemperatureChange;
     96         //现在的温度
     97         private float _CurrentTemperature;
     98  
     99         public TemperatureChangeHanlder OnTemperatureChange
    100         {
    101             set { _OnTemperatureChange = value; }
    102             get { return _OnTemperatureChange; }
    103         }
    104  
    105  
    106         public float CurrentTemperature
    107         {
    108             get { return _CurrentTemperature;}
    109             set
    110             {
    111                 if (value != _CurrentTemperature)
    112                 {
    113                     _CurrentTemperature = value;
    114                 }
    115             }
    116         }
    117     }
    复制代码
    上述代码使用+=运算符来直接赋值。向其OnTemperatureChange委托注册了两个订阅者。
    目前还没有将发布Thermostat类的CurrentTemperature属性每次变化时的值,通过调用委托来
    向订阅者通知温度的变化,为此需要修改属性的set语句。
    这样以后,每次温度变化都会通知两个订阅者。
    复制代码
     public float CurrentTemperature
            {
                get { return _CurrentTemperature; }
                set
                {
                    if (value != _CurrentTemperature)
                    {
                        _CurrentTemperature = value;
                        OnTemperatureChange(value);
                    }
                }
            }
    复制代码
    这里,只需要执行一个调用,即可向多个订阅者发出通知----这天是将委托更明确地
    称为“多播委托”的原因。
    针对这种以上的写法有几个需要注意的点:
    1、在发布事件代码时非常重要的一个步骤:假如当前没有订阅者注册接收通知。
    则OnTemperatureChange为空,执行OnTemperatureChange(value)语句会引发一
    个NullReferenceException。所以需要检查空值。
     
    复制代码
            public float CurrentTemperature
            {
                get { return _CurrentTemperature; }
                set
                {
                    if (value != _CurrentTemperature)
                    {
     
                        _CurrentTemperature = value;
                        TemperatureChangeHanlder localOnChange = OnTemperatureChange;
                        if (localOnChange != null)
                        {
                            //OnTemperatureChange = null;
                            localOnChange(value);
                        }
     
                    }
                }
            }
    复制代码
    在这里,我们并不是一开始就检查空值,而是首先将OnTemperatureChange赋值给另一个委托变量localOnChange .
    这个简单的修改可以确保在检查空值和发送通知之间,假如所有OnTemperatureChange订阅者都被移除(由一个不同的线程),那么不会触发
    NullReferenceException异常。
     
    注:将-=运算符应用于委托会返回一个新实例。
    对委托OnTemperatureChange-=订阅者,的任何调用都不会从OnTemperatureChange中删除一个委托而使它的委托比之前少一个,相反,
    会将一个全新的多播委托指派给它,这不会对原始的多播委托产生任何影响(localOnChange也指向那个原始的多播委托),只会减少对它的一个引用。
    委托是一个引用类型。
    2、委托运算符
    为了合并Thermostat例子中的两个订阅者,要使用"+="运算符。
    这样会获取引一个委托,并将第二个委托添加到委托链中,使一个委托指向下一个委托。
    第一个委托的方法被调用之后,它会调用第二个委托。从委托链中删除委托,则要使用"-="运算符。
    复制代码
    1             Thermostat.TemperatureChangeHanlder delegate1;
    2             Thermostat.TemperatureChangeHanlder delegate2;
    3             Thermostat.TemperatureChangeHanlder delegate3;
    4             delegate3 = tm.OnTemperatureChange;
    5             delegate1 = cl.OnTemperatureChanged;
    6             delegate2 = ht.OnTemperatureChanged;
    7             delegate3 += delegate1;
    8             delegate3 += delegate2;
    复制代码
    同理可以使用+ 与  - 。
    复制代码
    1             Thermostat.TemperatureChangeHanlder delegate1;
    2             Thermostat.TemperatureChangeHanlder delegate2;
    3             Thermostat.TemperatureChangeHanlder delegate3;
    4             delegate1 = cl.OnTemperatureChanged;
    5             delegate2 = ht.OnTemperatureChanged;
    6             delegate3 = delegate1 + delegate2;
    7             delegate3 = delegate3 - delegate2;
    8             tm.OnTemperatureChange = delegate3;
    复制代码
               
    使用赋值运算符,会清除之前的所有订阅者,并允许使用新的订阅者替换它们。
    这是委托很容易让人犯错的一个设置。因为本来需要使用"+="运算的时候,很容易就会错误地写成"="
    无论是 +、-、 +=、 -=,在内部都是使用静态方法System.Delegate.Combine()和System.Delegate.Remove()来实现的。
     
    3、顺序调用
     
    委托调用顺序图,需要下载。
    虽然一个tm.OnTemperatureChange()调用造成每个订阅者都收到通知,但它们仍然是顺序调用的,而不是同时调用,因为
    一个委托能指向另一个委托,后者又能指向其它委托。
     
    注:多播委托的内部机制
    delegate关键字是派生自System.MulticastDelegate的一个类型的别名。
    System.MulticastDelegate则是从System.Delegate派生的,后者由一个对象引用和一个System.Reflection.MethodInfo类型的该批针构成。
     
    创建一个委托时,编译器自动使用System.MulticastDelegate类型而不是System.Delegate类型。
    MulticastDelegate类包含一个对象引用和一个方法指针,这和它的Delegate基类是一样的,但除此之外,
    它还包含对另一个System.MulticastDelegate对象的引用 。
     
    向一个多播委托添加一个方法时,MulticastDelegate类会创建委托类型的一个新实例,在新实例中为新增的方法存储对象引用和方法指针,
    并在委托实例列表中添加新的委托实例作为下一项。
    这样的结果就是,MulticastDelegate类维护关由多个Delegate对象构成的一个链表。
     
    调用多播委托时,链表中的委托实例会被顺序调用。通常,委托是按照它们添加时的顺序调用的。
     
    4、错误处理
    错误处理凸显了顺序通知的重要性。假如一个订阅者引发一个异常,链中后续订阅不接收不到通知。
    为了避免这个问题,使所有订阅者都能收到通知,必须手动遍历订阅者列表,并单独调用它们。
    复制代码
     1         public float CurrentTemperature
     2         {
     3             get { return _CurrentTemperature; }
     4             set
     5             {
     6                 if (value != _CurrentTemperature)
     7                 {
     8  
     9                     _CurrentTemperature = value;
    10                     TemperatureChangeHanlder localOnChange = OnTemperatureChange;
    11                     if (localOnChange != null)
    12                     {
    13                         foreach (TemperatureChangeHanlder hanlder in localOnChange.GetInvocationList())
    14                         {
    15                             try
    16                             {
    17                                 hanlder(value);
    18                             }
    19                             catch (Exception e)
    20                             {
    21                                 Console.WriteLine(e.Message);
    22  
    23                             }
    24                         }
    25                     }
    26  
    27                 }
    28             }
    29         }
    复制代码
     
    5、方法返回值和传引用
    在这种情形下,也有必要遍历委托调用列表,而非直接激活一个通知。
    因为不同的订阅者返回的值可能不一。所以需要单独获取。
     
    二、事件
    目前使用的委托存在两个关键的问题。C#使用关键字event(事件)一解决这些问题。
     
    二、1 事件的作用:
     
    1、封装订阅
    如前所述,可以使用赋值运算符将一个委托赋给另一个。但这有可能造成bug。
    在本应该使用 "+=" 的位置,使用了"="。为了防止这种错误,就是根本
    不为包容类外部的对象提供对赋值运算符的运行。event关键字的目的就是提供额外
    的封装,避免你不小心地取消其它订阅者。
     
    2、封装发布
    委托和事件的第二个重要区别在于,事件确保只有包容类才能触发一个事件通知。防止在包容
    类外部调用发布者发布事件通知。
    禁止如以下的代码:
                tm.OnTemperatureChange(100);
    即使tm的CurrentTemperature没有发生改变,也能调用tm.OnTemperatureChange委托。
    所以和订阅者一样,委托的问题在于封装不充分。
     
     
    二、2 事件的声明
     
    C#用event关键字解决了上述两个问题,虽然看起来像是一个字段修饰符,但event定义的是一个新的成员类型。
    复制代码
     1     public class Thermostat
     2     {
     3         private float _CurrentTemperature;
     4         public float CurrentTemperature
     5         {
     6             set { _CurrentTemperature = value; }
     7             get { return _CurrentTemperature; }
     8         }
     9         //定义委托类型
    10         public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue);
    11  
    12         //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
    13         public event TemperatureChangeHandler OnTemperatureChange = delegate { };
    14  
    15  
    16         public class TemperatureArgs : System.EventArgs
    17         {
    18             private float _newTemperature;
    19             public float NewTemperature
    20             {
    21                 set { _newTemperature = value; }
    22                 get { return _newTemperature; }
    23             }
    24             public TemperatureArgs(float newTemperature)
    25             {
    26                 _newTemperature = newTemperature;
    27             }
    28  
    29         }
    30     }
    复制代码
     
    这个新的Thermostat类进行了几处修改:
    a、OnTemperatureChange属性被移除了,且被声明为一个public字段
    b、在OnTemperatureChange声明为字段的同时,使用了event关键字,这会禁止为一个public委托字段使用赋值运算符。
     只有包容类才能调用向所有订阅者发布通知的委托。
    以上两点解决了委托普通存在 的两个问题
    c、普通委托的另一个不利之处在于,易忘记在调用委托之前检查null值,
    通过event关键字提供的封装,可以在声明(或者在构造器中)采用一个替代方案,以上代码赋值了空委托。
    当然,如果委托存在被重新赋值为null的任何可能,仍需要进行null值检查。
    d、委托类型发生了改变,将原来的单个temperature参数替换成两个新参数。
     
    二、3 编码规范
    在以上的代码中,委托声明还发生另一处修改。
    为了遵循标准的C#编码规范,修改了TemperatureChangeHandler,将原来的单个temperature参数替换成两新参数,
    即sender和temperatureArgs。这一处修改并不是C#编译器强制的。
    但是,声明一个打算作为事件来使用的委托时,规范要求你传递这些类型的两个参数。
     
    第一个参数sender就包含"调用委托的那个类"的一个实例。假如一个订阅者方法注册了多个事件,这个参数就尤其有用。
    如两个不同的Thermostata实例都订阅了heater.OnTemperatureChanged事件,在这种情况下,任何一个Thermostat实例都
    可能触发对heater.OnTemperatureChanged的一个调用,为了判断具体是哪一个Thermostat实例触发了事件,要在Heater.OnTemperatureChanged()
    内部利用sender参数进行判断。
     
    第二个参数temperatureArgs属性Thermostat.TemperatureArgs类型。在这里使用嵌套类是恰当的,因为它遵循和OntermperatureChangeHandler委托本身
    相同的作用域。
    Thermostat.TemperatureArgs,一个重点在于它是从System.EventArgs派生的。System.EventArgs唯一重要的属性是
    Empty,它指出不存在事件数据。然而,从System.EventArgs派生出TemperatureArgs时,你添加了一个额外的属性,名为NewTemperature。这样一来
    就可以将温度从自动调温器传递到订阅者那里。
     
    编码规范小结:
    1、第一个参数sender是object类型的,它包含对调用委托的那个对象的一个引用。
    2、第二个参数是System.EventArgs类型的(或者是从System.EventArgs派生,但包含了事件数据的其它类型。)
    调用委托的方式和以前几乎完全一样,只是要提供附加的参数。
     
    复制代码
      1     class Program
      2     {
      3         static void Main(string[] args)
      4         {
      5             Thermostat tm = new Thermostat();
      6  
      7             Cooler cl = new Cooler(40);
      8             Heater ht = new Heater(60);
      9  
     10             //设置订阅者(方法)
     11             tm.OnTemperatureChange += cl.OnTemperatureChanged;
     12             tm.OnTemperatureChange += ht.OnTemperatureChanged;
     13  
     14             tm.CurrentTemperature = 100;
     15         }
     16     }
     17     //发布者类
     18     public class Thermostat
     19     {
     20         private float _CurrentTemperature;
     21         public float CurrentTemperature
     22         {
     23             set
     24             {
     25                 if (value != _CurrentTemperature)
     26                 {
     27                     _CurrentTemperature = value;
     28                     if (OnTemperatureChange != null)
     29                     {
     30                         OnTemperatureChange(this, new TemperatureArgs(value));
     31                     }
     32  
     33                 }
     34             }
     35             get { return _CurrentTemperature; }
     36         }
     37         //定义委托类型
     38         public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue);
     39  
     40         //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
     41         public event TemperatureChangeHandler OnTemperatureChange = delegate { };
     42  
     43         //用来给事件传递的数据类型
     44         public class TemperatureArgs : System.EventArgs
     45         {
     46             private float _newTemperature;
     47             public float NewTemperature
     48             {
     49                 set { _newTemperature = value; }
     50                 get { return _newTemperature; }
     51             }
     52             public TemperatureArgs(float newTemperature)
     53             {
     54                 _newTemperature = newTemperature;
     55             }
     56  
     57         }
     58     }
     59  
     60     //两个订阅者类
     61     class Cooler
     62     {
     63         public Cooler(float temperature)
     64         {
     65             _Temperature = temperature;
     66         }
     67         private float _Temperature;
     68         public float Temperature
     69         {
     70             set
     71             {
     72                 _Temperature = value;
     73             }
     74             get
     75             {
     76                 return _Temperature;
     77             }
     78         }
     79  
     80         //将来会用作委托变量使用,也称为订阅者方法
     81         public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
     82         {
     83             if (newTemperature.NewTemperature > _Temperature)
     84             {
     85                 Console.WriteLine("Cooler:on ! ");
     86             }
     87             else
     88             {
     89                 Console.WriteLine("Cooler:off ! ");
     90             }
     91         }
     92     }
     93     class Heater
     94     {
     95         public Heater(float temperature)
     96         {
     97             _Temperature = temperature;
     98         }
     99         private float _Temperature;
    100         public float Temperature
    101         {
    102             set
    103             {
    104                 _Temperature = value;
    105             }
    106             get
    107             {
    108                 return _Temperature;
    109             }
    110         }
    111         public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
    112         {
    113             if (newTemperature.NewTemperature < _Temperature)
    114             {
    115                 Console.WriteLine("Heater:on ! ");
    116             }
    117             else
    118             {
    119                 Console.WriteLine("Heater:off ! ");
    120             }
    121         }
    122     }
    复制代码
     
    通过将sender指定为容器类(this),因为它是能为事件调用委托的唯一一个类。
    在这个例子中,订阅者可以将sender参数强制转型为Thermostat,并以那种方式来访问当前温度,
    或通过TemperatureArgs实例来访问在。
    然而,Thermostat实例上的当前温度可能由一个不同的线程改变。
    在由于状态改变而发生事件的时候,连同新值传递前一个值是一个常见的编程模式,它可以决定哪些状态变化是
    允许的。
     
    二、4  泛型和委托
     
    使用泛型,可以在多个位置使用相同的委托数据类型,并在支持多个不同的参数类型的同时保持强类型。
    在C#2.0和更高版本需要使用事件的大多数场合中,都无需要声明一个自定义的委托数据类型
    System.EventHandler<T> 已经包含在Framework Class Library
    注:System.EventHandler<T> 用一个约束来限制T从EventArgs派生。注意是为了向上兼容。
            //定义委托类型
            public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue);
     
            //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
            public event TemperatureChangeHandler OnTemperatureChange = delegate { };
     
    使用以下泛型代替:
            public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { };
     
    事件的内部机制:
    事件是限制外部类只能通过 "+="运算符向发布添加订阅方法,并用"-="运算符取消订阅,除此之外的任何事件都不允许做。
    此外,它们还阻止除包容类之外的其他任何类调用事件。
    为了达到上述目的,C#编译器会获取带有event修饰符的public委托变量,并将委托声明为private。
    除此之外,它还添加了两个方法和两个特殊的事件块。从本质上说,event关键字是编译器用于生成恰当封装逻辑的
    一个C#快捷方式。
     
    C#实在现一个属性时,会创建get set,
    此处的事件属性使用了 add remove分别使用了Sytem.Delegate.Combine
    与 System.Delegate.Remove
     
     
    复制代码
     1         //定义委托类型
     2         public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue);
     3  
     4         //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
     5         public event TemperatureChangeHandler OnTemperatureChange = delegate { };
     6  
     7 在编译器的作用下,会自动扩展成:  
     8         private TemperatureChangeHandler _OnTemperatureChange = delegate { };
     9  
    10         public void add_OnTemperatureChange(TemperatureChangeHandler handler)
    11         {
    12             Delegate.Combine(_OnTemperatureChange, handler);
    13         }
    14         public void remove_OnTemperatureChange(TemperatureChangeHandler handler)
    15         {
    16             Delegate.Remove(_OnTemperatureChange, handler);
    17         }
    18         public event TemperatureChangeHandler OnTemperatureChange
    19         {
    20             add
    21             {
    22                 add_OnTemperatureChange(value);
    23             }
    24  
    25             remove
    26             {
    27                 remove_OnTemperatureChange(value);
    28             }
    29  
    30         }
    复制代码
    这两个方法add_OnTemperatureChange与remove_OnTemperatureChange 分别负责实现
    "+="和"-="赋值运算符。
    在最终的CIL代码中,仍然保留了event关键字。
    换言之,事件是CIL代码能够显式识别的一样东西,它并非只是一个C#构造。
     
     
    二、5 自定义事件实现
     
    编译器为"+="和"-="生成的代码是可以自定义的。
    例如,将OnTemperatureChange委托的作用域改成protected而不是private。这样一来,从Thermostat派生的类就被允许直接访问委托,
    而无需受到和外部类一样的限制。为此,可以允许添加定制的add 和 remove块。
    复制代码
     1         protected TemperatureChangeHandler _OnTemperatureChange = delegate { };
     2  
     3         public event TemperatureChangeHandler OnTemperatureChange
     4         {
     5             add
     6             {
     7                 //此处代码可以自定义
     8                 Delegate.Combine(_OnTemperatureChange, value);
     9  
    10             }
    11  
    12             remove
    13             {
    14                 //此处代码可以自定义
    15                 Delegate.Remove(_OnTemperatureChange, value);
    16             }
    17  
    18         }
    复制代码
     
    以后继承这个类的子类,就可以重写这个属性了。
    实现自定义事件。
     
    小结:通常,方法指针是唯一需要在事件上下文的外部乃至委托变量情况。
    换句话说:由于事件提供了额外的封装特性,而且允许你在必要时对实现进行自定义,所以最佳
    做法就是始终为Observer模式使用事件。
  • 相关阅读:
    思念
    空白
    curl json string with variable All In One
    virtual scroll list All In One
    corejs & RegExp error All In One
    socket.io All In One
    vue camelCase vs PascalCase vs kebabcase All In One
    element ui 表单校验,非必填字段校验 All In One
    github 定时任务 UTC 时间不准确 bug All In One
    input range & color picker All In One
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4663296.html
Copyright © 2011-2022 走看看