Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
PyTorch系列文章目录
Python系列文章目录
C#系列文章目录
01-C#与游戏开发的初次见面:从零开始的Unity之旅
02-C#入门:从变量与数据类型开始你的游戏开发之旅
03-C#运算符与表达式:从入门到游戏伤害计算实践
04-从零开始学C#:用if-else和switch打造智能游戏逻辑
05-掌握C#循环:for、while、break与continue详解及游戏案例
06-玩转C#函数:参数、返回值与游戏中的攻击逻辑封装
07-Unity游戏开发入门:用C#控制游戏对象移动
08-C#面向对象编程基础:类的定义、属性与字段详解
09-C#封装与访问修饰符:保护数据安全的利器
10-如何用C#继承提升游戏开发效率?Enemy与Boss案例解析
11-C#多态性入门:从零到游戏开发实战
12-C#接口王者之路:从入门到Unity游戏开发实战 (IAttackable案例详解)
13-C#静态成员揭秘:共享数据与方法的利器
14-Unity 面向对象实战:掌握组件化设计与脚本通信,构建玩家敌人交互
15-C#入门 Day15:彻底搞懂数组!从基础到游戏子弹管理实战
16-C# List 从入门到实战:掌握动态数组,轻松管理游戏敌人列表 (含代码示例)
17-C# 字典 (Dictionary) 完全指南:从入门到游戏属性表实战 (Day 17)
18-C#游戏开发【第18天】 | 深入理解队列(Queue)与栈(Stack):从基础到任务队列实战
19-【C# 进阶】深入理解枚举 Flags 属性:游戏开发中多状态组合的利器
20-C#结构体(Struct)深度解析:轻量数据容器与游戏开发应用 (Day 20)
21-Unity数据持久化进阶:告别硬编码,用ScriptableObject优雅管理游戏配置!(Day 21)
22-Unity C# 健壮性编程:告别崩溃!掌握异常处理与调试的 4 大核心技巧 (Day 22)
23-C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)
文章目录
前言
欢迎来到 C# for Unity 学习之旅的第 23 天!在前一天的学习中,我们探讨了如何通过异常处理和调试来增强代码的健壮性。今天,我们将深入 C# 的一个核心特性——委托(Delegate)和事件(Event)。它们是实现事件驱动编程和代码解耦的强大武器,对于构建灵活、可维护的系统(尤其是在像 Unity 这样复杂的开发环境中)至关重要。理解委托和事件,能让你在不同模块间建立松耦合的通信机制,告别“牵一发而动全身”的窘境。
一、理解委托 (Delegate)
委托是 C# 中一个非常核心且强大的概念,它为我们处理方法(函数)引用提供了一种类型安全的方式。
1.1 什么是委托?
从本质上讲,委托(Delegate) 是一种引用类型,它可以 “持有” 对一个或多个方法的引用。你可以把它想象成一个方法的“指针”,或者一个方法的“契约”。
- 类型安全的方法指针: 与 C/C++ 中的函数指针类似,但委托是类型安全的。它规定了它所能指向的方法必须具有特定的参数列表和返回类型。
- 解耦的桥梁: 委托允许我们将方法的调用者和方法本身解耦。调用者不需要知道具体是哪个方法会被执行,只需要知道这个方法符合委托定义的“契约”即可。这为程序的灵活性和可扩展性提供了巨大便利。
- 事件的基础: 委托是实现 C# 事件机制的基础。
类比: 想象一下,你有一个遥控器(委托),这个遥控器上有一个“播放”按钮。这个按钮可以控制不同的设备(方法),比如电视、音响或投影仪,只要这些设备都提供了符合“播放”指令的操作。你按下按钮时,并不需要关心具体是哪个设备在响应,只需要知道按下了“播放”这个动作。
1.2 委托的声明、实例化与调用
在 C# 中使用委托通常涉及三个步骤:声明委托类型、实例化委托、调用委托。
1.2.1 声明委托类型
使用 delegate
关键字来声明一个新的委托类型。声明时需要指定它所能引用的方法的签名(返回类型和参数列表)。
// 声明一个委托类型 MyDelegate
// 它可以引用任何 返回值为 void 且 接受一个 string 参数 的方法
public delegate void MyDelegate(string message);
1.2.2 实例化委托
声明了委托类型后,就可以创建该类型的实例,并让它指向一个或多个符合其签名的方法。
using System;
// 声明委托类型
public delegate void MyDelegate(string message);
public class DelegateExample
{
// 一个符合 MyDelegate 签名的方法
public static void MethodA(string msg)
{
Console.WriteLine($"MethodA executed with message: {
msg}");
}
// 另一个符合 MyDelegate 签名的方法
public static void MethodB(string info)
{
Console.WriteLine($"MethodB received info: {
info}");
}
public static void Main(string[] args) // 非Unity环境入口点示例
{
// 1. 实例化委托,指向 MethodA
MyDelegate delegateInstance1 = new MyDelegate(MethodA);
// 2. 更简洁的语法糖 (常用)
MyDelegate delegateInstance2 = MethodB;
// ... 调用将在下一节展示 ...
}
}
1.2.3 调用委托
调用委托实例就像调用一个普通方法一样。执行委托调用时,它会依次调用其引用的所有方法。
using System;
public delegate void MyDelegate(string message);
public class DelegateExample
{
public static void MethodA(string msg)
{
Console.WriteLine($"MethodA executed with message: {
msg}");
}
public static void MethodB(string info)
{
Console.WriteLine($"MethodB received info: {
info}");
}
public static void Main(string[] args)
{
MyDelegate delegateInstance1 = MethodA; // 指向 MethodA
MyDelegate delegateInstance2 = MethodB; // 指向 MethodB
// 调用委托实例1
Console.WriteLine("Calling delegateInstance1:");
delegateInstance1("Hello from Delegate 1!"); // 输出: MethodA executed with message: Hello from Delegate 1!
// 调用委托实例2
Console.WriteLine("\nCalling delegateInstance2:");
delegateInstance2.Invoke("Info for Delegate 2"); // 使用 Invoke() 方法调用,效果相同
// 输出: MethodB received info: Info for Delegate 2
}
}
1.3 多播委托 (Multicast Delegates)
C# 中的委托天生就是多播(Multicast)的,这意味着一个委托实例可以同时引用多个方法。当调用这个委托时,它会按照添加的顺序依次调用所有引用的方法。
1.3.1 添加与移除方法引用
使用 +
或 +=
运算符可以将方法添加到委托的调用列表中。使用 -
或 -=
运算符可以从中移除方法。
using System;
public delegate void MyDelegate(string message);
public class MulticastDelegateExample
{
public static void Listener1(string msg)
{
Console.WriteLine($"Listener 1 received: {
msg}");
}
public static void Listener2(string msg)
{
Console.WriteLine($"Listener 2 heard: {
msg}");
}
public static void Listener3(string msg)
{
Console.WriteLine($"Listener 3 got the message: {
msg}");
}
public static void Main(string[] args)
{
MyDelegate multicastDelegate = null; // 初始化为 null
// 使用 += 添加方法引用
multicastDelegate += Listener1;
multicastDelegate += Listener2;
multicastDelegate += Listener3;
Console.WriteLine("Invoking multicast delegate (3 listeners):");
// 调用委托,会依次执行 Listener1, Listener2, Listener3
multicastDelegate?.Invoke("Important News!"); // 使用 ?. 安全调用,防止委托为 null
Console.WriteLine("\nRemoving Listener2...");
// 使用 -= 移除方法引用
multicastDelegate -= Listener2;
Console.WriteLine("\nInvoking multicast delegate again (2 listeners):");
// 再次调用,只会执行 Listener1, Listener3
multicastDelegate?.Invoke("Another Update!");
}
}
输出:
Invoking multicast delegate (3 listeners):
Listener 1 received: Important News!
Listener 2 heard: Important News!
Listener 3 got the message: Important News!
Removing Listener2...
Invoking multicast delegate again (2 listeners):
Listener 1 received: Another Update!
Listener 3 got the message: Another Update!
1.3.2 多播委托的应用场景
多播委托最典型的应用场景就是实现一对多通知机制,也就是观察者模式。一个事件发生时(比如玩家升级),需要通知多个对该事件感兴趣的对象(比如更新UI、播放音效、记录日志等)。
二、深入事件 (Event)
虽然委托本身就能实现一对多通知,但直接将委托实例暴露给外部存在一些风险。事件(Event)的出现正是为了解决这些问题,提供一种更安全、更符合面向对象封装原则的发布-订阅模式实现。
2.1 为何需要事件?
直接使用公共委托(public MyDelegate myDelegate;
)作为通知机制,可能会遇到以下问题:
- 外部随意覆盖: 任何外部代码都可以通过
=
操作符直接将委托实例指向一个新的方法,从而清空之前所有的订阅者。 - 外部随意调用: 任何外部代码都可以直接调用该委托,触发通知,这可能不是我们期望的行为(通常只有事件的发布者才有权触发)。
- 封装性不足: 暴露委托实例破坏了类的封装性。
事件(Event) 通过在委托的基础上增加一层访问限制来解决这些问题:
- 限制外部访问: 外部类只能对事件进行
+=
(订阅)和-=
(取消订阅)操作。 - 封装触发逻辑: 只有声明事件的类(发布者)内部才能触发该事件(调用其底层的委托)。
事件本质上是对委托实例的一种封装,它强制实现了安全的发布-订阅模式。
2.2 事件的声明、订阅与触发
事件的声明使用 event
关键字,通常与一个委托类型一起使用。
2.2.1 声明事件
using System;
// 假设我们已经声明了委托类型 MyDelegate
// public delegate void MyDelegate(string message);
public class Publisher
{
// 声明一个基于 MyDelegate 类型的事件 MyEvent
// 通常声明为 public,以便外部订阅
public event MyDelegate MyEvent;
// 一个方法,用于触发事件
public void RaiseEvent(string data)
{
Console.WriteLine("Publisher is about to raise the event...");
// 触发事件:在类内部调用底层委托
// 使用 ?. 安全调用,确保有订阅者时才触发
MyEvent?.Invoke(data);
Console.WriteLine("Publisher finished raising the event.");
}
}
2.2.2 订阅事件
外部类(订阅者)通过 +=
操作符将自己的方法(事件处理器)注册到事件上。
public class Subscriber
{
private string _name;
public Subscriber(string name)
{
_name = name;
}
// 事件处理器方法,签名必须与事件的委托类型匹配
public void HandleEvent(string message)
{
Console.WriteLine($"{
_name} received event with message: {
message}");
}
// 订阅事件的方法 (通常在初始化时调用)
public void Subscribe(Publisher publisher)
{
// 使用 += 订阅事件
publisher.MyEvent += HandleEvent;
Console.WriteLine($"{
_name} subscribed to MyEvent.");
}
// 取消订阅事件的方法 (在不再需要时调用,如对象销毁时)
public void Unsubscribe(Publisher publisher)
{
// 使用 -= 取消订阅
publisher.MyEvent -= HandleEvent;
Console.WriteLine($"{
_name} unsubscribed from MyEvent.");
}
}
2.2.3 触发事件
事件只能在声明它的类内部被触发。触发事件实际上就是调用与事件关联的那个(可能为多播)委托实例。
using System;
// (此处省略 Publisher, Subscriber, MyDelegate 的代码)
public class EventDemo
{
public static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber1 = new Subscriber("Subscriber A");
Subscriber subscriber2 = new Subscriber("Subscriber B");
// 订阅者进行订阅
subscriber1.Subscribe(publisher);
subscriber2.Subscribe(publisher);
Console.WriteLine("\n--- Triggering Event ---");
// 发布者触发事件
publisher.RaiseEvent("First Notification!");
Console.WriteLine("\n--- Subscriber A Unsubscribes ---");
// 订阅者A取消订阅
subscriber1.Unsubscribe(publisher);
Console.WriteLine("\n--- Triggering Event Again ---");
// 再次触发事件,只有订阅者B会收到
publisher.RaiseEvent("Second Notification!");
}
}
输出:
Subscriber A subscribed to MyEvent.
Subscriber B subscribed to MyEvent.
--- Triggering Event ---
Publisher is about to raise the event...
Subscriber A received event with message: First Notification!
Subscriber B received event with message: First Notification!
Publisher finished raising the event.
--- Subscriber A Unsubscribes ---
Subscriber A unsubscribed from MyEvent.
--- Triggering Event Again ---
Publisher is about to raise the event...
Subscriber B received event with message: Second Notification!
Publisher finished raising the event.
2.3 事件与委托的关系
- 事件是基于委托的。 每个事件都有一个关联的(私有)委托实例。
- 事件提供了更严格的访问控制。 它限制了外部代码只能进行订阅 (
+=
) 和取消订阅 (-=
) 操作,而不能直接赋值或触发。 - 事件是实现观察者模式的标准方式。 它定义了清晰的发布者(拥有事件)和订阅者(响应事件)角色。
三、简化代码:Func与Action泛型委托
每次都为不同的方法签名声明一个新的委托类型可能会很繁琐。.NET Framework 提供了内置的泛型委托 Action
和 Func
,它们可以覆盖绝大多数常见的方法签名,从而简化代码。
3.1 Action委托
Action
泛型委托用于引用 不返回值(void
) 的方法。它有多个重载版本,可以接受 0 到 16 个输入参数。
Action
: 引用无参数、无返回值的方法。Action<T>
: 引用带一个T
类型参数、无返回值的方法。Action<T1, T2>
: 引用带两个参数(T1
,T2
类型)、无返回值的方法。- …以此类推
示例:
using System;
public class ActionExample
{
public static void PrintMessage()
{
Console.WriteLine("Hello!");
}
public static void PrintNumber(int num)
{
Console.WriteLine($"Number: {
num}");
}
public static void PrintSum(int a, int b)
{
Console.WriteLine($"Sum: {
a + b}");
}
public static void Main(string[] args)
{
// Action 委托,指向无参无返回值方法
Action action1 = PrintMessage;
action1(); // 输出: Hello!
// Action<int> 委托,指向带一个 int 参数、无返回值方法
Action<int> action2 = PrintNumber;
action2(100); // 输出: Number: 100
// Action<int, int> 委托
Action<int, int> action3 = PrintSum;
action3(5, 3); // 输出: Sum: 8
// 事件也可以使用 Action
// event Action OnSomethingHappened;
// event Action<string> OnMessageReceived;
}
}
3.2 Func委托
Func
泛型委托用于引用 有返回值 的方法。它的最后一个泛型参数表示 返回值的类型,前面的泛型参数(如果有的话)表示输入参数的类型。
Func<TResult>
: 引用无参数、返回TResult
类型值的方法。Func<T, TResult>
: 引用带一个T
类型参数、返回TResult
类型值的方法。Func<T1, T2, TResult>
: 引用带两个参数(T1
,T2
类型)、返回TResult
类型值的方法。- …以此类推 (最多接受 16 个输入参数)
示例:
using System;
public class FuncExample
{
public static string GetGreeting()
{
return "Good morning!";
}
public static int Add(int x, int y)
{
return x + y;
}
public static bool IsPositive(float value)
{
return value > 0;
}
public static void Main(string[] args)
{
// Func<string> 委托,指向无参、返回 string 的方法
Func<string> func1 = GetGreeting;
string greeting = func1();
Console.WriteLine(greeting); // 输出: Good morning!
// Func<int, int, int> 委托,指向带两个 int 参数、返回 int 的方法
Func<int, int, int> func2 = Add;
int sum = func2(10, 20);
Console.WriteLine($"Sum: {
sum}"); // 输出: Sum: 30
// Func<float, bool> 委托
Func<float, bool> func3 = IsPositive;
bool result = func3(-5.5f);
Console.WriteLine($"Is -5.5 positive? {
result}"); // 输出: Is -5.5 positive? False
}
}
3.3 何时使用泛型委托
- 简化声明: 当你需要一个委托来引用具有常见签名(尤其是无返回值或接受少量参数)的方法时,优先考虑使用
Action
或Func
,避免不必要的自定义委托声明。 - 标准化: 使用标准泛型委托可以提高代码的可读性和一致性。
- LINQ 和 Lambda 表达式:
Func
和Action
在 LINQ 查询和 Lambda 表达式中被广泛使用。
在事件中的应用:
许多现代 C# 代码和库(包括 Unity 的某些部分)倾向于使用 Action
或 System.EventHandler<TEventArgs>
(本质上也基于 Action
)作为事件的基础委托类型。
public class ModernPublisher
{
// 使用 Action<string> 作为事件的委托类型
public event Action<string> MessageSent;
public void SendMessage(string message)
{
MessageSent?.Invoke(message);
}
}
public class ModernSubscriber
{
public void HandleMessage(string msg) {
/* ... */ }
public void Subscribe(ModernPublisher p)
{
p.MessageSent += HandleMessage; // 订阅方式不变
}
}
四、实战演练:构建消息通知系统
现在,我们来创建一个简单的非 MonoBehaviour 类示例,演示如何使用委托和事件来实现一个基础的消息通知系统。
4.1 场景设定
我们假设有一个 Notifier
类,它负责发布某种消息(例如,进度更新)。同时,有多个 Listener
类对这些消息感兴趣,并希望在消息发布时执行各自的操作。
Notifier
:消息发布者。Listener
:消息订阅者/监听者。
4.2 使用委托实现 (对比演示,非推荐)
如果我们直接使用公共委托(不推荐):
// --- 不推荐的方式 ---
public class Notifier_Unsafe
{
// 直接暴露公共委托
public Action<string> OnNotify;
public void Notify(string message)
{
Console.WriteLine($"Notifier sending (unsafe): {
message}");
OnNotify?.Invoke(message);
}
}
// 外部可以直接执行 notifier.OnNotify = null; 或 notifier.OnNotify("Fake Message");
这种方式缺乏封装和安全性。
4.3 使用事件优化
现在我们使用事件来重构 Notifier
类,提供更安全的通知机制。
using System;
using System.Collections.Generic; // 用于管理 Listener 实例
// --- 推荐的方式:使用事件 ---
public class Notifier
{
// 1. 声明基于 Action<string> 委托的事件
public event Action<string> ProgressUpdated;
// 模拟一个耗时操作,并定期发布进度更新
public void DoWork()
{
for (int i = 0; i <= 10; i++)
{
// 模拟工作
System.Threading.Thread.Sleep(500); // 暂停 500 毫秒
// 2. 在类内部触发事件
string message = $"Work progress: {
i * 10}%";
Console.WriteLine($"Notifier: Raising ProgressUpdated event with message: '{
message}'");
OnProgressUpdated(message); // 调用辅助方法触发
}
}
// 3. 封装触发逻辑的辅助方法 (可选但推荐)
protected virtual void OnProgressUpdated(string progressMessage)
{
// 使用 ?. 安全调用,只有当有订阅者时才触发事件
ProgressUpdated?.Invoke(progressMessage);
}
}
public class Listener
{
private string _listenerName;
public Listener(string name)
{
_listenerName = name;
}
// 4. 事件处理器方法 (签名需匹配 Action<string>)
public void HandleProgressUpdate(string message)
{
Console.WriteLine($" [{
_listenerName}] Received update: {
message}");
}
// 5. 订阅事件
public void Subscribe(Notifier notifier)
{
notifier.ProgressUpdated += HandleProgressUpdate;
Console.WriteLine($"[{
_listenerName}] Subscribed to ProgressUpdated event.");
}
// 6. 取消订阅事件 (好习惯!)
public void Unsubscribe(Notifier notifier)
{
notifier.ProgressUpdated -= HandleProgressUpdate;
Console.WriteLine($"[{
_listenerName}] Unsubscribed from ProgressUpdated event.");
}
}
4.4 代码示例与解析
将上面的 Notifier
和 Listener
类组合起来运行。
using System;
// (此处省略 Notifier 和 Listener 类的代码)
public class NotificationSystemDemo
{
public static void Main(string[] args) // C# 程序入口点
{
// 创建发布者实例
Notifier notifier = new Notifier();
// 创建多个监听者实例
Listener listenerA = new Listener("UI Updater");
Listener listenerB = new Listener("Logger");
Listener listenerC = new Listener("Audio Player");
// 监听者订阅事件
listenerA.Subscribe(notifier);
listenerB.Subscribe(notifier);
listenerC.Subscribe(notifier);
Console.WriteLine("\nStarting work process...\n");
// 发布者开始工作并触发事件
notifier.DoWork();
Console.WriteLine("\nWork finished.");
// 演示取消订阅 (例如,某个监听者不再需要通知)
Console.WriteLine("\nLogger unsubscribing...");
listenerB.Unsubscribe(notifier);
// 如果 Notifier 再次触发事件,Logger 将不会收到
Console.WriteLine("\nDemo finished. Press any key to exit.");
Console.ReadKey();
}
}
代码解析:
Notifier
类声明了一个ProgressUpdated
事件,基于Action<string>
委托。这意味着任何订阅该事件的方法都必须接受一个string
参数且无返回值。Notifier
类内部通过调用OnProgressUpdated
方法来触发事件。这个方法使用了?.Invoke()
来安全地调用委托,只有当ProgressUpdated
事件至少有一个订阅者时,Invoke
才会被执行。Listener
类有一个HandleProgressUpdate
方法,其签名与Action<string>
匹配,作为事件处理器。- 在
Main
方法中,创建了Notifier
和多个Listener
实例。 - 每个
Listener
通过调用Subscribe
方法,使用+=
将自己的HandleProgressUpdate
方法注册(订阅)到Notifier
的ProgressUpdated
事件上。 - 当
Notifier
的DoWork
方法执行并调用OnProgressUpdated
时,所有已订阅的Listener
的HandleProgressUpdate
方法都会被自动依次调用,并收到相同的progressMessage
。 Notifier
不需要知道有哪些Listener
,也不需要知道它们具体做什么。它只负责在适当的时候发布(触发)事件。Listener
也不需要直接了解Notifier
的内部实现细节,只需要知道如何订阅(+=
)和响应(事件处理器方法)感兴趣的事件即可。- 这种方式实现了松耦合(Loose Coupling):
Notifier
和Listener
之间通过事件这个中介进行通信,相互依赖性大大降低。
五、常见问题与注意事项
5.1 空检查 (Null Check)
在触发事件(调用委托)之前,务必进行空检查,否则如果没有任何订阅者,尝试调用 null
的委托实例会导致 NullReferenceException
。
// 不安全的调用
// MyEvent(args); // 如果 MyEvent 为 null,会抛出异常
// 安全的调用方式
// 1. 使用 if 判断
if (MyEvent != null)
{
MyEvent(args);
}
// 2. 使用 C# 6.0 及以上版本的 Null 条件运算符 ?. (推荐)
MyEvent?.Invoke(args);
5.2 取消订阅的重要性 (Importance of Unsubscribing)
如果一个订阅者对象生命周期结束(例如,在 Unity 中一个 GameObject 被销毁),但它没有从它订阅的事件中取消订阅(使用 -=
),那么发布者仍然会持有对该订阅者方法的引用。这会导致:
- 内存泄漏: 只要发布者还存在,被销毁的订阅者对象就无法被垃圾回收器回收。
- 潜在错误: 如果发布者触发事件,尝试调用一个已经被销毁对象的方法,可能会导致异常或意外行为。
最佳实践: 在订阅者不再需要监听事件时(例如,在 Unity 脚本的 OnDestroy()
或 OnDisable()
方法中),务必执行取消订阅操作。
public class MyComponent : MonoBehaviour // 假设在 Unity 中
{
private SomeGlobalNotifier _notifier;
void OnEnable()
{
_notifier = FindObjectOfType<SomeGlobalNotifier>(); // 示例:找到发布者
if (_notifier != null)
{
_notifier.SomeEvent += HandleSomeEvent; // 订阅
}
}
void OnDisable() // 或者 OnDestroy()
{
if (_notifier != null)
{
_notifier.SomeEvent -= HandleSomeEvent; // 取消订阅!
}
}
void HandleSomeEvent() {
/* ... */ }
}
5.3 线程安全 (Thread Safety)
标准的 C# 事件(以及其底层的多播委托)在多线程环境下进行订阅 (+=
)、取消订阅 (-=
) 和调用 (Invoke
) 不是线程安全的。如果在多个线程中并发地修改或调用事件,可能会导致竞态条件或数据损坏。
如果需要在多线程环境中使用事件,需要自行实现同步机制(例如使用 lock
语句保护相关操作),或者考虑使用专门为并发设计的类库(如 .NET 的 Concurrent
集合或 Reactive Extensions (Rx.NET))。对于大多数 Unity 游戏逻辑(通常在主线程运行),这个问题不太常见,但在进行异步操作或与后台线程交互时需要注意。
六、总结
今天我们深入学习了 C# 中用于实现代码解耦和事件驱动编程的核心机制——委托与事件:
- 委托 (Delegate) 是一种类型安全的方法引用,可以指向一个或多个具有相同签名的方法。它是实现回调和事件的基础。
- 多播委托 (Multicast Delegate) 允许一个委托实例持有对多个方法的引用,并通过一次调用触发所有方法,非常适合“一对多”通知场景。
- 事件 (Event) 是对委托的一种封装,提供了更安全的发布-订阅模式。它限制外部只能进行订阅 (
+=
) 和取消订阅 (-=
),而触发权保留在发布者类内部。 Action
和Func
泛型委托 是 .NET 提供的内置委托类型,分别用于引用无返回值和有返回值的方法,可以简化代码,避免声明不必要的自定义委托类型。- 委托和事件是实现代码解耦的关键。 它们允许对象在不知道彼此具体实现的情况下进行通信,提高了代码的灵活性、可维护性和可扩展性。
- 使用事件时务必注意:触发前进行空检查 (
?.Invoke()
),并在不再需要时及时取消订阅 (-=
) 以避免内存泄漏。
掌握委托与事件,将为我们后续学习 Unity 中的事件系统(如 UnityEvent
)以及构建更复杂的游戏逻辑打下坚实的基础。明天,我们将专门探讨 Unity 内置的事件系统 UnityEvent
,看看它与 C# 原生事件有何异同,以及如何在 Unity 项目中更便捷地实现脚本间的通信。