1、多播委托
2、事件
3、自定义事件
 
在上一章中,所有委托都只支持单一回调。
然而,一个委托变量可以引用一系列委托,在这一系列委托中,每个委托都顺序指向一个后续的委托,
从而形成了一个委托链,或者称为多播委托*multicast delegate)。
使用多播委托,可以通过一个方法对象来调用一个方法链,创建变量来引用方法链,并将那些数据类型用
作参数传递给方法。
在C#中,多播委托的实现是一个通用的模式,目的是避免大量的手工编码。这个模式称为
observer(观察者)或者publish-subscribe模式,它要应对的是这样一种情形:你需要将单一事件的通知
(比如对象状态发生的一个变化)广播给多个订阅者(subscriber)。
 
一、使用多播委托来编码Observer模式
 
来考虑一个温度控制的例子。
假设:一个加热器和一个冷却器连接到同一个自动调温器。
 
为了控制加热器和冷却器的打开和关闭,要向它们通知温度的变化。
自动调温器将温度的变化发布给多个订阅者---也就是加热器和冷却器。

     class Program
{
static void Main(string[] args)
{
//连接发布者和订阅者
Thermostat tm = new Thermostat();
Cooler cl = new Cooler();
Heater ht = new Heater();
//设置委托变量关联的方法。+=可以存储多个方法,这些方法称为订阅者。
tm.OnTemperatureChange += cl.OnTemperatureChanged;
tm.OnTemperatureChange += ht.OnTemperatureChanged;
string temperature = Console.ReadLine(); //将数据发布给订阅者(本质是依次运行那些方法)
tm.OnTemperatureChange(float.Parse(temperature)); Console.ReadLine(); }
}
//两个订阅者类
class Cooler
{
public Cooler(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
} //将来会用作委托变量使用,也称为订阅者方法
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature > _Temperature)
{
Console.WriteLine("Cooler:on ! ");
}
else
{
Console.WriteLine("Cooler:off ! ");
}
}
}
class Heater
{
public Heater(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
}
public void OnTemperatureChanged(float newTemperature)
{
if (newTemperature < _Temperature)
{
Console.WriteLine("Heater:on ! ");
}
else
{
Console.WriteLine("Heater:off ! ");
}
}
} //发布者
class Thermostat
{ //定义一个委托类型
public delegate void TemperatureChangeHanlder(float newTemperature);
//定义一个委托类型变量,用来存储订阅者列表。注:只需一个委托字段就可以存储所有订阅者。
private TemperatureChangeHanlder _OnTemperatureChange;
//现在的温度
private float _CurrentTemperature; public TemperatureChangeHanlder OnTemperatureChange
{
set { _OnTemperatureChange = value; }
get { return _OnTemperatureChange; }
} public float CurrentTemperature
{
get { return _CurrentTemperature;}
set
{
if (value != _CurrentTemperature)
{
_CurrentTemperature = value;
}
}
}
}
上述代码使用+=运算符来直接赋值。向其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例子中的两个订阅者,要使用"+="运算符。
这样会获取引一个委托,并将第二个委托添加到委托链中,使一个委托指向下一个委托。
第一个委托的方法被调用之后,它会调用第二个委托。从委托链中删除委托,则要使用"-="运算符。

             Thermostat.TemperatureChangeHanlder delegate1;
Thermostat.TemperatureChangeHanlder delegate2;
Thermostat.TemperatureChangeHanlder delegate3;
delegate3 = tm.OnTemperatureChange;
delegate1 = cl.OnTemperatureChanged;
delegate2 = ht.OnTemperatureChanged;
delegate3 += delegate1;
delegate3 += delegate2;
同理可以使用+ 与  - 。
             Thermostat.TemperatureChangeHanlder delegate1;
