Unity/C#基礎複習(5) 之 淺析觀察者、中介者模式在游戲中的應用與delegate原理

来源:https://www.cnblogs.com/sword-magical-blog/archive/2019/08/29/11430891.html
-Advertisement-
Play Games

參考資料 【1】 《Unity 3D腳本編程 使用C 語言開發跨平臺游戲》陳嘉棟著 【2】 @張子陽【C 中的委托和事件 Part.1】 http://www.tracefact.net/tech/009.html 【3】 @張子陽【C 中的委托和事件 Part.2】 http://www.trac ...


參考資料

【1】 《Unity 3D腳本編程 使用C#語言開發跨平臺游戲》陳嘉棟著
【2】 @張子陽【C#中的委托和事件 - Part.1】 http://www.tracefact.net/tech/009.html
【3】 @張子陽【C#中的委托和事件 - Part.2】 http://www.tracefact.net/tech/029.html
【4】 @毛星雲【《Effective C#》提煉總結】提高Unity中C#代碼質量的22條準則 https://zhuanlan.zhihu.com/p/24553860
【5】 《游戲編程模式》 Robert Nystrom著

基礎知識

  1. C#中使用delegate關鍵字來便捷地完成類似回調函數的機制。

疑難解答

  1. 觀察者模式是什麼?它在游戲中的應用場景有哪些?
  2. 中介者模式的應用場景?
  3. delegate關鍵字為我們做了什麼?
  4. event與delegate的關係是?

觀察者模式

概述

觀察者模式是一種定義了對象之間一對多關係的模式。當被觀察者改變狀態時,它的所有觀察者都會收到通知並做出響應。有時也可以將這種關係理解為發佈/訂閱的模式,即我們所關心的對象(即被觀察者)發佈消息時,所有訂閱該消息的對象都會進行響應,從而做出某些操作。

在《Unity3D腳本編程》一書中有一個很棒的例子來解釋這個模式,它是這樣描述的[1]

  • 報刊的任務是出版報紙。
  • 顧客可以向報刊訂閱報紙。
  • 當報刊出版報紙後,所有訂閱該報紙的人都會收到。
  • 當顧客不需要時,也可以取消訂閱,取消後,顧客就不會收到報社出版的報紙。
  • 報刊和顧客是兩個不同的主體,只要報社存在,不同的訂閱者可以訂閱或取消訂閱。

其中的報社就是我們說的被觀察者(Subject),而訂閱者則是觀察者(Observer)。

何時使用觀察者模式?

設計模式不是銀彈,所有設計模式都有一個最適合他們的應用場景,濫用設計模式會造成代碼冗餘嚴重,後續想要修改更是會力不從心。顯然,觀察者模式應該也是有他最佳的應用場景的。

感覺目前網上介紹觀察者模式在游戲開發中的應用的解釋都顯得不那麼明朗,這裡博主結合自己的經驗來試著談談在哪些場合下,使用觀察者模式可以得到不錯的效果。當然,目前up還是個初學C#和設計模式的小萌新,可能存在說錯或者紕漏的情況,如果大家發現了還請不吝賜教,我會超級感激不盡的!!!

在我看來,觀察者模式就是一個將A依賴B的關係轉變為B依賴A的關係的模式。所以是否使用觀察者模式的第一點,我認為應該是,判斷AB之間的依賴關係,並判斷出對於哪個對象來說,他更不能容忍在代碼中出現依賴多個對象的情況

這麼說可能有點抽象,下麵結合具體的例子來看看。

應用場景1--判斷AB依賴關係

當游戲中的某個單位HP下降時,它表現在UI上的生命條也應該按比例改變寬度(或高度等屬性)。而單位的生命值改變在游戲中是一件非常常見的事情。比如,當單位收到傷害時,甚至是單位釋放技能時(點名DNF大紅神)。

那麼,如果不使用觀察者模式,我們可能會寫出下麵的代碼。

import UI.HpUI;
// 游戲單位對象
class Character{

