動態編程入門第二節:委托與事件 - Unity 開發者的高級回調與通信藝術

動態編程入門第一節:C# 反射 - Unity 開發者的超級工具箱
動態編程入門第二節:委托與事件 - Unity 開發者的高級回調與通信藝術

上次我們聊了 C# 反射,它讓程序擁有了在運行時“看清自己”的能力。但光能看清還不夠,我們還需要讓代碼能夠靈活地“溝通”和“響應”。這就不得不提到 C# 中另外兩個非常重要的概念:委托 (Delegate)事件 (Event)

作為 Unity 開發者,你可能每天都在使用它們,比如 Unity UI 按鈕的 OnClick 事件、SendMessageGetComponent<T>().SomeMethod() 等等,它們背后或多或少都離不開委托和事件的思想。今天,我們就來深入探討它們的進階用法,以及它們如何構建起 Unity 中高效、解耦的回調和消息系統。


1. 委托(Delegate):方法的“引用”或“簽名”

簡單來說,委托是一個類型安全的函數指針。它定義了一個方法的簽名(包括返回類型和參數列表),可以引用任何符合這個簽名的方法。一旦委托引用了一個或多個方法,你就可以通過調用委托來執行這些被引用的方法。

1.1 委托的基礎與回顧

你可能已經習慣了使用 Unity 的 UnityEvent 或者直接使用 ActionFunc。它們都是委托的體現。

  • 定義委托:

    // 定義一個委托類型,它能引用一個沒有參數,沒有返回值的函數
    public delegate void MyActionDelegate();// 定義一個委托類型,它能引用一個接收一個int參數,返回string的函數
    public delegate string MyFuncDelegate(int value);
    
  • 實例化與調用:

    using UnityEngine;public class DelegateBasicExample : MonoBehaviour
    {public delegate void MySimpleDelegate(); // 定義委托void Start(){MySimpleDelegate del; // 聲明委托變量// 引用一個方法 (方法簽名必須與委托匹配)del = SayHello;del(); // 調用委托,等同于調用 SayHello()// 委托可以引用靜態方法del += SayGoodbye; // += 用于添加方法到委托鏈 (多播委托)del(); // 會依次調用 SayHello() 和 SayGoodbye()del -= SayHello; // -= 用于從委托鏈中移除方法del(); // 只會調用 SayGoodbye()}void SayHello(){Debug.Log("Hello from delegate!");}static void SayGoodbye(){Debug.Log("Goodbye from static delegate!");}
    }
    
1.2 ActionFunc:泛型委托的便捷性

在 C# 3.0 之后,微軟引入了 ActionFunc 這兩個內置的泛型委托,極大地簡化了委托的定義。

  • Action 用于引用沒有返回值的委托。

    • Action:沒有參數,沒有返回值。
    • Action<T1, T2, ...>:接收 T1, T2… 類型參數,沒有返回值。
    • 最多支持 16 個參數。
  • Func 用于引用有返回值的委托。

    • Func<TResult>:沒有參數,返回 TResult 類型。
    • Func<T1, T2, ..., TResult>:接收 T1, T2… 類型參數,返回 TResult 類型。
    • 最多支持 16 個參數和 1 個返回值。

示例:

