• 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模式使用事件。
  • 相关阅读:
    ROS_Kinetic_24 使用catkin_create_qt_pkg快速创建qt-ros功能包
    ROS_Kinetic_23 ROS流行版本和相关书籍汇总
    Android 5.1 添加硬件抽象层(HAL)和JNI接口总结
    Android 5.1.1 源码目录结构
    数据化决策的魅力
    [Android L]SEAndroid开放设备文件结点权限(读或写)方法(涵盖常用操作:sys/xxx、proc/xxx、SystemProperties)热门干货
    Android实现系统ROOT, 并能赋予app root权限
    ROS_Kinetic_22 使用ROS的qt插件即ros_qtc_plugin实现Hi ROS!!!!
    android 关机充电流程
    linux qcom LCD framwork
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4663296.html
Copyright © 2020-2023  润新知