    // Character對象依賴於hp的UI,因為要主動通知該UI更新血條寬度
    HpUI hpView;

    int hp;
    // 收到傷害時執行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        // 主動通知hpUI更新
        hpView.update();
    }

    // 釋放技能時執行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

這樣能不能完成目標呢,也可以,但是我們可以發現在游戲對象Character上,他依賴了Hp的UI,這顯得特別突兀,如果後續還有MP的UI,人物屬性(攻擊力防禦力等)的UI,那麼Character對象全部都要引用一遍。

想象一下,當你開開心心創建了一個新的單位,想要讓他去打怪時,發現,報錯了。原因是沒有給這個新單位添加UI的依賴,這時,就可以發現,每次新建游戲單位對象都需要為他添加所有UI的依賴。

實際上,游戲對象是不是必須要依賴UI呢,感覺也不是,單位就是單位,就算沒有UI,他受傷了也會扣血也會死亡,不會說沒有了UI就會報錯,這個單位就死不了了(滑稽)。

那麼,可以判斷實際上如果是UI依賴游戲對象是不是更合理呢?UI沒有了他綁定的游戲對象,當然就無法表現出特定的效果啦。

下麵是使用觀察者模式之後的代碼。

// nowHp表示受傷後的血量,damage表示此次傷害數值
public delegate OnCharacterDamageHandler(int nowHp,int damage);

// 游戲單位對象
class Character{
    // 委托,當單位受傷時,向所有訂閱這個事件的訂閱者發送消息
    public OnCharacterDamageHandler onDamage;

    int hp;
    // 收到傷害時執行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // 發佈消息——這個對象受傷了
            onDamage(hp,damage);
    }

    // 釋放技能時執行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp條,單位hp改變時,自動調整寬度
    Image hpbar;

    // UI依賴於游戲對象
    Character character;

    // 根據當前hp改變血條UI的方法
    private void update(int hp,int damage){...}

    // UI初始化的方法
    public void Init(){
        // 血條UI訂閱單位受傷事件
        character.onDamage += update;
    }
}

這樣一來,依賴關係就從Character依賴HpUI,變成了HPUI依賴Character,HpUI監聽了Character的受傷事件,這樣好不好呢?感覺見仁見智把,還是那句話,要判斷AB對象誰更不能容忍依賴於其他對象。

應用場景1簡單總結

大家可以發現,經過上面的操作,實際上對象之間的耦合併沒有消失,只是從一種形式(A依賴B)變為了另一種形式(B依賴A)。

應用場景2——是否具備一對多的關係?

判斷是否使用觀察者模式,我認為,第二個要點是,消息的發佈者和訂閱者是否具備一對多的關係。

依舊以前面的受傷事件舉例(onDamge),如果此時又有一個需求要求我們做一個成就系統,當單位第一次死亡時,顯示出成就特效(類似於各類MOBA游戲的First Blood),那麼我們還是可以不讓Character對象依賴這個成就系統。

為什麼呢?依舊是用onDamage發佈消息,成就系統訂閱這個消息唄。當單位受傷時,成就系統訂閱受傷消息,從而判斷單位的HP是否減少到了0,如果為0,那麼就判斷單位死亡,此時再判斷單位是否是第一次死亡,再使用相應方法。

下麵上偽代碼。

class AchievementSystem{
    // 成就系統依賴於游戲對象
    Character character;

    // 根據對應事件觸發first blood成就
    public void update(int nowhp,int damage){...}

    public void Init(){
        character.onDamage = update;
    }
}

可以看到我們甚至沒有改動Character類一行,因為這本來就跟這個類沒有太大關係~~~

如果接下來又有一個類似的需求呢?繼續訂閱就行。舉個例子,如果策劃此時要求做一個單位血量減少到20%以下,增加防禦力的被動技能,怎麼做呢?

依舊是由被動技能訂閱受傷事件,判斷生命值是否到達20%以下,如果是,那麼觸發技能效果。

簡單總結

