上一篇随笔简单介绍了在F#中事件的有趣特性,即可组合的一等公民,在我们处理事件时就拥有了更好的灵活性。在里面的例子中可以看到如何订阅一个事件,包括在C#/VB.NET中定义的标准.NET事件。本文将介绍如何在F#中发布(创建)和订阅自定义事件。
在C#中发布和订阅事件
在此之前,先来看看如何在C#中发布一个事件,这里将只考虑标准的.NET事件(即微软推荐的事件定义方式)。这个过程涉及两个重要的类型:EventArgs,作为包含事件数据的类的基类;EventHandler<TEventArgs>,表示将处理事件的方法。比如下面的例子:
public class KeyEventArgs : EventArgs
{
private readonly char keyChar;
public KeyEventArgs(char keyChar)
{
this.keyChar = keyChar;
}
public char KeyChar
{
get { return keyChar; }
}
}
public class KeyInputMonitor
{
public event EventHandler<KeyEventArgs> KeyDown;
protected virtual void OnKeyDown(KeyEventArgs e)
{
EventHandler<KeyEventArgs> temp = KeyDown;
if (temp != null)
{
temp(this, e);
}
}
public void SimulateKeyDown(char key)
{
KeyEventArgs e = new KeyEventArgs(key);
OnKeyDown(e);
}
}
internal class UsingCustomEvent
{
private static void Main()
{
KeyInputMonitor monitor = new KeyInputMonitor();
monitor.KeyDown += new EventHandler<KeyEventArgs>(monitor_KeyDown);
monitor.SimulateKeyDown('A');
monitor.SimulateKeyDown('V');
}
static void monitor_KeyDown(object sender, KeyEventArgs e)
{
Console.WriteLine(e.KeyChar);
}
}
这里通过KeyEventArgs类来封装事件所包含的数据,然后以泛型EventHandler委托发布KeyDown事件,最后定义SimulateKeyDown方法来触发事件。在类型外部(这里是UsingCustomEvent类)就可以订阅该事件。
使用Event.create方法创建事件
事件的发布-订阅过程大致如此,理解了这个过程就比较容易理解F#的定义方式了。这里还是要用到那个Event模块,它的create方法将帮助我们创建事件,其签名信息为:
val it : (unit -> ('a -> unit) * IEvent<'a>)
该方法的返回值是一个tuple,一个用于触发事件的方法,另一个则是事件本身,就像前面例子中的SimulateKeyDown方法和KeyDown事件。比如:
let fire, event = Event.create()
event.Add(printfn "Fired %d")
fire(25) // Fired 25
val fire : (int -> unit)
val event : IEvent<int>
这里我们可以再一次见识类型推导的威力。不过问题是,以这样的方式创建事件后,事件是局部的,如何将事件封装在类中呢?
在F#中发布和订阅事件
现在将尝试在F#重写上面的C#示例。
open System
type KeyEventArgs(keyChar: char) =
inherit EventArgs()
member this.KeyChar = keyChar
type KeyInputMonitor() =
let (keyDownFire, keyDownEvent: IEvent<KeyEventArgs>) = Event.create()
member this.KeyDown = keyDownEvent
member this.SimulateKeyDown key =
let args = new KeyEventArgs(key)
keyDownFire args
let monitor = new KeyInputMonitor()
monitor.KeyDown.Add(fun args -> printfn "%A" args.KeyChar)
monitor.SimulateKeyDown 'A'
monitor.SimulateKeyDown 'V'
这里,在KeyInputMonitor发布KeyDown事件后,之后订阅和触发过程跟C#的例子很接近了。现在考虑在C#中使用F#中发布的事件。
发布标准的.NET的事件
现在我们在C#项目中引用上面创建的F#类,并尝试以常规的方式使用它的事件,就会发现难以使用。原因是它的KeyDown事件是IEvent类型的,而不是常规的委托类型。这个问题解决起来不难:
open System
type KeyEventArgs(keyChar: char) =
inherit EventArgs()
member this.KeyChar = keyChar
type KeyDownEventHandler = delegate of obj * KeyEventArgs -> unit
type KeyInputMonitor() =
let keyDownEvent = new Event<KeyDownEventHandler, KeyEventArgs>()
[<CLIEvent>]
member this.KeyDown = keyDownEvent.Publish
member this.SimulateKeyDown(key) =
keyDownEvent.Trigger(this, new KeyEventArgs(key))
let monitor = new KeyInputMonitor()
monitor.KeyDown.Add(fun args -> printfn "%A" args.KeyChar)
monitor.SimulateKeyDown 'A'
monitor.SimulateKeyDown 'V'
这里定义了一个委托KeyDownEventHandler,并且在创建事件的时候创建了一个Event类型的实例,而不是Event.create方法,更重要的是对事件属性添加了attribute:CLIEvent,这个attribute将使得这个事件属性编译为标准的.NET事件。现在来看,一方面在F#中使用该事件时与前面相同,另一方面在C#中使用时:
internal class UsingFsEvent
{
private static void Main()
{
FsLib.KeyInputMonitor monitor = new FsLib.KeyInputMonitor();
monitor.KeyDown += new FsLib.KeyDownEventHandler(monitor_KeyDown);
monitor.SimulateKeyDown('A');
monitor.SimulateKeyDown('V');
}
static void monitor_KeyDown(object sender, FsLib.KeyEventArgs e)
{
Console.WriteLine(e.KeyChar);
}
}
这个也与我们在C#中的使用习惯相同,这样在F#发布的事件就可以兼容于其它.NET语言了。
小结
本文首先介绍了如何使用Event.create方法创建新的事件,然后在此基础上讨论了如何发布和订阅事件,这样可以更符合我们的编码习惯。不过这种方式发布的事件在C#等其它.NET语言中却难以使用,所以最后介绍了如何发布标准的.NET事件,这样就可以兼容于F#和其它的.NET语言了。
参考
F# First Class Events – Creating Events
How to create .NET-compatible events in F#
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2009/08/21/composable-events-in-fsharp-part2.html,如需转载请自行联系原作者。