C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

9.4 事件

事件基于委托,为委托提供了一种发布/订阅机制。在.NET架构内到处都能看到事件。在Windows应用程序中,Button类提供了Click事件。这类事件就是委托。触发Click事件时调用的处理程序方法需要得到定义,而其参数由委托类型定义。

在本节的示例代码中,事件用于连接CarDealer类和Consumer类。CarDealer类提供了一个新车到达时触发的事件。Consumer类订阅该事件,以获得新车到达的通知。

9.4.1 事件发布程序

我们从CarDealer类开始介绍,它基于事件提供一个订阅。CarDealer类用event关键字定义了类型为EventHandler<CarInfoEventArgs>的NewCarInfo事件。在NewCar()方法中,通过调用RaiseNewCarInfo方法触发NewCarInfo事件。这个方法的实现确认委托是否为空,如果不为空,就引发事件(代码文件EventSample/CarDealer.cs):

        using static System.Console;
        using System;
        namespace Wrox.ProCSharp.Delegates
        {
          public class CarInfoEventArgs: EventArgs
          {
            public CarInfoEventArgs(string car)
            {
              Car = car;
            }
            public string Car { get; }
          }
          public class CarDealer
          {
            public event EventHandler<CarInfoEventArgs> NewCarInfo;
            public void NewCar(string car)
            {
              WriteLine($"CarDealer, new car {car}");
              NewCarInfo? .Invoke(this, new CarInfoEventArgs(car));
            }
          }
        }

注意:前面例子中使用的空传播运算符.?是C# 6新增的运算符。这个运算符的讨论参见第8章。

CarDealer类提供了EventHandler<CarInfoEventArgs>类型的NewCarInfo事件。作为一个约定,事件一般使用带两个参数的方法;其中第一个参数是一个对象,包含事件的发送者,第二个参数提供了事件的相关信息。第二个参数随不同的事件类型而改变。.NET 1.0为所有不同数据类型的事件定义了几百个委托。有了泛型委托EventHandler<T>后,就不再需要委托了。EventHandler<TEventArgs>定义了一个处理程序,它返回void,接受两个参数。对于EventHandler<TEventArgs>,第一个参数必须是object类型,第二个参数是T类型。EventHandler<TEventArgs>还定义了一个关于T的约束;它必须派生自基类EventArgs, CarInfoEventArgs就派生自基类EventArgs:

        public event EventHandler<CarInfoEventArgs> NewCarInfo;

委托EventHandler<TEventArgs>的定义如下:

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

在一行上定义事件是C#的简化记法。编译器会创建一个EventHandler<CarInfoEventArgs>委托类型的变量,并添加方法,以便从委托中订阅和取消订阅。该简化记法的较长形式如下所示。这非常类似于自动属性和完整属性之间的关系。对于事件,使用add和remove关键字添加和删除委托的处理程序:

        private EventHandler<CarInfoEventArgs> newCarInfo;
        public event EventHandler<CarInfoEventArgs> NewCarInfo
        {
          add
          {
            newCarInfo += value;
          }
          remove
          {
            newCarInfo -= value;
          }
        }

注意:如果不仅需要添加和删除事件处理程序,定义事件的长记法就很有用,例如,需要为多个线程访问添加同步操作。WPF控件使用长记法给事件添加冒泡和隧道功能。事件的冒泡和隧道详见第29章。

CarDealer类通过调用委托的RaiseNewCarInfo方法触发事件。使用委托NewCarInfo和花括号可以调用给事件订阅的所有处理程序。注意,与之前的多播委托一样,方法的调用顺序无法保证。为了更多地控制处理程序的调用,可以使用Delegate类的GetInvocationList()方法,访问委托列表中的每一项,并独立地调用每个方法,如上所示。

        NewCarInfo? .Invoke(this, new CarInfoEventArgs(car));

触发事件是只包含一行代码的程序。然而,这只是C# 6的功能。在C# 6版本之前,触发事件会更复杂。这是C# 6之前实现的相同功能。在触发事件之前,需要检查事件是否为空。因为在进行null检查和触发事件之间,可以使用另一个线程把事件设置为null,所以使用一个局部变量,如下所示:

        EventHandler<CarInfoEventArgs> newCarInfo = NewCarInfo;
        if (newCarInfo ! = null)
        {
          newCarInfo(this, new CarInfoEventArgs(car));
        }

在C # 6中,所有这一切都可以使用null传播运算符和一个代码行取代,如前所示。

在触发事件之前,需要检查委托NewCarInfo是否不为空。如果没有订阅处理程序,委托就为空:

        protected virtual void RaiseNewCarInfo(string car)
        {
          NewCarInfo? .Invoke(this, new CarInfoEventArgs(car));
        }

9.4.2 事件侦听器