上述情況就是我所認為的一對多情況,也就是一個消息發佈出來,他會有多個訂閱者,每個訂閱者都會做出不同的響應,這時就可以考慮使用觀察者模式消除一部分耦合(並不能完全消除)。

中介者模式

概述

如果說觀察者模式只能消除一部分耦合,那麼中介者模式就是可以完全消除兩個對象的依賴情況。在《游戲編程模式》一書中,對中介者模式(也常被稱為服務定位型模式)的定義如下[5]

為某服務提供一個全局訪問入口來避免使用者與該服務的具體實現類之間產生耦合。

在我看來,中介者模式的主要作用就是將蜘蛛網式的引用關係變為N個對象依賴於中介者,中介者為這N個對象提供服務,也就是將原本多對多的關係變成了多對一,一對多的關係。這樣說起來可能有點抽象,下麵舉一個具體的例子來說明一下。

在游戲中,我們經常要製作一種提示型的UI,它經常要做以下這幾件事:

  1. 在玩家購買物品時,判斷玩家的資源是否足夠,如果不夠,提示玩家
  2. 玩家釋放技能時,判斷玩家mp是否足夠,如果不夠,提示玩家
  3. 當玩家想要進行某種被禁止的操作時,提示玩家,如攻擊無敵的敵人,對友軍釋放技能等等
    .....
    諸如此類,就不一一列舉了,這種UI還是挺常見的。他會零散的分佈在游戲系統的各個角落,可能在另一個UI上操作時,採取了某種操作,他就要跳出來提示你。

如果不使用中介者模式,那麼可能代碼中就會出現如下引用關係:

如果使用中介者模式,那麼依賴關係就可以轉變成下麵這樣。

添加中介者後,所有原本直接引用(或間接引用)提示UI的對象,全都變成了直接引用中介者,當他們有消息要發佈時(比如玩家做了某個不允許的操作),就直接向中介者發佈消息。此時訂閱了這個消息的tipsUI就可以自動獲得這個消息並處理他。從而解耦了各個對象與TipsUI。

何時使用中介者模式?

前面說到,中介者模式可以最大限度的消除耦合,使對象的依賴關係之間變小。那麼,是不是游戲里所有地方有依賴關係的地方,都可以用中介者模式呢?比如將所有觀察者模式都替換成中介者模式。答案應該是:不能。前面說到,設計模式不是萬能的,中介者模式有它的優點自然就會有它的缺點。

中介者模式本質上其實是與單例模式極其相似,仔細觀察前面的案例,其實我們也可以用靜態方法(類)或單例模式來解決。不過對於Unity的MonoBehavior類來說有點特殊,這個繼承自MonoBehavior的TipsUI對象並不能設置成靜態,不過我們也能將TipsUI設置為單例模式來解決它零散地分佈在游戲系統這一問題。

中介者模式的一大缺點就在於:他將耦合變得不直觀。閱讀使用中介者模式的代碼,往往難以理解誰依賴他,這也是他的性質決定的,中介者並不知道誰會發佈消息(即並不知道服務被誰定位),我們需要滿代碼的找誰向中介者發佈了消息,誰又處理了這個消息。這與直接的對象引用比起來,更不容易看出來。

根據《游戲編程模式》一書所說,中介者模式的第一個應用場景應該是這樣的[5]

當手動將一個對象傳來傳去顯得毫無理由或者使得代碼難以閱讀時,可以嘗試考慮使用這個設計模式。畢竟將一個環境屬性傳遞10層函數以便讓一個底層函數能夠訪問,這會使代碼增加毫無意義的複雜度。

在游戲開發中,感覺有幾個鮮明的例子可以說明這點,比如音頻管理,音頻需要在任何需要它的地方播放,但是如果把音頻管理器對象在每份代碼之間傳來傳去就顯得毫無理由,這時,就可以考慮中介者模式(或單例模式)。

除此之外還有許多的例子,比如日誌系統,前面提到的提示UI等等。

中介者模式與單例模式的區別?

前面提到中介者模式與單例模式極其相似,他們都是全局能夠訪問的對象。當我們使用其中之一的時候,應該考慮哪一個更適合需求。

