C語言中文網 目錄

Go語言使用事件系統實現事件的響應和處理

Go 語言可以將類型的方法與普通函數視為一個概念,從而簡化方法和函數混合作為回調類型時的復雜性。這個特性和 C# 中的代理(delegate)類似,調用者無須關心誰來支持調用,系統會自動處理是否調用普通函數或類型的方法。

本節中,首先將用簡單的例子了解 Go 語言是如何將方法與函數視為一個概念,接著會實現一個事件系統,事件系統能有效地將事件觸發與響應兩端代碼解耦。

方法和函數的統一調用

本節的例子將讓一個結構體的方法(class.Do)的參數和一個普通函數(funcDo)的參數完全一致,也就是方法與函數的簽名一致。然后使用與它們簽名一致的函數變量(delegate)分別賦值方法與函數,接著調用它們,觀察實際效果。

詳細實現請參考下面的代碼。
package main

import "fmt"

// 聲明一個結構體
type class struct {
}

// 給結構體添加Do方法
func (c *class) Do(v int) {

    fmt.Println("call method do:", v)
}

// 普通函數的Do
func funcDo(v int) {

    fmt.Println("call function do:", v)
}

func main() {

    // 聲明一個函數回調
    var delegate func(int)

    // 創建結構體實例
    c := new(class)

    // 將回調設為c的Do方法
    delegate = c.Do

    // 調用
    delegate(100)

    // 將回調設為普通函數
    delegate = funcDo

    // 調用
    delegate(100)
}
代碼說明如下:
  • 第 10 行,為結構體添加一個 Do() 方法,參數為整型。這個方法的功能是打印提示和輸入的參數值。
  • 第 16 行,聲明一個普通函數,參數也是整型,功能是打印提示和輸入的參數值。
  • 第 24 行,聲明一個 delegate 的變量,類型為 func(int),與 funcDo 和 class 的 Do() 方法的參數一致。
  • 第 30 行,將 c.Do 作為值賦給 delegate 變量。
  • 第 33 行,調用 delegate() 函數,傳入 100 的參數。此時會調用 c 實例的 Do() 方法。
  • 第 36 行,將 funcDo 賦值給 delegate。
  • 第 39 行,調用 delegate(),傳入 100 的參數。此時會調用 funcDo() 方法。

運行代碼,輸出如下:
call method do: 100
call function do: 100

這段代碼能運行的基礎在于:無論是普通函數還是結構體的方法,只要它們的簽名一致,與它們簽名一致的函數變量就可以保存普通函數或是結構體方法。

了解了 Go 語言的這一特性后,我們就可以將這個特性用在事件中。

事件系統基本原理

事件系統可以將事件派發者與事件處理者解耦。例如,網絡底層可以生成各種事件,在網絡連接上后,網絡底層只需將事件派發出去,而不需要關心到底哪些代碼來響應連接上的邏輯。或者再比如,你注冊、關注或者訂閱某“大V”的社交消息后,“大V”發生的任何事件都會通知你,但他并不用了解粉絲們是如何為她喝彩或者瘋狂的。如下圖所示為事件系統基本原理圖。


圖:事件系統基本原理

一個事件系統擁有如下特性:
  • 能夠實現事件的一方,可以根據事件ID或名字注冊對應的事件。
  • 事件發起者,會根據注冊信息通知這些注冊者。
  • 一個事件可以有多個實現方響應。

通過下面的步驟詳細了解事件系統的構成及使用。

事件注冊

事件系統需要為外部提供一個注冊入口。這個注冊入口傳入注冊的事件名稱和對應事件名稱的響應函數,事件注冊的過程就是將事件名稱和響應函數關聯并保存起來,詳細實現請參考下面代碼的 RegisterEvent() 函數。
package main

// 實例化一個通過字符串映射函數切片的map
var eventByName = make(map[string][]func(interface{}))

// 注冊事件,提供事件名和回調函數
func RegisterEvent(name string, callback func(interface{})) {

    // 通過名字查找事件列表
    list := eventByName[name]

    // 在列表切片中添加函數
    list = append(list, callback)

    // 將修改的事件列表切片保存回去
    eventByName[name] = list
}

