跳转到内容

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 方法上的变化。按照惯例和 微软的推荐,事件通常命名为 VerbVerbPhrase,以及添加时态,如 VerbedVerbing,以指示后事件和前事件。

向事件处理程序添加回调

[编辑 | 编辑源代码]

向事件处理程序添加回调非常容易。每个事件处理程序都有类型 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.
注意: 当多个回调连接到单个事件时,它们将按照添加的顺序执行。但是,在实践中,您不应该编写依赖于事件以特定顺序触发的代码,因为这样做会导致函数之间产生复杂的依赖关系。事件驱动编程通常是非确定性的,并且本质上是有状态的,这有时与函数式编程的精神相矛盾。最好编写不修改状态,也不依赖于任何先前事件调用的回调函数。

显式使用 EventHandlers

[编辑 | 编辑源代码]

添加和删除事件处理程序

[编辑 | 编辑源代码]

上面的代码演示了如何使用 IEvent<'T>.add 方法。但是,有时我们需要删除回调。为此,我们需要使用 IEvent<'T>.AddHandlerIEvent<'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 事件和委托类型

[编辑 | 编辑源代码]

尝试使用现有的 .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.
  • (#1) 将处理程序附加到控件的某个事件:b.AddHandler(Button.ClickEvent,d)
  • (#2) 使用函数创建委托/处理程序:let d = new RoutedEventHandler(f)
  • (#3) 创建具有委托定义的特定签名的函数:let f(sender:obj)(e:RoutedEventArgs) = ....
  • b 是控件。
  • AddHandler 是附加。
  • Button.ClickEvent 是事件。
  • d 是委托/处理程序。它是一层,用于确保签名正确
  • f 是提供给委托的实际函数/程序。
  • 规则#1:b 必须有此事件 Button.ClickEvent:b 是类型-Button-对象。ClickEvent 是类型-ButtonBase 的静态属性,类型-Button 继承自它。所以 Button 类型也将有此静态属性 ClickEvent。
  • 规则#2:d 必须是 ClickEvent 的处理程序:ClickEvent 是类型-RoutedEvent。RoutedEvent 的处理程序是 RoutedEventHandler,只是在末尾添加了 Handler。RoutedEventHandler 是 .NET 库中定义的委托。要创建 d,只需让 d = new RoutedEventHandler(f),其中 f 是函数。
  • 规则#3:f 必须具有遵守委托-d 定义的签名:检查 .NET 库,RoutedEventHandler 是 C# 签名的委托: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 结尾。

    使用 Event 模块

    [编辑 | 编辑源代码]

    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)
    
    前一页:接口 索引 下一页:模块和命名空间
    华夏公益教科书