在《游戲編程模式》中沒有過多的討論如何選擇使用這兩個模式,根據菜雞博主這些年來單薄的項目經驗和淺薄的認知,我認為中介者模式比之單例模式多了一層健壯性

這是因為,有些在C#中的中介者模式是使用委托進行設計的,當訂閱一個消息的時候,實際上就是向該委托+=一個響應方法,而發佈消息時,實際上就是直接調用這個委托方法。

這樣一來,如果我們用中介者模式設計音頻管理器,那麼就算此時我們音頻管理器出錯了,無法在游戲中播放聲音,游戲也能正常運行,或者說,即使我們將音頻管理器的代碼全部刪除,再寫一個功能更強大的音頻系統,只要這個新的音頻系統響應的消息是一樣的(與之前那個一樣),那麼這個中介者模式就依舊沒有出錯,依然能正常運行。

如何在C#中實現中介者模式

前面提到,中介者模式大多依靠委托實現,下麵是基本的代碼框架(參考自網上)。

public delegate void CallBack();
public delegate void CallBack<T>(T arg);
public delegate void CallBack<T, X>(T arg, X arg1);
public delegate void CallBack<T, X, Y>(T arg, X arg1, Y arg2);
public delegate void CallBack<T, X, Y, Z>(T arg, X arg1, Y arg2, Z arg3);
public delegate void CallBack<T, X, Y, Z, W>(T arg, X arg1, Y arg2, Z arg3, W arg4);

/// <summary>
/// 充當所有UI視圖對象的中介者,單例類,具有兩個功能
/// 
/// 1. 訂閱事件:
///     向某種事件發起訂閱,參數是一個delegate委托函數,
///     表示當某個事件發生後,調用該函數
/// 2. 發佈事件:
///     發佈事件相當於我們沒有用中介者之前的的OnXXX委托,
///     在某個事件發生時,調用該中介者的發佈事件方法,
///     用以調用所有訂閱該事件的對象的方法
/// </summary>
public class MessageAggregator {

    // 單例類,使用餓汗模式載入,防止在多線程環境下出錯
    public static readonly MessageAggregator Instance = new MessageAggregator();
    private MessageAggregator() { }

    private Dictionary<string, Delegate> _messages;

    public Dictionary<string, Delegate> Messages {
        get {
            if (_messages == null) _messages = new Dictionary<string, Delegate>();
            return _messages;
        }
    }

    /// <summary>
    /// 當訂閱事件時調用
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callback"></param>
    private void OnListenerAdding(string string,Delegate callback) {
        //判斷字典裡面是否包含該事件碼
        if (!Messages.ContainsKey(string)) {
            Messages.Add(string, null);
        }
        Delegate d = Messages[string];
        if (d != null && d.GetType() != callback.GetType()) {
            throw new Exception(string.Format("嘗試為事件{0}添加不同類型的委托,當前事件所對應的委托是{1},要添加的委托是{2}", string, d.GetType(), callback.GetType()));
        }
    }


    /// <summary>
    /// 當取消訂閱事件時調用
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    private void OnListenerRemoving(string string,Delegate callBack) {
        if (Messages.ContainsKey(string)) {
            Delegate d = Messages[string];
            if (d == null) {
                throw new Exception(string.Format("移除監聽事件錯誤:事件{0}沒有對應的委托", string));
            } else if (d.GetType() != callBack.GetType()) {
                throw new Exception(string.Format("移除監聽事件錯誤:嘗試為事件{0}移除不同類型的委托,當前事件所對應的委托為{1},要移除的委托是{2}", string, d.GetType(), callBack.GetType()));
            }
        } else {
            throw new Exception(string.Format("移除監聽事件錯誤:沒有事件碼{0}", string));
        }
    }

    /// <summary>
    /// 無參的監聽事件(即訂閱事件)的方法
    /// </summary>
    /// <param name="string"></param>
    /// <param name="callBack"></param>
    public void AddListener(string string,CallBack callBack) {
        OnListenerAdding(string, callBack);
        Messages[string] = (CallBack)Messages[string] + callBack;
    }

