F# 编程/事件
F# : 事件 |
事件 允许对象通过一种同步消息传递的方式进行通信。事件仅仅是其他函数的钩子:对象向事件注册回调函数,这些回调函数将在某个对象触发事件时(如果触发)被执行。
例如,假设我们有一个可点击的按钮,它公开了一个名为 Click
的事件。我们可以向按钮的点击事件注册一段代码,例如 fun () -> printfn "I've been clicked!"
。当点击事件被触发时,它将执行我们注册的代码块。如果我们想要,可以向点击事件注册任意数量的回调函数——按钮不关心回调函数触发什么代码或注册了多少回调函数,它会盲目地执行连接到其点击事件的任何函数。
事件驱动编程在 GUI 代码中很自然,因为 GUI 往往由对用户输入做出反应和响应的控件组成。当然,事件在非 GUI 应用程序中也很有用。例如,如果我们有一个具有可变属性的对象,我们可能想要在这些属性发生变化时通知另一个对象。
事件是通过 F# 的 Event 类 创建和使用的。要创建事件,请使用 Event
构造函数,如下所示
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<string>()
member this.Name
with get() = _name
and set(value) = _name <- value
为了允许监听者连接到我们的事件,我们需要使用事件的 Publish
属性将 nameChanged
字段公开为公共成员
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
现在,任何对象都可以监听 person 方法上的变化。按照惯例和 微软的推荐,事件通常命名为 Verb
或 VerbPhrase
,以及添加时态,如 Verbed
和 Verbing
,以指示后事件和前事件。
向事件处理程序添加回调非常容易。每个事件处理程序都有类型 IEvent<'T>
,它公开了几个方法
val Add : event:('T -> unit) -> unit
- 将监听器函数连接到事件。事件触发时将调用监听器。
val AddHandler : 'del -> unit
- 将处理程序委托对象连接到事件。处理程序可以使用 RemoveHandler 稍后删除。事件触发时将调用监听器。
val RemoveHandler : 'del -> unit
- 从事件监听器存储中删除监听器委托。
这是一个示例程序
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
let p = new Person("Bob")
p.NameChanged.Add(fun () -> printfn "-- Name changed! New name: %s" p.Name)
printfn "Event handling is easy"
p.Name <- "Joe"
printfn "It handily decouples objects from one another"
p.Name <- "Moe"
p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")
printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"
printfn "The function NameChanged is invoked effortlessly."
此程序输出以下内容
Event handling is easy -- Name changed! New name: Joe It handily decouples objects from one another -- Name changed! New name: Moe It's also causes programs behave non-deterministically. -- Name changed! New name: Bo -- Another handler attached to NameChanged! The function NameChanged is invoked effortlessly.
- 注意: 当多个回调连接到单个事件时,它们将按照添加的顺序执行。但是,在实践中,您不应该编写依赖于事件以特定顺序触发的代码,因为这样做会导致函数之间产生复杂的依赖关系。事件驱动编程通常是非确定性的,并且本质上是有状态的,这有时与函数式编程的精神相矛盾。最好编写不修改状态,也不依赖于任何先前事件调用的回调函数。
上面的代码演示了如何使用 IEvent<'T>.add
方法。但是,有时我们需要删除回调。为此,我们需要使用 IEvent<'T>.AddHandler
和 IEvent<'T>.RemoveHandler
方法,以及 .NET 的内置 System.Delegate 类型。
函数 person.NameChanged.AddHandler
具有类型 val AddHandler : Handler<'T> -> unit
,其中 Handler<'T>
继承自 System.Delegate
。我们可以如下创建 Handler
的实例
type Person(name : string) =
let mutable _name = name;
let nameChanged = new Event<unit>() (* creates event *)
member this.NameChanged = nameChanged.Publish (* exposed event handler *)
member this.Name
with get() = _name
and set(value) =
_name <- value
nameChanged.Trigger() (* invokes event handler *)
let p = new Person("Bob")
let person_NameChanged =
new Handler<unit>(fun sender eventargs -> printfn "-- Name changed! New name: %s" p.Name)
p.NameChanged.AddHandler(person_NameChanged)
printfn "Event handling is easy"
p.Name <- "Joe"
printfn "It handily decouples objects from one another"
p.Name <- "Moe"
p.NameChanged.RemoveHandler(person_NameChanged)
p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")
printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"
printfn "The function NameChanged is invoked effortlessly."
此程序输出以下内容
Event handling is easy -- Name changed! New name: Joe It handily decouples objects from one another -- Name changed! New name: Moe It's also causes programs behave non-deterministically. -- Another handler attached to NameChanged! The function NameChanged is invoked effortlessly.
F# 的事件处理模型与 .NET 的其他部分略有不同。如果我们想将 F# 事件公开给 C# 或 VB.NET 等其他语言,我们可以使用 delegate
关键字定义一个自定义委托类型,该类型编译为 .NET 委托,例如
type NameChangingEventArgs(oldName : string, newName : string) =
inherit System.EventArgs()
member this.OldName = oldName
member this.NewName = newName
type NameChangingDelegate = delegate of obj * NameChangingEventArgs -> unit
约定 obj * NameChangingEventArgs
对应于 .NET 命名指南,建议所有事件都具有类型 val eventName : (sender : obj * e : #EventArgs) -> unit
。
尝试使用现有的 .NET WPF 事件和委托,例如 ClickEvent 和 RoutedEventHandler。使用引用这些库(PresentationCore PresentationFramework System.Xaml WindowsBase)创建 F# Windows 应用程序 .NET 项目。该程序将在窗口中显示一个按钮。单击按钮将显示按钮的内容作为字符串。
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System
[<EntryPoint>] [<STAThread>] // STAThread is Single-Threading-Apartment which is required by WPF
let main argv =
let b = new Button(Content="Button") // b is a Button with "Button" as content
let f(sender:obj)(e:RoutedEventArgs) = // (#3) f is a fun going to handle the Button.ClickEvent
// f signature must be curried, not tuple as governed by Delegate-RoutedEventHandler.
// that means f(sender:obj,e:RoutedEventArgs) will not work.
let b = sender:?>Button // sender will have Button-type. Convert it to Button into b.
MessageBox.Show(b.Content:?>string) // Retrieve the content of b which is obj.
// Convert it to string and display by <code>Messagebox.Show</code>
|> ignore // ignore the return because f-signature requires: obj->RoutedEventArgs->unit
let d = new RoutedEventHandler(f) // (#2) d will have type-RoutedEventHandler,
// RoutedEventHandler is a kind of delegate to handle Button.ClickEvent.
// The f must have signature governed by RoutedEventHandler.
b.AddHandler(Button.ClickEvent,d) // (#1) attach a RountedEventHandler-d for Button.ClickEvent
let w = new Window(Visibility=Visibility.Visible,Content=b) // create a window-w have a Button-b
// which will show the content of b when clicked
(new Application()).Run(w) // create new Application() running the Window-w.
b.AddHandler(Button.ClickEvent,d)
let d = new RoutedEventHandler(f)
let f(sender:obj)(e:RoutedEventArgs) = ....
void RoutedEventHandler(object sender, RoutedEventArgs e)
。所以 f 必须具有相同的签名。在 F# 中呈现签名是 (obj * RountedEventHandler) -> unit
事件可以轻松地将状态传递给回调。这是一个简单的程序,它以字符块的形式读取文件
open System
type SuperFileReader() =
let progressChanged = new Event<int>()
member this.ProgressChanged = progressChanged.Publish
member this.OpenFile (filename : string, charsPerBlock) =
use sr = new System.IO.StreamReader(filename)
let streamLength = int64 sr.BaseStream.Length
let sb = new System.Text.StringBuilder(int streamLength)
let charBuffer = Array.zeroCreate<char> charsPerBlock
let mutable oldProgress = 0
let mutable totalCharsRead = 0
progressChanged.Trigger(0)
while not sr.EndOfStream do
(* sr.ReadBlock returns number of characters read from stream *)
let charsRead = sr.ReadBlock(charBuffer, 0, charBuffer.Length)
totalCharsRead <- totalCharsRead + charsRead
(* appending chars read from buffer *)
sb.Append(charBuffer, 0, charsRead) |> ignore
let newProgress = int(decimal totalCharsRead / decimal streamLength * 100m)
if newProgress > oldProgress then
progressChanged.Trigger(newProgress) // passes newProgress as state to callbacks
oldProgress <- newProgress
sb.ToString()
let fileReader = new SuperFileReader()
fileReader.ProgressChanged.Add(fun percent ->
printfn "%i percent done..." percent)
let x = fileReader.OpenFile(@"C:\Test.txt", 50)
printfn "%s[...]" x.[0 .. if x.Length <= 100 then x.Length - 1 else 100]
此程序具有以下类型
type SuperFileReader =
class
new : unit -> SuperFileReader
member OpenFile : filename:string * charsToRead:int -> string
member ProgressChanged : IEvent<int>
end
val fileReader : SuperFileReader
val x : string
由于我们的事件具有类型 IEvent<int>
,我们可以将 int
数据作为状态传递给监听回调。此程序输出以下内容
0 percent done... 4 percent done... 9 percent done... 14 percent done... 19 percent done... 24 percent done... 29 percent done... 34 percent done... 39 percent done... 44 percent done... 49 percent done... 53 percent done... 58 percent done... 63 percent done... 68 percent done... 73 percent done... 78 percent done... 83 percent done... 88 percent done... 93 percent done... 98 percent done... 100 percent done... In computer programming, event-driven programming or event-based programming is a programming paradig{{typo help inline|reason=similar to parading|date=September 2022}}[...]
事件驱动编程中一个常见的习惯用法是前事件和后事件处理,以及取消事件的能力。取消需要事件处理程序和监听者之间的双向通信,我们可以通过使用 ref 单元格 或可变成员轻松实现
type Person(name : string) =
let mutable _name = name;
let nameChanging = new Event<string * bool ref>()
let nameChanged = new Event<unit>()
member this.NameChanging = nameChanging.Publish
member this.NameChanged = nameChanged.Publish
member this.Name
with get() = _name
and set(value) =
let cancelChange = ref false
nameChanging.Trigger(value, cancelChange)
if not !cancelChange then
_name <- value
nameChanged.Trigger()
let p = new Person("Bob")
p.NameChanging.Add(fun (name, cancel) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = name) exboyfriends then
printfn "-- No %s's allowed!" name
cancel := true
else
printfn "-- Name allowed")
p.NameChanged.Add(fun () ->
printfn "-- Name changed to %s" p.Name)
let tryChangeName newName =
printfn "Attempting to change name to '%s'" newName
p.Name <- newName
tryChangeName "Joe"
tryChangeName "Moe"
tryChangeName "Jon"
tryChangeName "Thor"
此程序具有以下类型
type Person =
class
new : name:string -> Person
member Name : string
member NameChanged : IEvent<unit>
member NameChanging : IEvent<string * bool ref>
member Name : string with set
end
val p : Person
val tryChangeName : string -> unit
此程序输出以下内容
Attempting to change name to 'Joe' -- Name allowed -- Name changed to Joe Attempting to change name to 'Moe' -- Name allowed -- Name changed to Moe Attempting to change name to 'Jon' -- No Jon's allowed! Attempting to change name to 'Thor' -- Name allowed -- Name changed to Thor
如果我们需要将大量状态传递给监听者,那么建议将状态包装在对象中,如下所示
type NameChangingEventArgs(newName : string) =
inherit System.EventArgs()
let mutable cancel = false
member this.NewName = newName
member this.Cancel
with get() = cancel
and set(value) = cancel <- value
type Person(name : string) =
let mutable _name = name;
let nameChanging = new Event<NameChangingEventArgs>()
let nameChanged = new Event<unit>()
member this.NameChanging = nameChanging.Publish
member this.NameChanged = nameChanged.Publish
member this.Name
with get() = _name
and set(value) =
let eventArgs = new NameChangingEventArgs(value)
nameChanging.Trigger(eventArgs)
if not eventArgs.Cancel then
_name <- value
nameChanged.Trigger()
let p = new Person("Bob")
p.NameChanging.Add(fun e ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true
else
printfn "-- Name allowed")
(* ... rest of program ... *)
按照惯例,自定义事件参数应该继承自 System.EventArgs
,并且应该以 EventArgs
结尾。
F# 允许用户以与所有其他函数基本相同的方式将事件处理程序作为一等公民传递。 Event 模块 有一系列用于处理事件处理程序的函数
val choose : ('T -> 'U option) -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)
- 返回一个新的事件,它触发原始事件中选定消息的事件。选择函数将原始消息映射到可选的新消息。
val filter : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> (要求委托和 'del :> Delegate)
- 返回一个新的事件,它监听原始事件,并且仅当事件的参数通过给定函数时才触发结果事件。
val listen : ('T -> unit) -> IEvent<'del,'T> -> unit (要求委托和 'del :> Delegate)
- 每次触发给定事件时运行给定函数。
val map : ('T -> 'U) -> IEvent<'del,'T> -> IEvent<'U> (要求委托和 'del :> Delegate)
- 返回一个新的事件,它触发原始事件中选定消息的事件。选择函数将原始消息映射到可选的新消息。
val merge : IEvent<'del1,'T> -> IEvent<'del2,'T> -> IEvent<'T> (要求委托和 'del1 :> Delegate 和委托和 'del2 :> Delegate)
- 当任一输入事件触发时,触发输出事件。
val pairwise : IEvent<'del,'T> -> IEvent<'T * 'T> (要求委托和 'del :> Delegate)
- 返回一个新的事件,该事件在输入事件的第二次及后续触发时触发。输入事件的第 N 次触发将第 N-1 次和第 N 次触发的参数作为一对传递。传递给第 N-1 次触发的参数将保存在隐藏的内部状态中,直到第 N 次触发发生。应确保通过事件发送的值的内容不可变。请注意,许多 EventArgs 类型是可变的,例如 MouseEventArgs,并且使用此参数类型的每个事件触发可能会重用具有不同值的相同物理参数对象。在这种情况下,应在使用此组合器之前从参数中提取必要的信息。
val partition : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> * IEvent<'T> (要求委托和 'del :> Delegate)
- 返回一个新的事件,它监听原始事件,如果将谓词应用于事件参数返回 true,则触发第一个结果事件,如果返回 false,则触发第二个事件。
val scan : ('U -> 'T -> 'U) -> 'U -> IEvent<'del,'T> -> IEvent<'U> (要求委托和 'del :> Delegate)
- 返回一个新的事件,该事件由将给定的累积函数应用于输入事件触发的连续值的结果组成。一个内部状态项记录状态参数的当前值。在执行累积函数期间,内部状态不会被锁定,因此应注意不要让多个线程同时触发输入 IEvent。
val split : ('T -> Choice<'U1,'U2>) -> IEvent<'del,'T> -> IEvent<'U1> * IEvent<'U2> (要求委托和 'del :> Delegate)
- 返回一个新的事件,它监听原始事件,如果将函数应用于事件参数返回 Choice2Of1,则触发第一个结果事件,如果返回 Choice2Of2,则触发第二个事件。
考虑以下代码片段
p.NameChanging.Add(fun (e : NameChangingEventArgs) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true)
我们可以用更函数式的方式重写它,如下所示
p.NameChanging
|> Event.filter (fun (e : NameChangingEventArgs) ->
let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends )
|> Event.listen (fun e ->
printfn "-- No %s's allowed!" e.NewName
e.Cancel <- true)