您的位置:首页 > 编程语言 > C#

C#学习笔记(三)—–C#高级特性中的委托与事件(下)

2017-05-20 17:27 656 查看

C#高级特性中的委托与事件(下)

方法的返回值和传引用:还有一种值得注意的情形,在这种情形下,也有必要遍历委托调用列表,而非直接激活一个通知。这种情形涉及的委托要么不返回void,要么有一个ref或out参数。在Thermostat例子中,OnTemperatureChange委托是Action类型,它返回void,而且没有ref或out参数。其结果是没有数据返回给发布者。这一点相当重要,因为调用委托可能造成将一个通知发送给多个订阅者。假如订阅者会返回一个值,就无法确定应该使用哪个订阅者的返回值。

假如修改OnTemperatureChange,让它不是返回void,而是返回枚举值,指出设备是否因温度的改变而启动,那么新委托就是Func

事件

到目前为止,使用的委托都存在两个关键的问题。C#使用关键字event(事件)来解决这些问题。本节描述了如何使用事件,以及它们是如何工作的。

事件的作用:前面已全面描述了委托是如何工作的。然而,委托结构中存在的缺陷可能造成程序员在不经意中引入一个bug。这个问题和封装有关,无论事件的订阅还是发布,都不能得到充分的控制。

①封装订阅:如前所述,可以使用赋值操作符将一个委托赋给另一个。遗憾的是,这同时可能造成bug。来看下面的代码:

class Program
{
public static void Main()
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temperature;
// Note: Use new Action(
// cooler.OnTemperatureChanged) if C# 1.0
thermostat.OnTemperatureChange =
heater.OnTemperatureChanged;
// Bug: assignment operator overrides
// previous assignment.
thermostat.OnTemperatureChange =
cooler.OnTemperatureChanged;
Console.Write("Enter temperature: ");
temperature = Console.ReadLine();
thermostat.CurrentTemperature = int.Parse(temperature);
}
}


关于Cooler和Heater以及Thermostat的定义请查看前面的笔记,建议还是从开始看起,因为前面的理解不清楚的话,后面的东西就跟不上,学习就是这样的,要一步一个脚印,尤其是学编程,我记得一个大牛说过如果你没有写上个十万行代码就别说你是个程序员了。其实我觉得学编程这事儿,动手有时候更重要。

上面这个代码和我们之前写的代码是十分相似的,只是它不是使用+=操作符,而是使用一个简单赋值操作符。其结果就是,当代码将cooler.OnTemperatureChanged赋给OnTemperatureChange时,heater.OnTemperatureChanged会被清除,因为一个全新的委托链替代了之前的链。在本该使用+=操作符的地方使用了赋值操作符“=”,由于这是一个十分容易犯的错误,所以最好的解决方案就是仅为包容类内部的对象提供对赋值操作符的支持。event关键字的作用就是提供额外的封装,避免不小心地取消其他订阅者。

②封装发布:委托和事件的第二个重要区别在于,事件确保只有包容类(包含委托类型的类)才能触发事件通知。看下面的例子。

class Program
{
public static void Main()
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temperature;
// Note: Use new Action(
// cooler.OnTemperatureChanged) if C# 1.0.
thermostat.OnTemperatureChange +=
heater.OnTemperatureChanged;
thermostat.OnTemperatureChange +=
cooler.OnTemperatureChanged;
thermostat.OnTemperatureChange(42);
}
}


在代码清单13-11中,即使thermostat的CurrentTemperature没有发生改变,Program也能调用OnTemperatureChange委托。因此,Program触发了对所有thermostat订阅者的一个通知,告诉它们温度已发生改变,而实际上thermostat的温度并没有变化。和之前一样,委托的问题在于封装不充分。Thermostat应禁止其他任何类调用OnTemperatureChange委托。

事件的声明:C#用event关键字解决了上述两个问题。虽然看起来像是一个字段修饰符,但event定义的是一个新的成员类型,如下代码所示。

public class Thermostat
{
public class TemperatureArgs: System.EventArgs
{
public TemperatureArgs( float newTemperature )
{
NewTemperature = newTemperature;
}
public float NewTemperature
{
get{return _newTemperature;}
set{_newTemperature = value;}
}
private float _newTemperature;
}
// Define the event publisher
public event EventHandler<TemperatureArgs> OnTemperatureChange =
delegate { };
public float CurrentTemperature
{
...
}
private float _CurrentTemperature;
}


这个新的Thermostat类进行了4处修改。首先,OnTemperatureChange属性被移除了。OnTemperatureChange被声明为一个public字段。从表面看,这似乎并不是在解决早先描述的封装问题。现在需要的是增强封装,而不是让一个字段变成public字段来削弱封装。然而,我们进行的第二处修改是在字段声明之前添加event关键字。这一处简单的修改提供了所需的全部封装。添加event关键字后,会禁止为一个public委托字段使用赋值操作符(比如thermostat.OnTemperatureChange = cooler.OnTemperatureChanged)。除此之外,只有包容类才能调用向所有订阅者发出通知的委托(例如,不允许在类的外部执行thermostat.OnTemperatureChange (42))。换言之,event关键字提供了必要的封装来防止任何外部类发布一个事件或者取消之前不是由其添加的订阅者。这样,就完美地解决了普通委托存在的两个问题,这是在C#中提供event关键字的关键原因之一。