    // 1參的監聽事件(即訂閱事件)的方法
    public void AddListener<T>(string string, CallBack<T> callBack) {...}

    // 2參的監聽事件(即訂閱事件)的方法
    public void AddListener<T,X>(string string, CallBack<T,X> callBack) {...}

    // 3參的監聽事件(即訂閱事件)的方法
    public void AddListener<T,X,V>(string string, CallBack<T,X,V> callBack) {...}

    // 4參的監聽事件(即訂閱事件)的方法
    public void AddListener<T,X,Y,Z>(string string, CallBack<T,X,Y,Z> callBack) {...}

    // 5參的監聽事件(即訂閱事件)的方法
    public void AddListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // 無參的移除監聽事件的方法
    public void RemoveListener(string string,CallBack callBack) {
        OnListenerRemoving(string, callBack);
        Messages[string] = (CallBack)Messages[string] - callBack;
    }

    // 1參的移除監聽事件的方法
    public void RemoveListener<T>(string string, CallBack<T> callBack) {...}


    // 2參的移除監聽事件的方法
    public void RemoveListener<T, X>(string string, CallBack<T, X> callBack) {...}

    // 3參的移除監聽事件的方法
    public void RemoveListener<T, X, V>(string string, CallBack<T, X, V> callBack) {...}

    // 4參的移除監聽事件的方法
    public void RemoveListener<T, X, Y, Z>(string string, CallBack<T, X, Y, Z> callBack) {...}

    // 5參的移除監聽事件的方法
    public void RemoveListener<T, X, Y, Z,W>(string string, CallBack<T, X, Y, Z,W> callBack) {...}

    // 無參的廣播監聽事件
    public void Broadcast(string string) {
        Delegate d;
        if (Messages.TryGetValue(string, out d)) {
            CallBack callBack = d as CallBack;
            if (callBack != null)
                callBack();
            else
                throw new Exception(string.Format("廣播事件錯誤:事件{0}對應委托有不同的類型", string));
        }
    }

    // 1參的廣播監聽事件
    public void Broadcast<T>(string string,T arg0) {...}

    // 2參的廣播監聽事件
    public void Broadcast<T,V>(string string, T arg0,V arg1) {...}

    // 3參的廣播監聽事件
    public void Broadcast<T,V,X>(string string, T arg0,V arg1,X arg2) {...}

    // 4參的廣播監聽事件
    public void Broadcast<T, V, X,Z>(string string, T arg0, V arg1, X arg2,Z arg3) {...}

    // 5參的廣播監聽事件
    public void Broadcast<T, V, X, Z,W>(string string, T arg0, V arg1, X arg2, Z arg3,W arg4) {...}
}

以前面的TipsUI為例子,我們下麵實現在人物釋放技能時如果mp不夠,對玩家進行提示。

// 單位對象
class Character{
    int mp;
    // 釋放技能
    public void ExecuteSkill(Skill skill){
        if(this.mp < skill.mp)
            // 向中介者發佈施法mp不夠的消息
            MessageAggregator.Instance.Broadcast< int,int >("ExecuteSkill",mp,skill.mp);
    }
}

// 提示UI對象
Class TipsUI{
    // 要提示的字元串信息
    private string tips;
    
    public void update(int nowmp,int skillmp){
        tips = string.format("當前mp為:%d,技能要消耗的mp:%d,mp不夠,不能釋放技能",nowmp,skillmp);
    }

    // 對各類消息進行訂閱的方法
    public void Bind(){
        // 訂閱施法的消息
        MessageAggregator.Instance.AddListener<int,int>("ExecuteSkill",update);
    }
}

這個時候,當玩家釋放技能時,如果mp不夠,就會觸發提示UI出來提示不能釋放技能。

delegate做了什麼?