using System; // Action 和 Func 在 System 命名空間
using UnityEngine;public class ActionFuncExample : MonoBehaviour
{void Start(){// Action 示例Action greetAction = () => Debug.Log("Hello using Action!");greetAction();Action<string> printMessage = (msg) => Debug.Log("Message: " + msg);printMessage("This is a test.");// Func 示例Func<int, int, int> addFunc = (a, b) => a + b;Debug.Log("10 + 20 = " + addFunc(10, 20));Func<string> getRandomString = () => Guid.NewGuid().ToString();Debug.Log("Random string: " + getRandomString());}
}

通過 ActionFunc,我們幾乎可以滿足所有常見委托簽名的需求,無需再手動定義 delegate 關鍵字。

1.3 匿名方法與 Lambda 表達式:讓委托更簡潔
  • 匿名方法: 在 C# 2.0 引入,允許你定義一個沒有名字的方法,直接賦值給委托。

    MySimpleDelegate del = delegate() { Debug.Log("I'm an anonymous method!"); };
    del();
    
  • Lambda 表達式: 在 C# 3.0 引入,是匿名方法的進一步簡化和增強,也是現在最常用的寫法。

    // 無參數:
    Action noParam = () => Debug.Log("No parameters!");
    noParam();// 單參數:
    Action<string> oneParam = msg => Debug.Log($"Message: {msg}"); // 如果只有一個參數,可以省略括號
    oneParam("Hello Lambda!");// 多參數:
    Func<int, int, int> add = (a, b) => a + b;
    Debug.Log($"Add: {add(3, 5)}");// 包含多行代碼:
    Action multiLine = () =>
    {Debug.Log("First line.");Debug.Log("Second line.");
    };
    multiLine();
    

Lambda 表達式極大地提高了代碼的可讀性和簡潔性,使得編寫事件回調和 LINQ 查詢變得非常流暢。


2. 事件(Event):基于委托的安全發布/訂閱機制

委托為我們提供了回調的能力,而 事件 (Event) 則是在委托基礎上構建的一種特殊的類型成員,它提供了一種安全的機制來發布和訂閱通知。

事件的核心思想是:發布者(擁有事件的類)只負責“發出通知”,而不知道誰會接收;訂閱者(其他類)只負責“接收通知”,而不需要知道通知來自何方。這種解耦是實現松耦合代碼的關鍵。

2.1 事件的優勢

事件相對于直接暴露委托變量有以下優勢:

  1. 封裝性: 事件只能在聲明它的類內部被觸發(Invoke),外部代碼只能通過 +=-= 運算符來訂閱或取消訂閱,不能直接賦值或清空整個委托鏈。這防止了外部代碼不小心破壞事件的訂閱列表。
  2. 安全性: 外部代碼無法得知事件有多少個訂閱者,也無法在未經授權的情況下觸發事件。
2.2 事件的實現與使用
using System;
using UnityEngine;// 事件發布者
public class GameEventManager : MonoBehaviour
{// 聲明一個事件,通常使用 Action 或自定義委托類型public event Action OnPlayerDeath; // 當玩家死亡時觸發public event Action<int> OnScoreChanged; // 當分數改變時觸發,并傳遞新分數// 單例模式,方便全局訪問public static GameEventManager Instance { get; private set; }void Awake(){if (Instance == null){Instance = this;}else{Destroy(gameObject);}}// 外部調用此方法來“發布”或“觸發”事件public void PlayerDied(){// 檢查是否有訂閱者,避免 NullReferenceExceptionOnPlayerDeath?.Invoke(); // C# 6.0 的 ?. 操作符糖,等同于 if (OnPlayerDeath != null) OnPlayerDeath.Invoke();Debug.Log("玩家死亡事件已發布!");}public void ChangeScore(int newScore){OnScoreChanged?.Invoke(newScore);Debug.Log("分數改變事件已發布,新分數: " + newScore);}
}// 事件訂閱者
public class PlayerStats : MonoBehaviour
{private int currentScore = 0;void OnEnable() // 建議在 OnEnable 訂閱,在 OnDisable 取消訂閱{if (GameEventManager.Instance != null){GameEventManager.Instance.OnPlayerDeath += HandlePlayerDeath;GameEventManager.Instance.OnScoreChanged += UpdateScore;Debug.Log("PlayerStats 已訂閱事件。");}}void OnDisable() // 退出時取消訂閱,防止內存泄漏{if (GameEventManager.Instance != null){GameEventManager.Instance.OnPlayerDeath -= HandlePlayerDeath;GameEventManager.Instance.OnScoreChanged -= UpdateScore;Debug.Log("PlayerStats 已取消訂閱事件。");}}void HandlePlayerDeath(){Debug.Log("PlayerStats 收到玩家死亡事件,執行死亡處理邏輯。");// 例如:顯示死亡界面}void UpdateScore(int newScore){currentScore = newScore;Debug.Log($"PlayerStats 收到分數改變事件,當前分數: {currentScore}");// 例如:更新UI顯示}void Update(){// 測試代碼:按下空格鍵觸發玩家死亡事件if (Input.GetKeyDown(KeyCode.Space)){GameEventManager.Instance?.PlayerDied();}// 測試代碼:按下回車鍵改變分數if (Input.GetKeyDown(KeyCode.Return)){GameEventManager.Instance?.ChangeScore(currentScore + 100);}}
}

在這個例子中:

  • GameEventManager 是事件的發布者,它聲明并觸發 OnPlayerDeathOnScoreChanged 事件。
  • PlayerStats 是事件的訂閱者,它通過 += 運算符將自己的方法關聯到 GameEventManager 的事件上。
  • 注意 OnEnableOnDisable 這是 Unity 中管理事件訂閱非常重要的模式。在組件激活時訂閱事件,在組件禁用或銷毀時取消訂閱,可以有效防止因訂閱者被銷毀而發布者仍在觸發事件導致的 NullReferenceException 和內存泄漏問題。

3. 委托與反射的結合:從性能問題引出表達式樹

在上一篇教程中,我們提到了反射的性能開銷,特別是 MethodInfo.Invoke() 方法。雖然它能讓我們動態地調用方法,但每次調用都會有不小的運行時性能損耗。

你可能會想,既然委托就是方法的“引用”,我能不能把反射獲取到的 MethodInfo 轉換為一個委托來調用呢?答案是肯定的,而且這正是 表達式樹 出現的重要原因之一。

C# 提供了一個方法 Delegate.CreateDelegate(),它可以在運行時根據 MethodInfo 創建一個委托。

using System;
using System.Reflection;
using UnityEngine;public class DelegateFromReflectionExample : MonoBehaviour
{public void MyTargetMethod(string msg){Debug.Log("Target method invoked: " + msg);}void Start(){Type type = typeof(DelegateFromReflectionExample);MethodInfo methodInfo = type.GetMethod("MyTargetMethod");if (methodInfo != null){// 嘗試創建委托// 參數1:委托類型 (例如 Action<string>)// 參數2:委托要綁定的對象實例 (如果是靜態方法則為 null)Action<string> myDelegate = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), this, methodInfo);// 通過委托調用方法myDelegate("Hello from Delegate.CreateDelegate!");// 測量性能差異(簡單粗略測試)MeasurePerformance(methodInfo, this);}}void MeasurePerformance(MethodInfo methodInfo, object instance){int iterations = 1000000; // 100萬次迭代// 1. 直接調用long startTime = System.Diagnostics.Stopwatch.GetTimestamp();for (int i = 0; i < iterations; i++){MyTargetMethod("test");}long endTime = System.Diagnostics.Stopwatch.GetTimestamp();double directCallTime = (double)(endTime - startTime) / System.Diagnostics.Stopwatch.Frequency * 1000;Debug.Log($"直接調用 {iterations} 次耗時: {directCallTime:F2} ms");// 2. 反射 InvokestartTime = System.Diagnostics.Stopwatch.GetTimestamp();for (int i = 0; i < iterations; i++){methodInfo.Invoke(instance, new object[] { "test" });}endTime = System.Diagnostics.Stopwatch.GetTimestamp();double reflectionInvokeTime = (double)(endTime - startTime) / System.Diagnostics.Stopwatch.Frequency * 1000;Debug.Log($"反射 Invoke {iterations} 次耗時: {reflectionInvokeTime:F2} ms");// 3. Delegate.CreateDelegate 編譯后的委托Action<string> compiledDelegate = (Action<string>)Delegate.CreateDelegate(typeof(Action<string>), instance, methodInfo);startTime = System.Diagnostics.Stopwatch.GetTimestamp();for (int i = 0; i < iterations; i++){compiledDelegate("test");}endTime = System.Diagnostics.Stopwatch.GetTimestamp();double compiledDelegateTime = (double)(endTime - startTime) / System.Diagnostics.Stopwatch.Frequency * 1000;Debug.Log($"Delegate.CreateDelegate 委托 {iterations} 次耗時: {compiledDelegateTime:F2} ms");//你會發現:直接調用 > Delegate委托 > 反射Invoke。//Delegate.CreateDelegate創建委托的“一次性”開銷,是小于反射Invoke每次調用的開銷的。//尤其是在多次調用同一方法時,委托的性能優勢會非常明顯。}
}

運行上面的代碼,你會觀察到:

  • 直接調用 的性能是最好的。
  • Delegate.CreateDelegate 創建并調用的委托 性能接近直接調用,遠好于 Invoke
  • MethodInfo.Invoke() 的性能是最差的。

這是為什么呢?
Delegate.CreateDelegate 在創建委托時,會執行一次性的編譯工作,將 MethodInfo 轉換為一個高效的委托。一旦這個委托被創建,后續的調用就和直接調用方法幾乎一樣快。而 MethodInfo.Invoke() 每次調用都需要進行一系列的運行時檢查和參數裝箱拆箱操作,開銷較大。

在你的 UIManager 腳本中,你正是利用了這種思想,只不過你用的是更強大、更靈活的 表達式樹 來完成這個“一次性編譯”的工作。表達式樹能夠更細粒度地控制委托的生成,實現更復雜的動態調用邏輯。


總結與展望

委托和事件是 C# 中實現回調解耦的重要機制。

  • 委托 讓你能夠像操作變量一樣操作方法,實現了代碼的動態綁定。
  • 事件 在委托之上提供了一層封裝,構建了安全、可靠的發布/訂閱通信模型,這在 Unity 中尤其適用于 UI、游戲狀態管理和模塊間通信。

了解并熟練運用它們,將極大地提升你代碼的靈活性、可維護性和擴展性。

然而,當我們需要在運行時根據類型信息動態生成復雜的代碼邏輯,并追求極致的性能時,僅僅依靠 Delegate.CreateDelegate 就不夠了。這就是 表達式樹 大展身手的地方。

在下一篇教程中,我們將深入探索 表達式樹,理解它如何讓我們在運行時像寫代碼一樣“構建代碼”,并將其編譯成高性能的委托,最終揭示我的框架中的 UIManagerCacheInitDelegate 方法的原理。

動態編程入門第一節:C# 反射 - Unity 開發者的超級工具箱
動態編程入門第二節:委托與事件 - Unity 開發者的高級回調與通信藝術

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/87780.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/87780.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/87780.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

降低網絡安全中的人為風險:以人為本的路徑

有效降低網絡安全中的人為風險&#xff0c;關鍵在于采取以人為本的方法。這種方法的核心在于通過高效的培訓和實踐&#xff0c;使員工掌握安全知識、踐行安全行為&#xff0c;并最終培育出安全且相互支持的文化氛圍。 誠然&#xff0c;技術和政策必須為良好的安全行為提供支持、…

opencv裁剪和編譯

opencv裁剪和編譯 0. 準備工作 0.1 下載和安裝Eigen 地址 https://eigen.tuxfamily.org/index.php?titleMain_Page對于opencv編譯&#xff0c;需要增加EIGEN_INCLUDE_PATH和開啟WITH_EIGEN -DWITH_EIGENON -DEIGEN_INCLUDE_PATH./3rd/eigen-3.4.01. 實際腳本 編譯腳本如下: ch…

小白成長之路-mysql數據基礎(三)

文章目錄一、主從復制二、案例總結一、主從復制 1、master開啟二進制日志記錄2、slave開啟IO進程&#xff0c;從master中讀取二進制日志并寫入slave的中繼日志3、slave開啟SQL進程&#xff0c;從中繼日志中讀取二進制日志并進行重放4、最終&#xff0c;達到slave與master中數據…

通過 Windows 共享文件夾 + 手機訪問(SMB協議)如何實現

通過 Windows 共享文件夾 手機訪問&#xff08;SMB協議&#xff09; 實現 PC 和安卓手機局域網文件共享&#xff0c;具體步驟如下&#xff1a; &#x1f4cc; 前置條件 電腦和手機連接同一局域網&#xff08;同一個Wi-Fi或路由器&#xff09;。關閉防火墻或放行SMB端口&#…

【Python3教程】Python3高級篇之正則表達式

博主介紹:?全網粉絲23W+,CSDN博客專家、Java領域優質創作者,掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域? 技術范圍:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大數據、物聯網、機器學習等設計與開發。 感興趣的可…

Redis--黑馬點評--達人探店功能實現詳解

達人探店發布探店筆記探店筆記類似于點評網站的評價&#xff0c;往往是圖文結合&#xff0c;對應的表有兩個&#xff1a;tb_blog&#xff1a;探店筆記表&#xff0c;包含筆記中的標題、文字、圖片等tb_blog_comments&#xff1a;其他用戶對探店筆記的評價tb_blog表結構如下&…

一探 3D 互動展廳的神奇構造?

3D 互動展廳的神奇之處&#xff0c;離不開一系列先進技術的強力支撐 。其中&#xff0c;VR(虛擬現實)技術無疑是核心亮點之一。通過佩戴 VR 設備&#xff0c;觀眾仿佛被瞬間 “傳送” 到一個全新的世界&#xff0c;能夠全身心地沉浸其中&#xff0c;360 度無死角地觀察周圍的一…

C++ 網絡編程(15) 利用asio協程搭建異步服務器

&#x1f680; [協程與異步服務器實戰]&#xff1a;[C20協程原理與Boost.Asio異步服務器開發] &#x1f4c5; 更新時間&#xff1a;2025年07月05日 &#x1f3f7;? 標簽&#xff1a;C20 | 協程 | Boost.Asio | 異步編程 | 網絡服務器 文章目錄前言一、什么是協程&#xff1f;二…

【Java21】在spring boot中使用虛擬線程

文章目錄 0.環境說明1.原理解析2.spring boot的方案3.注意事項&#xff08;施工中&#xff0c;歡迎補充&#xff09; 前置知識 虛擬線程VT&#xff08;Virtual Thread&#xff09; 0.環境說明 用于驗證的版本&#xff1a; spring boot: 3.3.3jdk: OpenJDK 21.0.5 spring boot…

利器:NPM和YARN及其他

文章目錄**1. 安裝 Yarn&#xff08;推薦方法&#xff09;****2. 驗證安裝****3. 常見問題及解決方法****① 權限不足&#xff08;Error: EPERM&#xff09;****② 網絡問題&#xff08;連接超時或下載失敗&#xff09;****③ 環境變量未正確配置****4. 替代安裝方法&#xff0…

跨平臺直播美顏SDK集成實錄:Android/iOS如何適配貼紙功能

眾所周知&#xff0c;直播平臺與短視頻平臺的貼紙功能不僅是用戶表達個性的方式&#xff0c;更是平臺提高用戶粘性和互動轉化的法寶。 可問題來了&#xff1a;如何讓一個貼紙功能&#xff0c;在Android和iOS兩大平臺上表現一致、運行流暢、加載穩定&#xff1f;這背后&#xff…

JavaWeb(蒼穹外賣)--學習筆記04(前端:HTML,CSS,JavaScript)

前言 本片文章是學習B站黑馬程序員蒼穹外賣的學習筆記。因為最近期末周&#xff0c;一直在應付考試所以就學的很少&#xff0c;恰好視頻中在講Nginx反向代理和負載均衡&#xff08;寫著對前端的內容做一個復習&#xff09; 概述&#xff1a; 1.web前端主要由三部分組成&…

智能學號抽取系統 V5.4.3.2 —— Vue.js 實現的多功能課堂隨機抽簽工具

智能學號抽取系統 V5.4.3.2 —— Vue.js 實現的多功能課堂隨機抽簽工具 在教學或會議場景中&#xff0c;我們經常需要隨機抽取一個或多個學號/編號來決定發言者、答題者或者參與者。為了提高效率和公平性&#xff0c;我們可以使用一些智能化的小工具來實現這一過程。 今天介紹…

從0開始學習R語言--Day39--Spearman 秩相關

在非參數統計中&#xff0c;不看數據的實際數值&#xff0c;單純比較兩組變量的值的排名是通用的基本方法&#xff0c;但在客觀數據中&#xff0c;很多變量的關系都是非線性的&#xff0c;其他的方法不是對樣本數據的大小和線性有要求&#xff0c;就是只能對比數據的差異性&…

WSL - Linux 安裝 Anaconda3-2025.06-0 詳細教程 [WSL 分發版均適用]

一、檢查系統狀態 安裝前先確認 WSL - Linxu 已正常啟動&#xff08;比如 Ubuntu&#xff09;&#xff0c;網絡連接穩定&#xff0c;并且系統磁盤有足夠空間&#xff0c;一般建議預留至少 5GB 以上的可用空間&#xff0c;避免因空間不足導致安裝失敗。 二、下載安裝包 Anacond…

熱血三國建筑攻略表格

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>熱血三國建筑攻略表格</title><style>…

SpringBoot+MySQL醫院掛號系統源碼

概述 基于SpringBootMySQL開發的醫院掛號系統完整源碼&#xff0c;該系統功能完善&#xff0c;包含從患者掛號到醫生管理的全流程解決方案&#xff0c;采用主流技術棧開發&#xff0c;代碼規范易于二次開發。 主要內容 系統包含完整的前后臺功能模塊&#xff1a; ??前臺功…

Linux系統之MySQL數據庫基礎

目錄 一、概述 數據庫概念 數據庫的類型 關系型數據庫模型 關系數據庫相關概念 二、安裝 1、mariadb安裝 2、mysql安裝 3、啟動并開機自啟 4、本地連接&#xff08;本地登錄&#xff09; 三、mysqld數據庫配置與命令 yum安裝后生成的目錄 mysqld服務器的啟動腳本 …

MySQL--InnoDB存儲引擎--頁結構

目錄 一、頁的大小 二、頁的分類 三、頁頭和頁尾 3.1 頁頭--File Header 3.2 頁尾--File Trailer 3.3 LSN 四、數據行 五、頁中數據的查詢 六、事務和索引在頁中的記錄 一、頁的大小 前面介紹了每個數據頁默認大小為16KB&#xff0c;是操作系統“數據塊” 4KB 的整數倍…

卡車檢測數據集-700張圖片交通運輸管理 智能監控系統 道路安全監測

跌倒檢測數據集-4500張圖片&#x1f4e6; 已發布目標檢測數據集合集&#xff08;持續更新&#xff09;&#x1f69b; Deteccin de carpa 2 Computer Vision Project&#x1f4cc; 數據集概覽包含類別&#x1f3af; 應用場景&#x1f5bc; 數據樣本展示&#x1f527; 使用建議&a…