前面三章中依次讲了C# 委托的基本概念、C# Action和Func委托和C# 委托之匿名方法和lambda表达式,基于以上内容,来说说事件
事件基于委托,为委托提供了一种发布/订阅机制。在Windows应用程序中,Button类提供了Click事件,这类事件就是委托。
在本章讲述中,采用示例GameDealer类和Consumer类来讲述事件,GameDealer类提供一个新游戏发布时的触发事件,Consumer类订阅该事件,获得新游戏的通知。
一、事件发布程序
GameDealer类基于事件提供一个订阅,GameDealer类用event关键字定义了类型为EventHandler<GameInfoEventArgs>的NewGameInfo事件。在NewGame方法中,触发NewGameInfo事件(注:?.是C#6.0后新增的空值传播运算符)
public class GameInfoEventArgs : EventArgs
{
public GameInfoEventArgs(string game)
{
Game = game;
}
public string Game { get; }
}
public class GameDealer
{
public event EventHandler<GameInfoEventArgs> NewGameInfo;
public void NewGame(string game)
{
Console.WriteLine($"GameDealer, new game {game}");
NewGameInfo?.Invoke(this, new GameInfoEventArgs(game));
}
}
GameDealer类提供了EventHandler<GameInfoEventArgs>的NewGameInfo事件。作为约定,事件一般使用带两个参数的方法,其中一个参数是一个事件发送者对象,第二个参数是事件的相关信息,随事件类型而改变。
public event EventHandler<GameInfoEventArgs> NewGameInfo;
泛型委托EventHandler<TEventArgs>,返回void,接受两个参数,分别是Object和T。
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
二、事件侦听器
Consumer类用作事件侦听器,这个类订阅了GameDealer类的事件,并定义了NewGameIsHere方法,该方法符合EventHandler<GameInfoEventArgs>委托的要求,委托的参数类型是object和GameInfoEventArgs:
public class Consumer
{
private string _name;
public Consumer(string name)
{
_name = name;
}
public void NewGameIsHere(object sender, GameInfoEventArgs e)
{
Console.WriteLine($"{_name}: Game {e.Game} is new");
}
}
现在连接事件发布程序和订阅器,使用GameDealer类的NewGameInfo事件,通过“+=”创建一个订阅。消费者Zhang San订阅了这个事件,接着消费者Li Si也订阅了这个事件,然后Zhang San通过“-=”取消了订阅:
static void Main(string[] args)
{
var dealer = new GameDealer();
var zhangSan = new Consumer("Zhang San");
dealer.NewGameInfo += zhangSan.NewGameIsHere;
dealer.NewGame("Hello Game");
var liSi = new Consumer("Li Si");
dealer.NewGameInfo += liSi.NewGameIsHere;
dealer.NewGame("Surprising Game");
dealer.NewGameInfo -= zhangSan.NewGameIsHere;
dealer.NewGame("Laughing Out Loud Game");
Console.ReadKey();
}
运行程序,新游戏Hello Game发布,Zhang San得到了通知,之后Li Si也注册了该订阅,所以Zhang San 和Li Si都得到了Surprising Game的通知。接着Zhang San取消的订阅,所以只有Li Si得到了通知:
三、弱事件
通过事件,可以直接发布程序和侦听器,但是如果后面不再使用侦听器,那么垃圾回收器就不能清空侦听器占用的内存,因为发布程序依然有一个引用,会针对侦听器触发事件。如上面的例子中,如果Li Si这个对象在程序中一直被使用,但是后面的代码已经不需要侦听器了,而Li Si又没有被标记为disposed,那么垃圾回收器就永远不会清空侦听器占用的内存。
可以通过弱事件模式,把WeakEventManager作为发布程序和侦听器之间的中间来解决这个问题。
WeakEventManager<T>在System.Windows程序集中,需要添加引用WindowsBase.dll:
使用弱事件,不需要改变事件发布器,只需修改使用者,使其实现接口IWeakEventListener。这个接口定义了方法ReceiveWeakEvent,在事件触发时,会在弱事件管理器中调用该方法。该方法的实现充当代理,调用NewGameInfo:
public class Consumer: IWeakEventListener
{
private string _name;
public Consumer(string name)
{
_name = name;
}
public void NewGameIsHere(object sender, GameInfoEventArgs e)
{
Console.WriteLine($"{_name}: Game {e.Game} is new");
}
bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
NewGameIsHere(sender, e as GameInfoEventArgs);
return true;
}
}
在Main方法中,连接发布器和侦听器,使用WeakEventManager<TEventSource, TEventArgs> 类的静态AddHandler和RemoveHandler方法建立连接:
static void Main(string[] args)
{
var dealer = new GameDealer();
var zhangSan = new Consumer("Zhang San");
dealer.NewGameInfo += zhangSan.NewCarIsHere;
WeakEventManager<GameDealer, GameInfoEventArgs>.AddHandler(dealer, "NewGameInfo", zhangSan.NewGameIsHere);
dealer.NewGame("Hello Game");
var liSi = new Consumer("Li Si");
WeakEventManager<GameDealer, GameInfoEventArgs>.AddHandler(dealer, "NewGameInfo", liSi.NewGameIsHere);
dealer.NewGame("Surprising Game");
WeakEventManager<GameDealer, GameInfoEventArgs>.RemoveHandler(dealer, "NewGameInfo", zhangSan.NewGameIsHere);
dealer.NewGame("Laughing Out Loud Game");
Console.ReadKey();
}
更多关于弱事件的详情可以看这个文章,指路:译文:C#中的弱事件(Weak Events in C#)