在C#中delegate是一個非常方便的關鍵字,它一般用於回調函數機制,利用它我們很容易就能寫出低耦合的觀察者模式。他有些類似與C++中的函數指針,但是相比於函數指針,C#中的委托天然支持多播委托(即有多個回調方法,也就是委托鏈),以及類型安全,寫起來相當方便舒服。

那麼delegate到底為我們做了什麼呢?delegate關鍵字後面聲明的到底是類型還是變數呢?老實說,博主初學委托的時候,經常會寫出像下麵這樣傻傻的代碼。

// 錯誤代碼
class Main{

    delegate void Func();

    public void init(){
        // 為委托添加回調方法
        Func+=Func1;
    }

    void Func1(){}
}

大家發現了把~
菜鳥博主sjm經常把委托看成是函數指針一樣的東西了,然後以為聲明的是一個類似指針的變數,所以就直接指向目標方法了。這樣當然就報錯啦~~

這就是不瞭解delegate背後工作惹的禍~C#編譯器在後面為我們做了相當多的工作,要理解委托,就得去看看由C#代碼生成的中間代碼IL。

下麵是一個簡單的使用委托的代碼示例。

using System;
public delegate void Func(int a);

public class C {
    Func func;
    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

我們可以去 https://sharplab.io 中查看C#代碼生成的中間代碼是什麼樣的。

由上面代碼生成的IL(中間)代碼大概是下麵這樣的。

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{
    // Methods
    .method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        ) runtime managed 
    {
    } // end of method Func::.ctor

    .method public hidebysig newslot virtual 
        instance void Invoke (
            int32 a
        ) runtime managed 
    {
    } // end of method Func::Invoke

    .method public hidebysig newslot virtual 
        instance class [mscorlib]System.IAsyncResult BeginInvoke (
            int32 a,
            class [mscorlib]System.AsyncCallback callback,
            object 'object'
        ) runtime managed 
    {
    } // end of method Func::BeginInvoke

    .method public hidebysig newslot virtual 
        instance void EndInvoke (
            class [mscorlib]System.IAsyncResult result
        ) runtime managed 
    {
    } // end of method Func::EndInvoke

} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func

    // Methods
    .method public hidebysig 
        instance void M () cil managed 
    {
        .....
        IL_0014: call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate, class [mscorlib]System.Delegate)
        .....
        IL_002b: callvirt instance void Func::Invoke(int32)
        ....
    } // end of method C::M

    .method private hidebysig 
        instance void aa (
            int32 a
        ) cil managed 
    {...} // end of method C::aa
} // end of class C

大家可以看到,神奇的事情發生了,在我們用delegate聲明Func時,一個名為Func的類在本地被聲明瞭!由此,我們也可以發現delegate的秘密,那就是,在delegate定義的地方,編譯器會自動幫我們聲明一個繼承於MulticastDelegate(多播委托)的類型。事實上,如果繼續追溯,我們還可以發現MulticastDelegate繼承於Delegate,這個Delegate類有個Combine方法,主要用於將兩份委托連接起來形成委托鏈。

因此,當我們以後看到delegate xxx時,可以自動將其等價為class xxx : MulticastDelegate。因為delegate是用於聲明類型的,所以類型能用的修飾符他理論上也能用,如public、private、internal等等。

而對於這個新聲明的類型,有兩個方法是值得註意的,分別是他的構造方法和Invoke方法。

它的構造方法的簽名如下:

.method public hidebysig specialname rtspecialname 
        instance void .ctor (
            object 'object',
            native int 'method'
        )

可以看到這裡有兩個參數,分別是一個object對象和一個整型,當委托添加了一個實例方法時,這個object就是該實例方法所操作的對象,而如果是靜態方法,那麼這個參數就為null。
而整型method可以看作是一個指針,指向了我們要調用的函數,即保存了該函數的地址。在《Unity3D腳本編程》中介紹該參數是運行時用來標識要回調的方法,是一個引用回調函數的句柄~

而Invoke方法顧名思義,就是用來調用回調函數的方法。