// 調用事件
func CallEvent(name string, param interface{}) {

    // 通過名字找到事件列表
    list := eventByName[name]

    // 遍歷這個事件的所有回調
    for _, callback := range list {

        // 傳入參數調用回調
        callback(param)
    }

}
代碼說明如下:
  • 第 4 行,創建一個 map 實例,這個 map 通過事件名(string)關聯回調列表([]func(interface{}),同一個事件名稱可能存在多個事件回調,因此使用回調列表保存。回調的函數聲明為 func(interface{})。
  • 第 7 行,提供給外部的通過事件名注冊響應函數的入口。
  • 第 10 行,eventByName 通過事件名(name)進行查詢,返回回調列表([]func(interface{})。
  • 第 13 行,為同一個事件名稱在已經注冊的事件回調的列表中再添加一個回調函數。
  • 第 16 行,將修改后的函數列表設置到 map 的對應事件名中。

擁有事件名和事件回調函數列表的關聯關系后,就需要開始準備事件調用的入口了。

事件調用

事件調用方和注冊方是事件處理中完全不同的兩個角色。事件調用方是事發現場,負責將事件和事件發生的參數通過事件系統派發出去,而不關心事件到底由誰處理;事件注冊方通過事件系統注冊應該響應哪些事件及如何使用回調函數處理這些事件。事件調用的詳細實現請參考上面代碼的 CallEvent() 函數。

代碼說明如下:
  • 第 20 行,調用事件的入口,提供事件名稱 name 和參數 param。事件的參數表示描述事件具體的細節,例如門打開的事件觸發時,參數可以傳入誰進來了。
  • 第 23 行,通過注冊事件回調的 eventByName 和事件名字查詢處理函數列表 list。
  • 第 26 行,遍歷這個事件列表,如果沒有找到對應的事件,list 將是一個空切片。
  • 第 29 行,將每個函數回調傳入事件參數并調用,就會觸發事件實現方的邏輯處理。

使用事件系統

例子中,在 main() 函數中調用事件系統的 CallEvent 生成 OnSkill 事件,這個事件有兩個處理函數,一個是角色的 OnEvent() 方法,還有一個是函數 GlobalEvent(),詳細代碼實現過程請參考下面的代碼。
package main

import "fmt"

// 聲明角色的結構體
type Actor struct {
}

// 為角色添加一個事件處理函數
func (a *Actor) OnEvent(param interface{}) {

    fmt.Println("actor event:", param)
}

// 全局事件
func GlobalEvent(param interface{}) {

    fmt.Println("global event:", param)
}

func main() {

    // 實例化一個角色
    a := new(Actor)

    // 注冊名為OnSkill的回調
    RegisterEvent("OnSkill", a.OnEvent)

    // 再次在OnSkill上注冊全局事件
    RegisterEvent("OnSkill", GlobalEvent)

    // 調用事件,所有注冊的同名函數都會被調用
    CallEvent("OnSkill", 100)

}
代碼說明如下:
  • 第 6 行,聲明一個角色的結構體。在游戲中,角色是常見的對象,本例中,角色也是 OnSkill 事件的響應處理方。
  • 第 10 行,為角色結構添加一個 OnEvent() 方法,這個方法擁有 param 參數,類型為 interface{},與事件系統的函數(func(interface{}))簽名一致。
  • 第 16 行為全局事件響應函數。有時需要全局進行偵聽或者處理一些事件,這里使用普通函數實現全局事件的處理。
  • 第 27 行,注冊一個 OnSkill 事件,實現代碼由 a 的 OnEvent 進行處理。也就是 Actor的OnEvent() 方法。
  • 第 30 行,注冊一個 OnSkill 事件,實現代碼由 GlobalEvent 進行處理,雖然注冊的是同一個名字的事件,但前面注冊的事件不會被覆蓋,而是被添加到事件系統中,關聯 OnSkill 事件的函數列表中。
  • 第 33 行,模擬處理事件,通過 CallEvent() 函數傳入兩個參數,第一個為事件名,第二個為處理函數的參數。

整個例子運行結果如下:
actor event: 100
global event: 100

結果演示,角色和全局的事件會按注冊順序順序地觸發。

一般來說,事件系統不保證同一個事件實現方多個函數列表中的調用順序,事件系統認為所有實現函數都是平等的。也就是說,無論例子中的 a.OnEvent 先注冊,還是 GlobalEvent() 函數先注冊,最終誰先被調用,都是無所謂的,開發者不應該去關注和要求保證調用的順序。

一個完善的事件系統還會提供移除單個和所有事件的方法。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo