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

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

事件

  • 委托本身又是一个更大的模式(pattern)的基本单位,这个模式称为publish-subscribe(发布——订阅)。委托的使用及其对publish-subscribe模式的支持是本章的重点。本章描述的所有内容几乎都可以单独使用委托来实现。然而,本章所着眼的事件构造提供了额外的“封装性”,使publish-subscribe模式更容易实现,更不容易出错。
  • 一个委托值是可以引用一系列方法的,这些方法将顺序调用。这样的委托称为多播委托(multicast delegate)。这样一来,单一事件(比如对象状态的改变)的通知就可以发布给多个订阅者。
  • 虽然事件在C# 1.0中就有了,但C# 2.0对泛型的引入极大地改变了编码规范,因为使用泛型委托数据类型意味着不再需要为每种可能的事件签名声明一个委托。所以,本章的最低起点是C# 2.0。但是,仍在使用C# 1.0的读者也不是不能使用事件,只是必须声明自己的委托数据类型。

使用多播委托来编码Observer模式

  • 来考虑一个温度控制的例子。在这个假想的情形中,一个加热器(Heater)和一个冷却器(Cooler)连接到同一个自动调温器。为了控制加热器和冷却器的打开和关闭,要向它们通知温度的变化。自动调温器将温度的变化发布给多个订阅者——也就是加热器和冷却器。
 class Cooller
    {
        private float _temprature;
        public float Temprature { get { return _temprature; } set { _temprature = value; } }
        public Cooller(float newTemprature)
        {
            Temprature = newTemprature;
        }

        public void OnTempratureChanged(float newTemperature)
        {
            if (newTemperature>Temprature)
            {
                Console.WriteLine("Coller:Off");
            }
            else
            {
                Console.WriteLine("Coller:On");
            }
        }
    }

    class Heater
    {
        private float _temprature;
        public float Temprature { get { return _temprature; } set { _temprature = value; } }
        public Heater(float newTemprature)
        {
            Temprature = newTemprature;
        }

        public void OnTempratureChanged(float newTemperature)
        {
            if (newTemperature>Temprature)
            {
                Console.WriteLine("Heater:On");
            }
            else
            {
                Console.WriteLine("Heater:Off");
            }
        } 
    }
除了温度比较,两个类几乎完全一致,事实上,如果在OnTempretureChanged方法中使用对一个比较方法的委托,两个类还可以再减少一个。每个类都存储了启动设备所需的温度。此外,两个类都提供了OnTemperatureChanged()方法。调用OnTemperatureChanged()方法的目的是向Heater和Cooler类指出温度已发生改变。在方法的实现中,用newTemperature同存储好的触发温度进行比较,从而决定是否让设备启动。
两个OnTemperatureChanged()方法都是订阅者方法。作为订阅者方法,很重要的一点在于,它们的参数和返回类型必须与来自Thermostat类的委托匹配,Thermostat类的详情将在下一节讨论。

- 定义发布者:Thermostat类负责向heater和cooler对象实例报告温度变化。下例展示了Thermostat类。

public class Thermostat
{
// Define the event publisher
public Action<float> OnTemperatureChange { get; set; }
public float CurrentTemperature
{
get{return _CurrentTemperature;}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
}
}
}
private float _CurrentTemperature;
}

Thermostat包含一个名为OnTemperatureChange的属性,它具有Action<float>委托类型。OnTemperatureChange存储了订阅者列表。注意,只需一个委托字段即可存储所有订阅者。换言之,来自同一个发布者的温度变化通知会同时被Cooler和Heater类接收。
Thermostat的最后一个成员是CurrentTemperature属性。它负责设置和获取由Thermostat类报告的当前温度值。
连接发布者和订阅者:

class Program
{
public static void Main()
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temperature;
thermostat.OnTemperatureChange +=
heater.OnTemperatureChanged;
thermostat.OnTemperatureChange +=
cooler.OnTemperatureChanged;
Console.Write("Enter temperature: ");
temperature = Console.ReadLine();
thermostat.CurrentTemperature = int.Parse(temperature);
}
}

上述代码使用+=操作符来直接赋值,向OnTemperatureChange委托注册了两个订阅者,即heater.OnTemperatureChanged和cooler.OnTemperatureChanged。
通过获取用户输入的温度值,可以设置thermostat(自动调温器)的CurrentTemperature (当前温度)。然而,目前还没有写任何代码将温度变化发布给订阅者。

  • 调用委托:Thermostat类的CurrentTemperature属性每次发生变化时,你都希望调用委托来通知订阅者(heater和cooler)温度的变化。为此,需要修改CurrentTemperature属性来保存新值,并向每个订阅者发出一个通知:
public class Thermostat
{
...
public float CurrentTemperature
{
get{return _CurrentTemperature;}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
// INCOMPLETE: Check for null needed
// Call subscribers
OnTemperatureChange(value);
}
}
}
private float _CurrentTemperature;
}