除此之外,delegate還有很多語法糖,比如說,當我們初始化時,不必填寫object參數,編譯器會自動幫我們完成。還有我們可以像調用一個正常方法一樣調用委托,就像上面代碼一樣,我們聲明瞭一個Func類型的委托變數func,可以像調用正常的方法一樣通過func()來調用它。這其中編譯器會將這種代碼翻譯為func.invoke(xxx);

event是什麼?它與delegate關係是?

除了delegate外,我們還經常會看到像下麵這樣的代碼。

public delegate OnCharacterDamageHandler(int nowHp,int damage);

// 游戲單位對象
class Character{
    // 委托,當單位受傷時,向所有訂閱這個事件的訂閱者發送消息
    public event OnCharacterDamageHandler onDamage;

    int hp;
    // 收到傷害時執行的方法
    public void Damaged(int damage){
        this.hp -= damage;
        if(onDamage!=null)
            // 發佈消息——這個對象受傷了
            onDamage(hp,damage);
    }

    // 釋放技能時執行的方法
    public void SkillExecute(Skill skill){
        // 某些特殊技能需要消耗HP
        if(skill == xxx){
            Damaged(xxxx);
        }
    }
}

class HpUI{
    // hp條,單位hp改變時,自動調整寬度
    Image hpbar;

    // UI依賴於游戲對象
    Character character;

    // 根據當前hp改變血條UI的方法
    private void update(int hp,int damage){...}

    // UI初始化的方法
    public void Init(){
        // 血條UI訂閱單位受傷事件
        character.onDamage += update;
    }
}

閱讀上述代碼,可以發現event關鍵字似乎和delegate能達到同樣的效果。那麼這兩個關鍵字到底有什麼區別呢?

event的由來

這要從event的由來說起。我們已經知道了delegate關鍵字實際上是聲明瞭一個類型,而Character類的內部則是聲明瞭一個OnCharacterDamageHandler類型的變數,可以向這個變數添加或消除回調方法。

那麼,onDamage作為類中的欄位,就要考慮他訪問修飾符的問題,我們知道類中的屬性並不應該都是public,有些屬性不應該暴露在外面。因為外部有可能會對這個屬性做出一些奇怪的改動,比如將其賦值為null。

對於委托類型來說,實際上我們在外部所需要的操作只是+=和-=而已。

然而,當你真的想把onDamage改成private或internal時候,你會發現,這個變數根本不能改成除了public以外的訪問修飾符。為啥呢?因為把它改成非public後,外部就不能該這個委托類型添加方法了啊!而我們設計委托,不就是為了能在外部向他註冊方法麽。如果把他設為private,那就相當於他完全失效了~

但是,我們又不想將onDamge設為public,因為"在客戶端可以對它進行隨意的賦值等操作,嚴重破壞對象的封裝性"[2]

為此,C#提供event來解決這個問題。當我們使用event封裝委托類型的變數時,該委托變數在外部只接受Add和Remove操作,而不能被隨意的賦值,增強了安全性。

試著將上面的代碼

character.onDamage += update;

更改為

character.onDamage = update;

可以發現編譯錯誤~~

event做了什麼?

public event OnCharacterDamageHandler onDamage;

那麼,在上面這行語句中,到底發生了什麼我們不知道的事呢?

依舊是老方法,看看生成的中間代碼。

為了防止生成的中間代碼過長,下麵是一個簡單的使用event的示例。

using System;
public delegate void Func(int a);

public class C {
    public event Func func;

    public void M() {
        func += aa;
        
        func(11);
    }
    void aa(int a){
        Console.WriteLine(a);
    }
}

他生成的中間代碼如下所示。

.class private auto ansi '<Module>'
{
} // end of class <Module>

.class public auto ansi sealed Func
    extends [mscorlib]System.MulticastDelegate
{...} // end of class Func

.class public auto ansi beforefieldinit C
    extends [mscorlib]System.Object
{
    // Fields
    .field private class Func func
    ...

    // Methods
    .method public hidebysig specialname 
        instance void add_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::add_func

    .method public hidebysig specialname 
        instance void remove_func (
            class Func 'value'
        ) cil managed 
    {...} // end of method C::remove_func

    .method public hidebysig 
        instance void M () cil managed 
    {
        ...
        IL_000e: call instance void C::add_func(class Func)
        ...
        IL_0015: ldfld class Func C::func
        IL_001a: ldc.i4.s 11
        IL_001c: callvirt instance void Func::Invoke(int32)
        ...
    } // end of method C::M
    ...
    // Events
    .event Func func
    {
        .addon instance void C::add_func(class Func)
        .removeon instance void C::remove_func(class Func)
    }
} // end of class C

我略去了一部分與本次示例無關的代碼。大家可以看到,在public event這一行中,雖然我們用的是public來聲明委托變數,但最後編譯器還是將其當做private變數,同時編譯器還為類中增加了兩個方法,分別是add_func和remove_func,用於向委托添加或刪除方法。

這樣,就相當於在類中封裝了Func類型,僅僅暴露了增加和刪除方法的介面給外部,增強了安全性~~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、概念介紹 素數又稱為質數。一個大於1的自然數(從2開始),除了1和它本身外,不能被其他自然數整除的叫做素數,否則稱為合數。 0和1既不是素數也不是合數,最小的素數是2。 二、代碼 方法一: 註意:在for迴圈判斷時不能忘記 i <= sqrt(num) 的等號,因為假設 p*p = n , n的 ...
  • 原文:https://blog.csdn.net/LaySwift/article/details/79458947 1,vscode原生支持markdown,導出需要插件,基於node.js,需要node.js環境。2,只需要裝一個預覽插件神器:Markdown Preview Enhanced。 ...
  • 給 asp.net core 寫個中間件來記錄介面耗時 Intro 寫介面的難免會遇到別人說介面比較慢,到底慢多少,一個介面伺服器處理究竟花了多長時間,如果能有具體的數字來記錄每個介面耗時多少,別人再說介面慢的時候看一下介面耗時統計,如果幾毫秒就處理完了,對不起這鍋我不背。 中間件實現 asp.ne ...
  • 一、C#提供對面向對象編程(Object Oriented Programming)的完整支持;類描述對象的類型,而對象是類的具體實例,創建對象的過程也被稱為實例化(Instantiation);通常使用new運算符來創建對象: ※其中()實際上代表的是調用類的預設構造函數來構建類的實例; ※C#還 ...
  • 先看看netcore有哪些特性,哪些優點,與.net frameworkd 差異吧: l 跨平臺: 可以在 Windows、macOS 和 Linux 操作系統上運行。 l 跨體繫結構保持一致: 在多個體繫結構(包括 x64、x86 和 ARM)上以相同的行為運行代碼。 l 命令行工具: 包括可用於 ...
  • 1. 前言 近兩年來,很多前端的同學都開始將 VSCode 作為前端主力開發工具,其豐富的擴展給程式開發尤其是前端開髮帶來了很多便利,但是作為微軟主力語言的 .NET,卻由於有宇宙第一編輯器 Visual Studio存在,很少有看到有後端同學使用,筆者自己在 VSCode 剛出來時就折騰過將主力開 ...
  • 這是一個DotNet輕量級ORM框架,解決C#.Net開發過程中重覆繁瑣的資料庫CURD操作。 前言 因工作中接手的.net項目,源碼裡面都用了動軟代碼生成的源碼做為資料庫操作類庫。其中,有些根本就沒有用到,今後也不會用到的冗餘代碼——垃圾代碼。而每次如果有表結構修改,就得重新生成表實體/手動修改實 ...
  • 工作需求,開發釘釘微應用和小程式,之前有接觸過支付寶小程式和生活號的開發,流程沒有很大的差別,這裡記錄下我用ASP.NET MVC實現釘釘微應用的開發,並實現獲取用戶的userid。小弟我技術有限,本文中的一些命名或方法寫的不好的,還請指點。 釘釘開發者平臺上有各個平臺的SDK,我也有下載對應的.N ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...