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)