Thermostat.TemperatureChangeHanlder delegate2;
Thermostat.TemperatureChangeHanlder delegate3;
delegate1 = cl.OnTemperatureChanged;
delegate2 = ht.OnTemperatureChanged;
delegate3 = delegate1 + delegate2;
delegate3 = delegate3 - delegate2;
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、错误处理
错误处理凸显了顺序通知的重要性。假如一个订阅者引发一个异常,链中后续订阅不接收不到通知。
为了避免这个问题,使所有订阅者都能收到通知,必须手动遍历订阅者列表,并单独调用它们。

         public float CurrentTemperature
{
get { return _CurrentTemperature; }
set
{
if (value != _CurrentTemperature)
{ _CurrentTemperature = value;
TemperatureChangeHanlder localOnChange = OnTemperatureChange;
if (localOnChange != null)
{
foreach (TemperatureChangeHanlder hanlder in localOnChange.GetInvocationList())
{
try
{
hanlder(value);
}
catch (Exception e)
{
Console.WriteLine(e.Message); }
}
} }
}
}
 
5、方法返回值和传引用
在这种情形下,也有必要遍历委托调用列表,而非直接激活一个通知。
因为不同的订阅者返回的值可能不一。所以需要单独获取。
 
二、事件
目前使用的委托存在两个关键的问题。C#使用关键字event(事件)一解决这些问题。
 
二、1 事件的作用:
 
1、封装订阅
如前所述,可以使用赋值运算符将一个委托赋给另一个。但这有可能造成bug。
在本应该使用 "+=" 的位置,使用了"="。为了防止这种错误,就是根本
不为包容类外部的对象提供对赋值运算符的运行。event关键字的目的就是提供额外
的封装,避免你不小心地取消其它订阅者。
 
2、封装发布
委托和事件的第二个重要区别在于,事件确保只有包容类才能触发一个事件通知。防止在包容
类外部调用发布者发布事件通知。
禁止如以下的代码:
            tm.OnTemperatureChange(100);
即使tm的CurrentTemperature没有发生改变,也能调用tm.OnTemperatureChange委托。
所以和订阅者一样,委托的问题在于封装不充分。
 
 
二、2 事件的声明
 
C#用event关键字解决了上述两个问题,虽然看起来像是一个字段修饰符,但event定义的是一个新的成员类型。

     public class Thermostat
{
private float _CurrentTemperature;
public float CurrentTemperature
{
set { _CurrentTemperature = value; }
get { return _CurrentTemperature; }
}
//定义委托类型
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; public class TemperatureArgs : System.EventArgs
{
private float _newTemperature;
public float NewTemperature
{
set { _newTemperature = value; }
get { return _newTemperature; }
}
public TemperatureArgs(float newTemperature)
{
_newTemperature = newTemperature;
} }
}
 
这个新的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派生,但包含了事件数据的其它类型。)
调用委托的方式和以前几乎完全一样,只是要提供附加的参数。

     class Program
{
static void Main(string[] args)
{
Thermostat tm = new Thermostat(); Cooler cl = new Cooler();
Heater ht = new Heater(); //设置订阅者(方法)
tm.OnTemperatureChange += cl.OnTemperatureChanged;
tm.OnTemperatureChange += ht.OnTemperatureChanged; tm.CurrentTemperature = ;
}
}
//发布者类
public class Thermostat
{
private float _CurrentTemperature;
public float CurrentTemperature
{
set
{
if (value != _CurrentTemperature)
{
_CurrentTemperature = value;
if (OnTemperatureChange != null)
{
OnTemperatureChange(this, new TemperatureArgs(value));
} }
}
get { return _CurrentTemperature; }
}
//定义委托类型
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; //用来给事件传递的数据类型
public class TemperatureArgs : System.EventArgs
{
private float _newTemperature;
public float NewTemperature
{
set { _newTemperature = value; }
get { return _newTemperature; }
}
public TemperatureArgs(float newTemperature)
{
_newTemperature = newTemperature;
} }
} //两个订阅者类
class Cooler
{
public Cooler(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
} //将来会用作委托变量使用,也称为订阅者方法
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
{
if (newTemperature.NewTemperature > _Temperature)
{
Console.WriteLine("Cooler:on ! ");
}
else
{
Console.WriteLine("Cooler:off ! ");
}
}
}
class Heater
{
public Heater(float temperature)
{
_Temperature = temperature;
}
private float _Temperature;
public float Temperature
{
set
{
_Temperature = value;
}
get
{
return _Temperature;
}
}
public void OnTemperatureChanged(object sender, Thermostat.TemperatureArgs newTemperature)
{
if (newTemperature.NewTemperature < _Temperature)
{
Console.WriteLine("Heater:on ! ");
}
else
{
Console.WriteLine("Heater:off ! ");
}
}
}
 
