为了方便 FSharp 用户以更加函数式的方式与 C# 的事件进行交互,F# 提供了 IEvent
类型。IEvent
继承了 IObservable
,方便 F# 用户使用 Rx.Net 来处理事件流,另一方面,IEvent
还继承了 IDelegateEvent
方便用户将 F# 函数注册为事件的监听器。
除此之外,F# 还会将系统类库、第三方类库中的 CLIEvent
转换成对应类型的 IEvent
,这个过程自动地发生在编译的过程中。接下来我将用一段非常简单的代码(代码来源)来演示其中的玄机。
type MyClassWithCLIEvent() =
let event1 = new Event<_>()
[<CLIEvent>]
member this.Event1 = event1.Publish (1)
member this.TestEvent(arg) =
event1.Trigger(this, arg)
let classWithEvent = new MyClassWithCLIEvent()
<@ classWithEvent.Event1 @> (2)
-
声明一个
CLIEvent
-
使用 Quotation 获取 F# 表达式
Quotations.Expr<IEvent<Handler<MyClassWithCLIEvent * obj>,
(MyClassWithCLIEvent * obj)>> =
Call (None, CreateEvent, (1)
[Lambda (eventDelegate, Call (Some (PropertyGet (None, classWithEvent, [])), add_Event1, [eventDelegate])), (2)
Lambda (eventDelegate,
Call (Some (PropertyGet (None, classWithEvent, [])), remove_Event1, [eventDelegate])), (3)
Lambda (callback,
NewDelegate (FSharpHandler`1, delegateArg0, delegateArg1,
Application (Application (callback, delegateArg0),
delegateArg1)))]) (4)
-
调用
RuntimeHelper.CreateEvent()
-
CreateEvent
的第一个参数:一个 lambda,可以将事件委托添加为classWithEvent.Event1
的监听器 -
CreateEvent
的第二个参数:一个 lambda,将事件委托从classWithEvent.Event1
的监听器中移除 -
CreateEvent
的第三个参数:一个 lambda,将一个 F# 函数转换为Event1
的委托类型
可以看到,当我们使用 F# 访问 CLIEvent
的时候,编译器会生成一个 IEvent
对象,并在 IEvent
的事件处理函数闭包中捕获 CLIEvent
所属的实例。编译器的这种行为有时候就让一些奇怪的代码正常运行:
let nullObj = Unchecked.defaultof<MyClassWithCLIEvent>
nullObj.Event1.GetType() // 一切正常
nullObj.Event1.Add(ignore) // NullReferenceException
在 F# 中仅访问 IEvent
类型的属性不会出现空引用异常,只有在注册、移除事件监听器的时候才会触发。