动态编程入门第一节: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…