现在,对CurrentTemperature的赋值包含了一些特殊的逻辑,可以向订阅者通知CurrentTemperature发生的变化。为了向所有订阅者发出通知,只需执行一个简单的C#语句,即OnTemperatureChange(value);。这个语句将温度的变化发布给cooler和heater对象。在此,只需执行一个调用,即可向多个订阅者发出通知——这正是将委托更明确地称为“多播委托”的原因。
上上面的代码中,遗漏了事件发布代码的一个重要部分。假如当前没有订阅者注册接收通知,则OnTemperatureChange为null,执行OnTemperatureChange(value)语句就会引发一个NullReferenceException异常。为了避免这个问题,有必要在触发事件之前检查null值。下面的代码清单演示了会如何检查null值:

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
Action<float> localOnChange =
OnTemperatureChange;
if(localOnChange != null)
{
// Call subscribers
localOnChange(value);
}
}
}
}
private float _CurrentTemperature;
}

我们并不是一开始就检查null值,而是首先将OnTemperatureChange赋给另一个委托变量localOnChange。这个简单的修改可确保在检查null值和发送通知之间,假如所有OnTemperatureChange订阅者都(由一个不同的线程)移除,那么不会引发NullReferenceException异常。关于线程的讨论我们会在后面的笔记中来继续完善这个例子的解释。现在先给出规范:要在调用委托前检查它的值是不是null值。

  • 提示:将-=操作符赋值给一个委托会创建一个新的实例。既然委托是引用类型,那么肯定有人会觉得奇怪:为什么赋值给一个局部变量,再用那个局部变量就可以保证null检查的线程安全性?由于localOnChange指向的位置就是OnTemperatureChange指向的位置,所以很自然的结论就是:OnTemperatureChange中发生的任何变化都会在localOnChange中反映出来。但实情并非如此。事实上,对OnTemperatureChange -=<listener>的任何调用都不会从OnTemperatureChange删除一个委托而使它的委托比之前少一个。相反,会赋值一个全新的多播委托,原始的多播委托不受任何影响(localOnChange也指向那个原始的多播委托)。
  • 如前所述,由于订阅者可以由不同的线程从委托中增加或删除,所以在进行null值检查前有必要将委托引用复制到一个局部变量中。但是,这虽然能防止调用空委托,却不能防止所有可能的竞态条件。例如,一个线程进行复制,另一个线程将这个委托重置为null,然后原始线程可以调用委托的前一个值,借此通知一个已经不再在订阅者列表中的订阅者。在多线程程序中,订阅者应确保在这种情况下的健壮性。一个“过气”的订阅者随时都可能被调用。
  • 委托操作符:为了合并Thermostat例子中的两个订阅者,要使用+=操作符。这样会获取第一个委托,并将第二个委托添加到委托链中。第一个委托的方法返回后,会调用第二个委托。从委托链中删除委托,则要使用-=操作符,如下面代码清单所示:
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
Action<float> delegate1;
Action<float> delegate2;
Action<float> delegate3;
delegate1 = heater.OnTemperatureChanged;
delegate2 = cooler.OnTemperatureChanged;
Console.WriteLine("Invoke both delegates:");
delegate3 = delegate1;
delegate3 += delegate2;
delegate3(90);
Console.WriteLine("Invoke only delegate2");
delegate3 -= delegate1;
delegate3(30);
输出如下所示:
Invoke both delegates:
Heater: Off
Cooler: On
Invoke only delegate2
Cooler: Off

除此之外,还可以使用+和-操作符来合并委托,如下所示:

Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
Action<float> delegate1;
Action<float> delegate2;
Action<float> delegate3;
// Note: Use new Action(
// cooler.OnTemperatureChanged) for C# 1.0 syntax.
delegate1 = heater.OnTemperatureChanged;
delegate2 = cooler.OnTemperatureChanged;
Console.WriteLine("Combine delegates using + operator:");
delegate3 = delegate1 + delegate2;
delegate3(60);
Console.WriteLine("Uncombine delegates using - operator:");
delegate3 = delegate3 - delegate2;
delegate3(60);
输出和上面的是一样的。