通过将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
 

         //定义委托类型
public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperatrue); //定义一个委托变量,并用event修饰,被修饰后有一个新的名字,事件发布者。
public event TemperatureChangeHandler OnTemperatureChange = delegate { }; 在编译器的作用下,会自动扩展成:
private TemperatureChangeHandler _OnTemperatureChange = delegate { }; public void add_OnTemperatureChange(TemperatureChangeHandler handler)
{
Delegate.Combine(_OnTemperatureChange, handler);
}
public void remove_OnTemperatureChange(TemperatureChangeHandler handler)
{
Delegate.Remove(_OnTemperatureChange, handler);
}
public event TemperatureChangeHandler OnTemperatureChange
{
add
{
add_OnTemperatureChange(value);
} remove
{
remove_OnTemperatureChange(value);
} }
这两个方法add_OnTemperatureChange与remove_OnTemperatureChange 分别负责实现
"+="和"-="赋值运算符。
在最终的CIL代码中,仍然保留了event关键字。
换言之,事件是CIL代码能够显式识别的一样东西,它并非只是一个C#构造。
 
 
二、5 自定义事件实现
 
编译器为"+="和"-="生成的代码是可以自定义的。
例如,将OnTemperatureChange委托的作用域改成protected而不是private。这样一来,从Thermostat派生的类就被允许直接访问委托,
而无需受到和外部类一样的限制。为此,可以允许添加定制的add 和 remove块。

         protected TemperatureChangeHandler _OnTemperatureChange = delegate { };

         public event TemperatureChangeHandler OnTemperatureChange
{
add
{
//此处代码可以自定义
Delegate.Combine(_OnTemperatureChange, value); } remove
{
//此处代码可以自定义
Delegate.Remove(_OnTemperatureChange, value);
} }
 
以后继承这个类的子类,就可以重写这个属性了。
实现自定义事件。
 
小结:通常,方法指针是唯一需要在事件上下文的外部乃至委托变量情况。
换句话说:由于事件提供了额外的封装特性,而且允许你在必要时对实现进行自定义,所以最佳
做法就是始终为Observer模式使用事件。