Consumer类用作事件侦听器。这个类订阅了CarDealer类的事件,并定义了NewCarIsHere方法,该方法满足EventHandler<CarInfoEventArgs>委托的要求,该委托的参数类型是object和CarInfoEventArgs(代码文件EventsSample/Consumer.cs):

        using static System.Console;
        namespace Wrox.ProCSharp.Delegates
        {
          public class Consumer
          {
            private string _name;
            public Consumer(string name)
            {
              _name = name;
            }
            public void NewCarIsHere(object sender, CarInfoEventArgs e)
            {
              WriteLine($"{_name}: car {e.Car} is new");
            }
          }
        }

现在需要连接事件发布程序和订阅器。为此使用CarDealer类的NewCarInfo事件,通过“+=”创建一个订阅。消费者michael(变量)订阅了事件,接着消费者sebastian(变量)也订阅了事件,然后michael(变量)通过“-=”取消了订阅(代码文件EventsSample/Program.cs)。

        namespace Wrox.ProCSharp.Delegates
        {
          class Program
          {
            static void Main()
            {
              var dealer = new CarDealer();
              var daniel = new Consumer("Daniel");
              dealer.NewCarInfo += michael.NewCarIsHere;
              dealer.NewCar("Mercedes");
              var sebastian = new Consumer("Sebastian");
              dealer.NewCarInfo += sebastian.NewCarIsHere;
              dealer.NewCar("Ferrari");
              dealer.NewCarInfo -= sebastian.NewCarIsHere;
              dealer.NewCar("Red Bull Racing");
            }
          }
        }

运行应用程序,一辆Mercedes汽车到达,Daniel得到了通知。因为之后Sebastian也注册了该订阅,所以Daniel和Sebastian都获得了新款Ferrari汽车的通知。接着Sebastian取消了订阅,所以只有Daniel获得了Red Bull汽车的通知:

        CarDealer, new car Mercedes
        Daniel: car Mercedes is new
        CarDealer, new car Ferrari
        Daniel: car Ferrari is new
        Sebastian: car Ferrari is new
        CarDealer, new car Red Bull Racing
        Daniel: car Red Bull is new

9.4.3 弱事件

通过事件,可直接连接发布程序和侦听器。但是,垃圾回收方面存在问题。例如,如果不再直接引用侦听器,发布程序就仍有一个引用。垃圾回收器不能清空侦听器占用的内存,因为发布程序仍保有一个引用,会针对侦听器触发事件。

这种强连接可以通过弱事件模式来解决,即使用WeakEventManager作为发布程序和侦听器之间的中介。

前面的示例把CarDealer作为发布程序,把Consumer作为侦听器,本节将修改这个示例,以使用弱事件模式。

WeakEventManager < T >在System.Windows程序集中定义,不属于.NET Core。这个示例用.NET Framework 4.6控制台应用程序完成,不运行在其他平台上。

注意:动态创建订阅器时,为了避免出现资源泄露,必须特别留意事件。也就是说,需要在订阅器离开作用域(不再需要它)之前,确保取消对事件的订阅,或者使用弱事件。事件常常是应用程序中内存泄露的一个原因,因为订阅器有长时间存在的作用域,所以源代码也不能被垃圾回收。

使用弱事件,就不需要改变事件发布器(在示例代码CarDealer类中)。无论使用紧密耦合的事件还是弱事件都没有关系,其实现是一样的。不同的是使用者的实现。使用者需要实现接口IWeakEventListener。这个接口定义了方法ReceiveWeakEvent,在事件触发时会在弱事件管理器中调用该方法。该方法的实现充当代理,调用方法NewCarIsHere(代码文件WeakEvents/Consumer.cs):

        using System;
        using static System.Console;
        using System.Windows;
        namespace Wrox.ProCSharp.Delegates
        {
          public class Consumer: IWeakEventListener
          {
            private string _name;
            public Consumer(string name)
            {
              this._name = name;
            }
            public void NewCarIsHere(object sender, CarInfoEventArgs e)
            {
              WriteLine("\{_name}: car \{e.Car} is new");
            }
            bool IWeakEventListener.ReceiveWeakEvent(Type managerType,
              object sender, EventArgs e)
            {
              NewCarIsHere(sender, e as CarInfoEventArgs);
              return true;
            }
          }
        }

在Main方法中,连接发布器和监听器,目前使用WeakEventManager < TEventSource, TEventArgs>类的静态AddHandler和RemoveHandler方法建立连接(代码文件WeakEventsSample/Program.cs):

        var dealer = new CarDealer();
        var daniel = new Consumer("Daniel");
        WeakEventManager<CarDealer, CarInfoEventArgs>.AddHandler(dealer,
            "NewCarInfo", daniel.NewCarIsHere);
        dealer.NewCar("Mercedes");
        var sebastian = new Consumer("Sebastian");
        WeakEventManager<CarDealer, CarInfoEventArgs>.AddHandler(dealer,
            "NewCarInfo", sebastian.NewCarIsHere);
        dealer.NewCar("Ferrari");
        WeakEventManager<CarDealer, CarInfoEventArgs>.RemoveHandler(dealer,
            "NewCarInfo", sebastian.NewCarIsHere);
        dealer.NewCar("Red Bull Racing");