使用赋值操作符会清除之前的所有订阅者,并允许用新订阅者替换它们。这是委托很容易让人犯错的一个设计,因为在本来应该使用+=操作符的时候,很容易就会错误地写成=。解决这个问题的良方是事件,详情将在稍后笔记中讲述。
应注意的是,无论+、-还是它们的复合赋值版本(+=和-=),在内部都是使用静态方法System.Delegate.Combine()和System.Delegate.Remove()来实现的。两个方法都获取delegate类型的两个参数(public static Delegate Combine(Delegate a, Delegate b);Remove类似,可以通过visual studio来查看System.Delegate.Combine()和Remove的定义)。第一个方法Combine()会连接两个参数,将两个委托的调用列表按顺序连接到一起。第二个方法Remove()则搜索由第一个参数指定的委托链,删除由第二个参数指定的委托。
对于Combine()方法,一个有趣的地方在于,它的两个参数都可以为null。如果其中任何一个参数为null,Combine()就返回非空的那个。如果两个都为null,则Combine()返回null。这就解释了为什么可以调用thermostat.OnTemperatureChange += heater.OnTemperatureChanged;而不引发异常(即使thermostat.OnTemperatureChange尚未赋值)。

  • 顺序调用:下图展示了上例中委托的调用顺序
    委托的调用顺序
    虽然代码中只包含一个简单的OnTemperatureChange()调用,但这个调用会广播给两个订阅者,使cooler和heater都会收到温度发生变化的通知。假如添加更多的订阅者,它们也会收到OnTemperatureChange()的通知。
    虽然一个OnTemperatureChange()调用造成每个订阅者都收到通知,但它们仍然是顺序调用的,而不是同时调用,因为它们全都在一个执行线程上调用。

  • 多播委托的内部工作机制:
    为了理解事件是如何工作的,你需要回顾第12章中我们第一次探讨System.Delegate类型的内部机制的部分。delegate关键字是派生自System.MulticastDelegate的一个类型的别名。System.MulticastDelegate则是从System.Delegate派生的,后者由一个对象引用(以满足非静态方法的需要)和一个方法引用构成。创建委托时,编译器自动使用System.MulticastDelegate类型而不是System.Delegate类型。MulticastDelegate类包含一个对象引用和一个方法引用,这和它的Delegate基类一样。但除此之外,它还包含对另一个System.MulticastDelegate对象的引用。
    向多播委托添加方法时,MulticastDelegate类会创建委托类型的一个新实例,在新实例中为新增的方法存储对象引用和方法引用,并在委托实例列表中添加新的委托实例作为下一项。实际上,MulticastDelegate类维护着一个Delegate对象链表。从概念上讲,可以下图那样表示Thermostat的例子。
    这里写图片描述
    调用多播委托时,链表中的委托实例会被顺序调用。通常,委托是按照它们添加时的顺序调用的,但CLI规范并未对此做出硬性规定,而且这个顺序可能被覆盖。所以,程序员不应依赖于一个特定的调用顺序。

  • 错误处理:错误处理凸显了顺序通知可能造成的问题。假如一个订阅者引发了异常,链中的后续订阅者就收不到通知。例如,假定修改Heater的OnTemperatureChanged()方法,使它引发异常,那么会发生什么?

class Program
{
public static void Main()
{
Thermostat thermostat = new Thermostat();
Heater heater = new Heater(60);
Cooler cooler = new Cooler(80);
string temperature;
thermostat.OnTemperatureChange +=
heater.OnTemperatureChanged;
// Using C# 3.0. Change to anonymous method
// if using C# 2.0
thermostat.OnTemperatureChange +=
(newTemperature) =>
{
throw new InvalidOperationException();
};
thermostat.OnTemperatureChange +=
cooler.OnTemperatureChanged;
Console.Write("Enter temperature: ");
temperature = Console.ReadLine();
thermostat.CurrentTemperature = int.Parse(temperature);
}
}

虽然cooler和heater已进行了订阅来接收消息,但Lambda表达式异常会使链发生中断,造成cooler对象收不到通知。为了避免这个问题,使所有订阅者都能收到通知(不管之前的订阅者有过什么行为),必须手动遍历订阅者列表,并单独调用它们。下列代码展示了需要在CurrentTemperature属性中进行的更新。

public class Thermostat
{
// Define the event publisher
public Action<float> OnTemperatureChange;
public float CurrentTemperature
{
get{return _CurrentTemperature;}
set
{
if (value != CurrentTemperature)
{
_CurrentTemperature = value;
if(OnTemperatureChange != null)
{
List<Exception> exceptionCollection =
new List<Exception>();
foreach (
Action<float> handler in
OnTemperatureChange.GetInvocationList())//这里是代码关键
{
try
{
handler(value);
}
catch (Exception exception)
{
exceptionCollection.Add(exception);
}}
if (exceptionCollection.Count > 0)
{
throw new AggregateException(
"There were exceptions thrown by OnTemperatureChange Event subscribers.",exceptionCollection);
}}}}}
private float _CurrentTemperature;
}
输出:
Enter temperature: 45
Heater: On
Error in the application
Cooler: Off

这个代码清单演示了你可以从委托的GetInvocationList()方法获得一份订阅者列表。枚举该列表中的每一项,可以返回给单独的订阅者。如果随后将每个订阅者调用都放到一个try/catch块中,就可以先处理好任何出错的情形,再继续循环迭代。在这个例子中,尽管委托侦听者(delegate listener)引发了一个异常,但cooler仍会接收到温度发生改变的通知。所有通知都发送完毕之后,代码清单13-9通过引发一个AggregateException来报告所有已发生的异常。AggregateException包装了一个异常集合。集合中的异常可以通过InnerExceptions属性访问。用这种方法,所有异常都得到报告,同时所有订阅者都不会错过通知。

本节完

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值