普通委托的另一个不利之处在于,很容易忘记在调用委托之前检查null值。这会引发一个非预期的NullReferenceException异常。幸好,通过event关键字提供的封装,可以在声明时(或者在构造器中)采用一个替代方案,如代码清单13-12所示。注意在声明事件时,我们赋的值是delegate { },它是一个空委托,代表由零个侦听者构成的一个集合。通过赋值一个空委托,就可以引发事件而不必检查是否有任何侦听者。(这个行为类似于向变量赋一个包含零个元素的数组。然后,在调用一个数组成员时,就不必先检查变量是否为null。)当然,如果委托存在被重新赋值为null的任何可能,那么仍需进行null值检查。不过,由于event关键字限制赋值只能在类的内部发生,所以要重新对委托进行赋值,只能在类中进行。如果从未在类中赋过null值,就不必在代码每次调用委托时检查null。

编码规范:为了获得希望的功能,唯一要做的就是将原始委托变量声明更改为字段,然后添加event关键字。进行了这两处修改之后,就可以提供全部必要的封装。与此同时,其他所有功能都和以前一样。然而,在上面的代码中,委托声明还进行了另一处修改。为了遵循标准的C#编码规范,要将
Action<float>
替换成一个新的委托类型
EventHandler<TemperatureArgs>
,这是一个CLR类型,其声明如下面的代码所示(自.NET Framework 2.0起添加)。

public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e)
where TEventArgs : EventArgs;


结果是Action<TEventArgs>委托类型中的单个温度参数被替换成两个新参数,一个代表发送者,另一个代表事件数据。这一处修改并不是C#编译器强制的。但是,声明一个打算作为事件来使用的委托时,规范是要求传递这些类型的两个参数。
第一个参数sender应包含调用委托的那个类的实例。假如一个订阅者方法注册了多个事件,这个参数就尤其有用。例如,假定两个Thermostat实例都订阅了heater.OnTemperatureChanged事件。在这种情况下,任何一个Thermostat实例都可能触发对heater.OnTemperatureChanged的调用。为了判断具体是哪个Thermostat实例触发了事件,要在Heater.OnTemperatureChanged()内部利用sender参数进行判断。如果事件是静态的,就无法做这种判断,所以要为sender传递null值作为实参。
第二个参数TEventArgse是Thermostat.TemperatureArgs类型。关于TemperatureArgs,一个重点在于它是从System.EventArgs派生的。(事实上,一直到.NET Framework 4.5,都通过一个泛型约束来强制从System.EventArgs派生。)System.EventArgs唯一重要的属性是Empty,它用于指出不存在事件数据。然而,从System.EventArgs派生出TemperatureArgs时添加了一个额外的属性,名为NewTemperature,用于将温度从自动调温器传递给订阅者。
这里简单总结一下事件的编码规范:第一个参数sender是object类型的,它包含对调用委托的那个对象的一个引用(静态事件则为null)。第二个参数是System.EventArgs类型的(或者从System.EventArgs派生,但包含了事件的附加数据)。调用委托的方式和以前几乎完全一样,只是要提供附加的参数。下面的代码展示了一个例子。


public class Thermostat
{
...
public float CurrentTemperature
{
get{return _CurrentTemperature;}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
// If there are any subscribers
// then notify them of changes in
// temperature
if(OnTemperatureChange != null)
{
// Call subscribers
OnTemperatureChange(
this,new TemperatureArgs(value) );
}
}
}
}
private float _CurrentTemperature;
}


通常将sender指定为容器类(this),因为它是唯一一个能为事件调用委托的类。

在这个例子中,订阅者可以将sender参数转型为Thermostat,并以那种方式访问当前温度,或者通过TemperatureArgs实例来访问。然而,Thermostat实例上的当前温度可能由一个不同的线程改变。在由于状态改变而发生事件的时候,连同新值传递前一个值是常见的编程模式,它可以控制哪些状态变化是允许的。

这里给出一个规范:

①要在调用委托前检查它的值不为null。

②不要为非静态事件的sender传递null值。

③要为静态事件的sender传递null值。

④不要为eventArgs传递null值。

⑤要为事件使用EventHandler委托类型。

⑥要为TEventArgs使用System.EventArgs类型或者它的派生类型。

⑦考虑使用System.EventArgs的子类作为事件的实参类型(TEventArgs),除非完全确定事件永远不需要携带任何数据。

本节完

前面三个章节的笔记(C#高级特性中的委托与事件)主要是根据C#本质论来做的,因为翻译的比较好,看起来比较放心一些,本人的英语水平有限,完全的不看中文的东西去看原书的话还是会有一些困难,我相信大部分学习的读者也和我差不多。。但是现在国内把一些C#好的著作拿来,然后再翻译成中文出版的书籍大部分水平都不高,说难听点翻译简直就是烂透了。比如C#5.0 In A NutShell,这本书其实写的非常好,但是被中国水利水电出版社搞砸了。看的我心惊肉跳的,实在不行,我从网上下载了一个英文原版的,就随便和其中的几个章节对了一下,这翻译简直特么毁三观!翻译这本书的人真的是可以去死了。 且不说翻译的水平高不高,关键翻译的态度就是一个问题,翻译这本书的人简直就是抱着玩儿的态度,太不负责任了,真的是没有一点公德心,连带的,我建议大家以后还是不要买中国水利水电出版社的任何书籍了,完全是坑。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: