代码改变世界

F#中的事件(下)

2009-08-21 00:56 by Anders Cui, ... 阅读, ... 评论, 收藏, 编辑

上一篇随笔简单介绍了在F#中事件的有趣特性,即可组合的一等公民,在我们处理事件时就拥有了更好的灵活性。在里面的例子中可以看到如何订阅一个事件,包括在C#/VB.NET中定义的标准.NET事件。本文将介绍如何在F#中发布(创建)和订阅自定义事件。

在C#中发布和订阅事件

在此之前,先来看看如何在C#中发布一个事件,这里将只考虑标准的.NET事件(即微软推荐的事件定义方式)。这个过程涉及两个重要的类型:EventArgs,作为包含事件数据的类的基类;EventHandler<TEventArgs>,表示将处理事件的方法。比如下面的例子:

C# Code - 发布和订阅事件
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方法将帮助我们创建事件,其签名信息为:

F# Signature Info
val it : (unit -> ('a -> unit) * IEvent<'a>)


该方法的返回值是一个tuple,一个用于触发事件的方法,另一个则是事件本身,就像前面例子中的SimulateKeyDown方法和KeyDown事件。比如:

F# Code - 使用Event.create方法创建事件
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#示例。

F# Code - 发布和订阅事件
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类型的,而不是常规的委托类型。这个问题解决起来不难:

F# Code - 发布标准的.NET事件
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#中使用时:

C# Code - 使用在F#中发布的事件
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