C#代码解耦利器:委托与事件(Delegate & Event)从入门到实践 (Day 23)

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;)作为通知机制,可能会遇到以下问题:

  1. 外部随意覆盖: 任何外部代码都可以通过 = 操作符直接将委托实例指向一个新的方法,从而清空之前所有的订阅者。
  2. 外部随意调用: 任何外部代码都可以直接调用该委托,触发通知,这可能不是我们期望的行为(通常只有事件的发布者才有权触发)。
  3. 封装性不足: 暴露委托实例破坏了类的封装性。

事件(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 提供了内置的泛型委托 ActionFunc,它们可以覆盖绝大多数常见的方法签名,从而简化代码。

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 何时使用泛型委托

  • 简化声明: 当你需要一个委托来引用具有常见签名(尤其是无返回值或接受少量参数)的方法时,优先考虑使用 ActionFunc,避免不必要的自定义委托声明。
  • 标准化: 使用标准泛型委托可以提高代码的可读性和一致性。
  • LINQ 和 Lambda 表达式: FuncAction 在 LINQ 查询和 Lambda 表达式中被广泛使用。

在事件中的应用:
许多现代 C# 代码和库(包括 Unity 的某些部分)倾向于使用 ActionSystem.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 代码示例与解析

将上面的 NotifierListener 类组合起来运行。

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();
    }
}

代码解析:

  1. Notifier 类声明了一个 ProgressUpdated 事件,基于 Action<string> 委托。这意味着任何订阅该事件的方法都必须接受一个 string 参数且无返回值。
  2. Notifier 类内部通过调用 OnProgressUpdated 方法来触发事件。这个方法使用了 ?.Invoke() 来安全地调用委托,只有当 ProgressUpdated 事件至少有一个订阅者时,Invoke 才会被执行。
  3. Listener 类有一个 HandleProgressUpdate 方法,其签名与 Action<string> 匹配,作为事件处理器。
  4. Main 方法中,创建了 Notifier 和多个 Listener 实例。
  5. 每个 Listener 通过调用 Subscribe 方法,使用 += 将自己的 HandleProgressUpdate 方法注册(订阅)到 NotifierProgressUpdated 事件上。
  6. NotifierDoWork 方法执行并调用 OnProgressUpdated 时,所有已订阅的 ListenerHandleProgressUpdate 方法都会被自动依次调用,并收到相同的 progressMessage
  7. Notifier 不需要知道有哪些 Listener,也不需要知道它们具体做什么。它只负责在适当的时候发布(触发)事件。
  8. Listener 也不需要直接了解 Notifier 的内部实现细节,只需要知道如何订阅(+=)和响应(事件处理器方法)感兴趣的事件即可。
  9. 这种方式实现了松耦合(Loose Coupling)NotifierListener 之间通过事件这个中介进行通信,相互依赖性大大降低。

五、常见问题与注意事项

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# 中用于实现代码解耦和事件驱动编程的核心机制——委托与事件:

  1. 委托 (Delegate) 是一种类型安全的方法引用,可以指向一个或多个具有相同签名的方法。它是实现回调和事件的基础。
  2. 多播委托 (Multicast Delegate) 允许一个委托实例持有对多个方法的引用,并通过一次调用触发所有方法,非常适合“一对多”通知场景。
  3. 事件 (Event) 是对委托的一种封装,提供了更安全的发布-订阅模式。它限制外部只能进行订阅 (+=) 和取消订阅 (-=),而触发权保留在发布者类内部。
  4. ActionFunc 泛型委托 是 .NET 提供的内置委托类型,分别用于引用无返回值和有返回值的方法,可以简化代码,避免声明不必要的自定义委托类型。
  5. 委托和事件是实现代码解耦的关键。 它们允许对象在不知道彼此具体实现的情况下进行通信,提高了代码的灵活性、可维护性和可扩展性。
  6. 使用事件时务必注意:触发前进行空检查 (?.Invoke()),并在不再需要时及时取消订阅 (-=) 以避免内存泄漏。

掌握委托与事件,将为我们后续学习 Unity 中的事件系统(如 UnityEvent)以及构建更复杂的游戏逻辑打下坚实的基础。明天,我们将专门探讨 Unity 内置的事件系统 UnityEvent,看看它与 C# 原生事件有何异同,以及如何在 Unity 项目中更便捷地实现脚本间的通信。


猜你喜欢

转载自blog.csdn.net/Kiradzy/article/details/147052939
今日推荐