十三、C# 事件的更多相关文章

  1. (十三)事件分发器——event()函数,事件过滤

    事件分发器——event()函数 事件过滤 事件进入窗口之前被拦截 eventFilter #include "mywidget.h" #include "ui_mywi ...

  2. 【转载】COM 组件设计与应用(十三)——事件和通知(VC6.0)

    原文:http://vckbase.com/index.php/wv/1243.html 一.前言 我的 COM 组件运行时产生一个窗口,当用户双击该窗口的时候,我需要通知调用者: 我的 COM 组件 ...

  3. JavaScript高级程序设计学习笔记第十三章--事件

    事件冒泡: IE 的事件流,事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档).例如: <!DOCTYPE html> <htm ...

  4. JavaScript 事件——“事件类型”中“HTML5事件”的注意要点

    contextmenu事件 该事件用以表示何时应该显示上下文菜单,以便开发者取消默认的上下文菜单,转而提供自定义的菜单. 因为该事件属于鼠标事件,所以其事件对象中包含与光标位置有关的所有属性.如: & ...

  5. 《Javascript高级程序设计第3版》精华总结

    一.JavaScript简介   1.1 javascript简史 1.2 javascript实现 + javascript是一种专为网页交互而设计的一种脚本语言,javascript由三大部分组成 ...

  6. js-JavaScript高级程序设计学习笔记9

    依然第十三章 事件 1.页面上的所有元素都支持鼠标事件,除了mouseenter和mouseleave,所有鼠标事件都会冒泡. 2.修改键:shift.ctrl.alt.meta.四个属性表示修改键的 ...

  7. js-JavaScript高级程序设计学习笔记8

    第十三章 事件 1.DOM2级事件规定的事件流包括三个阶段:事件捕获阶段.处于目标阶段.事件冒泡阶段. 2.大部分浏览器都会在捕获阶段出发对象上的事件,结果就是,有两个机会在目标对象上面操作事件. 3 ...

  8. javascript高级程序设计读书笔记

    第2章  在html中使用javascript 一般都会把js引用文件放在</body>前面,而不是放在<head>里, 目的是最后读取js文件以提高网页载入速度. 引用js文 ...

  9. 《JavaScript高级程序设计》 阅读计划

    第一周       第1章 JavaScript简介   1 第2章 在Html中使用JavaScript 1 第3章 基本概念   3         第二周       第4章 变量.作用域和内存 ...

  10. 使用回调接口实现ActiveX控件和它的容器程序的通讯

    本文阅读基础:有一定的C++基础知识(了解继承.回调函数),对MFC的消息机制有一定了解,对COM的基础知识有一定了解,对ActiveX控件有一定了解. 一. 前言 ActiveX控件和它的容器程序如 ...

随机推荐

  1. Volley框架支持HTTPS请求。

    第一次写帖子,嘿嘿. 最近了解到google2013IO大会出了个网络框架,正好项目也需要用到,就看了下. 最后发现接口都是HTTPS的,但是Volley默认是不支持HTTPS,网上找了好久,都没有对 ...

  2. BZOJ 1491 [NOI2007]社交网络

    1491: [NOI2007]社交网络 Time Limit: 10 Sec  Memory Limit: 64 MBSubmit: 1159  Solved: 660[Submit][Status] ...

  3. (转载)php获取mysql版本的几种方法小结

    (转载)http://www.jb51.net/article/13930.htm 查询当前连接的MYSQL数据库的版本,可以用下面SQL语句来实现 select VERSION(); 当前$res= ...

  4. (转载)mysql_query( )返回值

    (转载)http://hi.baidu.com/tfbzccqceabfhyd/item/bd01db9f8995204af04215e4 调用mysql_query( ),当查询操作是update. ...

  5. 【转】 log4cpp 的使用

    [转自] http://sogo6.iteye.com/blog/1154315     Log4cpp配置文件格式说明   log4cpp有3个主要的组件:categories(类别).append ...

  6. C# Dictionary的xml序列化

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.I ...

  7. 利用Trie树对字符串集合进行排序并计算特征值

    该算法用于将一组乱序的字符串反序列化到一个Trie树中,这个过程即可视为对字符串进行了一次排序. 还可以通过调用 GetFeatureString 将该 Trie 树重新序列化. #include & ...

  8. 趣解curl

    Curl是Linux下一个很强大的http命令行工具,其功能十分强大. 1) 二话不说,先从这里开始吧! $ curl http://www.linuxidc.com 回车之后,www.linuxid ...

  9. in_array严格模式和普通模式的区别

    貌似是因为test转整型变0  0和0 匹配能成功 返回真 启用严格模式发现没有这个问题

  10. html或jsp实现打印三种方法

    1.使用window.print()方法 优点:支持多浏览器 缺点:取消打印,隐藏打印不必要的信息后再显示比较麻烦 如下实现,可以打印当前页面 <input name ="Button ...