# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)

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)
24-Unity脚本通信终极指南:从0到1精通UnityEvent与事件解耦(Day 24)
25-精通C# Lambda与LINQ:Unity数据处理效率提升10倍的秘诀! (Day 25)
26-# Unity C#进阶:掌握泛型编程,告别重复代码,编写优雅复用的通用组件!(Day26)



前言

欢迎来到《Unity C#入门到精通:50天学习计划》的第26天!经过前几周对C#基础、面向对象、数据结构以及Unity核心机制的学习,我们已经打下了坚实的基础。今天,我们将迈向一个能显著提升代码抽象层次复用性的高级主题——泛型编程(Generic Programming)。泛型是C#中一项强大的特性,它允许我们编写与具体数据类型无关的、通用的代码模板。在Unity开发中,熟练运用泛型不仅能减少冗余代码,还能提高代码的类型安全性和性能。本篇将深入探讨泛型类、泛型方法、泛型约束的概念与使用,并结合Unity API中的泛型应用,最后通过一个实战案例——创建泛型对象池,让你真正掌握这一利器。

一、什么是泛型编程?

1.1 为何需要泛型:代码复用的痛点

想象一下,你需要编写一个方法来交换两个整数变量的值,然后再编写一个类似的方法来交换两个浮点数,接着可能还需要交换两个字符串…

// 交换两个整数
public void SwapInts(ref int a, ref int b)
{
    
    
    int temp = a;
    a = b;
    b = temp;
}

// 交换两个浮点数
public void SwapFloats(ref float a, ref float b)
{
    
    
    float temp = a;
    a = b;
    b = temp;
}

// 交换两个字符串
public void SwapStrings(ref string a, ref string b)
{
    
    
    string temp = a;
    a = b;
    b = temp;
}

// 如果还需要交换 GameObject, Vector3, 自定义类... 代码会变得非常冗余

你会发现这些方法的逻辑完全相同,唯一的区别在于处理的数据类型不同。如果每种类型都需要写一个版本,代码将变得极其冗余且难以维护。这就是泛型编程要解决的核心痛点:如何编写一份代码,使其能够适用于多种数据类型?

1.2 泛型的核心思想:类型参数化

泛型的核心思想是类型参数化(Type Parameterization)。它允许我们在定义类、接口、方法时,不指定具体的类型,而是使用一个类型占位符(Type Placeholder),通常用大写字母 T 表示(也可以是其他字母,如 TKey, TValue)。这个占位符被称为类型参数(Type Parameter)

当实际使用这些泛型定义时,我们需要为类型参数提供一个具体的类型实参(Type Argument)。编译器或运行时会根据提供的类型实参,生成针对该特定类型的代码版本。

类比: 就像一个饼干模具(泛型定义),你可以用它来制作不同口味的饼干(具体类型的实例),比如巧克力味(int)、草莓味(string)、抹茶味(GameObject)。模具本身是通用的,但制作出来的饼干口味是具体的。

1.3 泛型带来的优势

使用泛型主要有以下几个显著优势:

  1. 代码复用性(Reusability): 一份泛型代码可以处理多种数据类型,大大减少了代码量和维护成本。
  2. 类型安全(Type Safety): 泛型在编译时进行类型检查。例如,如果你创建了一个 List<int>,就不能向其中添加字符串,编译器会直接报错,避免了运行时的类型转换错误。
  3. 性能提升(Performance): 对于值类型(如 int, float, struct),泛型避免了装箱(Boxing)和拆箱(Unboxing)操作,相比于使用 object 类型进行通用处理,性能更高。
  4. 代码更清晰(Clarity): 泛型代码通常更能明确地表达其意图,因为它显式地处理了类型参数。

二、泛型类与泛型方法:定义与使用

2.1 泛型类的定义

泛型类是在类名后面加上尖括号 <T> 来定义的,其中 T 是类型参数。

using UnityEngine;

// 定义一个简单的泛型容器类
public class GenericContainer<T>
{
    
    
    private T item; // 使用类型参数 T 作为字段类型

    public void SetItem(T newItem)
    {
    
    
        this.item = newItem;
    }

    public T GetItem()
    {
    
    
        return this.item;
    }

    public void LogItemType()
    {
    
    
        // typeof(T) 可以获取类型参数在运行时的具体类型
        Debug.Log($"The type of item is: {
      
      typeof(T)}");
    }
}

在这个例子中,GenericContainer<T> 可以存储任何类型的数据。T 在类定义中充当了一个类型的占位符。

2.2 泛型类的使用

使用泛型类时,需要提供具体的类型实参。

using UnityEngine;

public class GenericClassExample : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        // 创建一个存储整数的容器
        GenericContainer<int> intContainer = new GenericContainer<int>();
        intContainer.SetItem(100);
        int number = intContainer.GetItem();
        Debug.Log($"Got integer: {
      
      number}"); // 输出: Got integer: 100
        intContainer.LogItemType();          // 输出: The type of item is: System.Int32

        // 创建一个存储字符串的容器
        GenericContainer<string> stringContainer = new GenericContainer<string>();
        stringContainer.SetItem("Hello Generics!");
        string message = stringContainer.GetItem();
        Debug.Log($"Got string: {
      
      message}"); // 输出: Got string: Hello Generics!
        stringContainer.LogItemType();       // 输出: The type of item is: System.String

        // 创建一个存储GameObject的容器
        GenericContainer<GameObject> goContainer = new GenericContainer<GameObject>();
        goContainer.SetItem(this.gameObject); // 存储当前脚本所在的GameObject
        GameObject go = goContainer.GetItem();
        Debug.Log($"Got GameObject: {
      
      go.name}"); // 输出: Got GameObject: [当前对象名]
        goContainer.LogItemType();            // 输出: The type of item is: UnityEngine.GameObject
    }
}

可以看到,同一个 GenericContainer<T> 类定义,通过提供不同的类型实参(int, string, GameObject),就能创建出处理不同数据类型的具体实例,并且保证了类型安全。

2.3 泛型方法的定义

泛型方法是在方法名后面、参数列表前面加上尖括号 <T> 来定义的。类型参数 T 可以在方法的参数、返回值以及方法体内部使用。

using UnityEngine;

public class GenericMethodExample
{
    
    
    // 定义一个泛型方法来打印任何类型的值
    public void LogValue<T>(T value)
    {
    
    
        Debug.Log($"Logging value ({
      
      typeof(T)}): {
      
      value}");
    }

    // 定义一个泛型方法来比较两个值是否相等
    public bool AreEqual<T>(T value1, T value2)
    {
    
    
        // 使用 Equals 方法进行比较,适用于大多数类型
        // 对于自定义类型,需要确保 Equals 被正确实现
        return value1.Equals(value2);
    }

    // 泛型方法也可以有返回值
    public T GetFirst<T>(T[] array)
    {
    
    
        if (array == null || array.Length == 0)
        {
    
    
            // default(T) 返回类型 T 的默认值
            // 引用类型是 null, 值类型是 0 或 false, struct 是其成员的默认值
            return default(T);
        }
        return array[0];
    }
}

2.4 泛型方法的使用

调用泛型方法时,通常编译器可以通过传入的参数**推断(Infer)**出类型实参,因此可以省略尖括号中的类型。当然,也可以显式指定。

using UnityEngine;

public class UseGenericMethod : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        GenericMethodExample example = new GenericMethodExample();

        // 调用泛型方法,编译器自动推断类型
        example.LogValue(123);       // T 推断为 int
        example.LogValue("Hello");   // T 推断为 string
        example.LogValue(transform); // T 推断为 Transform

        bool intsEqual = example.AreEqual(5, 5);       // T 推断为 int
        bool stringsEqual = example.AreEqual("A", "B"); // T 推断为 string
        Debug.Log($"Are 5 and 5 equal? {
      
      intsEqual}");     // 输出: true
        Debug.Log($"Are 'A' and 'B' equal? {
      
      stringsEqual}"); // 输出: false

        int[] numbers = {
    
     1, 2, 3 };
        string[] names = {
    
     "Alice", "Bob" };

        int firstNumber = example.GetFirst(numbers);      // T 推断为 int
        string firstName = example.GetFirst(names);       // T 推断为 string
        // 也可以显式指定类型
        // string firstNameExplicit = example.GetFirst<string>(names);

        Debug.Log($"First number: {
      
      firstNumber}"); // 输出: 1
        Debug.Log($"First name: {
      
      firstName}");   // 输出: Alice

        // 处理空数组
        float[] emptyFloats = {
    
    };
        float firstFloat = example.GetFirst(emptyFloats); // T 推断为 float
        Debug.Log($"First float (empty): {
      
      firstFloat}"); // 输出: 0 (float 的默认值)
    }
}

三、泛型约束:为类型参数添加限制

虽然泛型的目标是通用,但有时我们希望对可以用作类型实参的类型施加一些限制(Constraints)。例如,我们可能希望确保类型参数 T 必须是某个类的子类,或者必须实现某个接口,或者必须有一个无参数的构造函数。这就是泛型约束的作用。

3.1 为何需要约束

考虑一个场景:你想在一个泛型方法中调用类型参数 T 的某个特定方法或访问其特定属性。但编译器不知道 T 具体是什么类型,无法保证该方法或属性一定存在。

public void DoSomething<T>(T obj)
{
    
    
    // 编译错误!编译器不知道 T 是否有 .SomeMethod() 方法
    // obj.SomeMethod();
}

泛型约束就是用来告诉编译器,类型参数 T 必须满足某些条件,这样在泛型定义内部就可以安全地使用这些条件所保证的成员了。

3.2 常见的泛型约束类型

约束使用 where 关键字,放在泛型定义的参数列表之后。一个类型参数可以有多个约束,用逗号分隔。

3.2.1 where T : struct

类型参数 T 必须是一个值类型(如 int, float, bool, Vector3, 自定义 struct)。注意 Nullable<T> 类型除外。

public class ValueContainer<T> where T : struct
{
    
    
    // ... 只能用于值类型
}

3.2.2 where T : class

类型参数 T 必须是一个引用类型(如 string, GameObject, 自定义 class,接口,委托)。

public void ProcessReference<T>(T obj) where T : class
{
    
    
    if (obj == null) // 可以安全地与 null 比较
    {
    
    
        Debug.Log("Object is null");
    }
    // ...
}

3.2.3 where T : new()

类型参数 T 必须有一个公共的、无参数的构造函数。这允许你在泛型代码中使用 new T() 来创建该类型的实例。new() 约束必须是所有约束中的最后一个。

public T CreateInstance<T>() where T : new()
{
    
    
    return new T(); // 可以安全地调用无参构造函数
}

3.2.4 where T : <base class name>

类型参数 T 必须是指定的基类,或者是该基类的派生类

// 假设有一个基类 Enemy
public abstract class Enemy : MonoBehaviour {
    
     /* ... */ }

// 这个管理器只能处理 Enemy 或其子类 (如 Boss, Minion)
public class EnemyManager<T> where T : Enemy
{
    
    
    private List<T> enemies = new List<T>();

    public void AddEnemy(T enemy)
    {
    
    
        enemies.Add(enemy);
        enemy.gameObject.SetActive(true); // 可以安全地访问 Enemy 或 MonoBehaviour 的成员
    }
}

3.2.5 where T : <interface name>

类型参数 T 必须是指定的接口,或者实现了该接口的类型。

// 假设有一个接口 IAttackable
public interface IAttackable
{
    
    
    void TakeDamage(float amount);
}

// 这个方法可以对任何可攻击的对象造成伤害
public void DealDamage<T>(T target, float damage) where T : IAttackable
{
    
    
    target.TakeDamage(damage); // 可以安全地调用接口方法
}

3.2.6 where T : U

类型参数 T 必须是另一个类型参数 U,或者是 U 的派生类。这称为裸类型约束,通常用于建立两个类型参数之间的继承关系。

public class AdvancedContainer<T, U> where T : U
{
    
    
    // T 必须派生自 U (或就是 U)
}

3.3 Unity中的关键约束:where T : Component

在 Unity 开发中,最常用的约束之一是 where T : Component(或者 where T : MonoBehaviour,因为 MonoBehaviour 继承自 Component)。这个约束表明类型参数 T 必须是 Unity 的一个组件类型。

为什么重要?

因为 Unity 的核心 API,如 GetComponent<T>, AddComponent<T>, FindObjectOfType<T> 等,都是用来操作组件的。通过添加 where T : Component 约束,我们可以在自己的泛型代码中安全地使用这些 API,并且可以访问 GameObjectTransformComponent 类提供的通用属性和方法。

using UnityEngine;

public class ComponentFinder<T> where T : Component // 约束 T 必须是 Component 或其子类
{
    
    
    public T FindComponentInScene(string objectName)
    {
    
    
        GameObject obj = GameObject.Find(objectName);
        if (obj != null)
        {
    
    
            // 因为有约束,这里可以安全地调用 GetComponent<T>
            T component = obj.GetComponent<T>();
            if (component != null)
            {
    
    
                Debug.Log($"Found component {
      
      typeof(T).Name} on {
      
      objectName}");
                return component;
            }
            else
            {
    
    
                Debug.LogWarning($"Object {
      
      objectName} found, but component {
      
      typeof(T).Name} not found.");
            }
        }
        else
        {
    
    
            Debug.LogWarning($"Object {
      
      objectName} not found in scene.");
        }
        return null; // 或者 default(T)
    }
}

// 使用示例
public class UseComponentFinder : MonoBehaviour
{
    
    
    void Start()
    {
    
    
        // 查找场景中名为 "Main Camera" 的对象上的 Camera 组件
        ComponentFinder<Camera> cameraFinder = new ComponentFinder<Camera>();
        Camera mainCam = cameraFinder.FindComponentInScene("Main Camera");

        // 查找场景中名为 "Player" 的对象上的 Rigidbody 组件
        ComponentFinder<Rigidbody> rbFinder = new ComponentFinder<Rigidbody>();
        Rigidbody playerRb = rbFinder.FindComponentInScene("Player");

        // 尝试查找不存在的组件或对象会打印警告
        ComponentFinder<Light> lightFinder = new ComponentFinder<Light>();
        Light directionalLight = lightFinder.FindComponentInScene("Directional Light"); // 假设它存在但没有 Light 组件
    }
}

四、泛型在Unity API中的应用

Unity 大量使用了泛型来提供更类型安全、更易用的 API。熟悉这些泛型方法对于高效开发至关重要。

4.1 获取组件:GetComponent<T>()

这是最常用的泛型方法之一。它用于获取附加到同一 GameObject 上的特定类型的组件。

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    
    
    private Rigidbody rb;
    private Animator anim;

    void Start()
    {
    
    
        // 使用泛型版本获取组件,无需强制类型转换,且类型安全
        rb = GetComponent<Rigidbody>();
        anim = GetComponent<Animator>();

        if (rb == null)
        {
    
    
            Debug.LogError("Rigidbody component not found on player!");
        }
         if (anim == null)
        {
    
    
            Debug.LogError("Animator component not found on player!");
        }

        // 对比旧的非泛型方法 (需要强制类型转换,且容易出错)
        // rb = (Rigidbody)GetComponent(typeof(Rigidbody));
        // rb = GetComponent("Rigidbody") as Rigidbody; // 基于字符串的版本更不推荐
    }
}

优势:

  • 编译时类型检查: 如果 RigidbodyAnimator 组件不存在,GetComponent<T>() 会返回 null,但代码本身能编译。如果尝试获取一个非 Component 类型(如 GetComponent<int>()),编译器会直接报错。
  • 无需强制转换: 返回值直接是 T 类型,代码更简洁。

类似的泛型方法还有 GetComponents<T>(), GetComponentInChildren<T>(), GetComponentInParent<T>() 等。

4.2 查找对象:FindObjectOfType<T>()

此方法用于在整个场景中查找第一个激活的、类型为 TMonoBehaviour 实例。

using UnityEngine;

public class GameManagerFinder : MonoBehaviour
{
    
    
    private GameManager gameManager;

    void Start()
    {
    
    
        // 查找场景中唯一的 GameManager 实例 (假设 GameManager 是一个 MonoBehaviour)
        gameManager = FindObjectOfType<GameManager>();

        if (gameManager == null)
        {
    
    
            Debug.LogError("GameManager not found in the scene!");
        }
        else
        {
    
    
            gameManager.StartGame(); // 调用 GameManager 的方法
        }

        // 对比旧的非泛型方法
        // gameManager = (GameManager)FindObjectOfType(typeof(GameManager));
    }
}

注意: FindObjectOfType<T>() 是一个相对昂贵的操作,因为它需要遍历场景中的所有 GameObject。应避免在 Update() 等频繁调用的方法中使用。通常在 Start()Awake() 中调用一次并缓存结果。

类似的泛型方法还有 FindObjectsOfType<T>(),用于查找场景中所有类型为 T 的实例。

4.3 其他泛型API示例(可选)

  • AddComponent<T>(): 向 GameObject 添加一个新的 T 类型组件,返回添加的组件实例。
    Rigidbody newRb = gameObject.AddComponent<Rigidbody>();
    
  • Resources.Load<T>(string path): 从 Resources 文件夹加载指定路径下的资源,并将其转换为 T 类型。
    Texture2D playerAvatar = Resources.Load<Texture2D>("Textures/PlayerAvatar");
    GameObject enemyPrefab = Resources.Load<GameObject>("Prefabs/Enemy");
    
  • ScriptableObject.CreateInstance<T>(): 创建一个 T 类型的 ScriptableObject 实例,其中 T 必须继承自 ScriptableObject
    MyGameSettings settings = ScriptableObject.CreateInstance<MyGameSettings>();
    

五、实践:创建泛型对象池

对象池(Object Pool)是 Unity 中常用的性能优化技术,尤其适用于需要频繁创建和销毁相同类型对象的场景(如子弹、特效、敌人)。我们可以利用泛型来创建一个通用的对象池,使其能够管理任何类型的 MonoBehaviour 对象。

5.1 对象池的设计思路

  1. 池化容器: 使用一个集合(如 Queue<T>List<T>)来存储非活动的对象实例。
  2. 预制体(Prefab): 需要一个对象模板(Prefab)来创建新的实例。
  3. 获取对象(Get): 当需要对象时,先检查池中是否有可用的非活动对象。如果有,则取出、激活并返回;如果没有,则根据预制体创建一个新对象,并返回。
  4. 回收对象(Return): 当对象不再需要时,不销毁它,而是将其设置为非活动状态,并放回池中以供后续使用。
  5. 初始化/重置: 从池中获取对象时,可能需要重置其状态(如位置、速度、生命值等)。回收对象时,也可能需要清理状态。

5.2 泛型对象池的实现

下面是一个简单的泛型对象池实现:

using UnityEngine;
using System.Collections.Generic;
using System; // 需要引入 System 命名空间以使用 Action

public class GenericObjectPool<T> where T : MonoBehaviour // 约束 T 必须是 MonoBehaviour
{
    
    
    private readonly T prefab;              // 对象预制体
    private readonly Queue<T> pool = new Queue<T>(); // 池容器
    private readonly Transform parentTransform; // 池化对象的父节点(可选,用于整理层级)
    private readonly Action<T> onGet;       // 获取对象时的回调(用于初始化/重置)
    private readonly Action<T> onReturn;    // 回收对象时的回调(用于清理)

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="prefab">要池化的对象预制体</param>
    /// <param name="initialSize">初始池大小</param>
    /// <param name="onGetCallback">获取对象时调用的方法 (可选)</param>
    /// <param name="onReturnCallback">回收对象时调用的方法 (可选)</param>
    /// <param name="parent">池化对象的父级Transform (可选)</param>
    public GenericObjectPool(T prefab, int initialSize, Action<T> onGetCallback = null, Action<T> onReturnCallback = null, Transform parent = null)
    {
    
    
        this.prefab = prefab;
        this.parentTransform = parent;
        this.onGet = onGetCallback;
        this.onReturn = onReturnCallback;

        // 预先填充池
        for (int i = 0; i < initialSize; i++)
        {
    
    
            T obj = CreateNewObject();
            pool.Enqueue(obj);
        }
    }

    /// <summary>
    /// 从池中获取一个对象
    /// </summary>
    /// <returns>可用的对象实例</returns>
    public T Get()
    {
    
    
        T obj;
        if (pool.Count > 0)
        {
    
    
            obj = pool.Dequeue(); // 从池中取出
        }
        else
        {
    
    
            obj = CreateNewObject(setActive: false); // 池空了,创建新的(先不激活)
        }

        // 调用获取回调(如果已设置)
        onGet?.Invoke(obj); // C# 6.0 null 条件运算符 ?.

        obj.gameObject.SetActive(true); // 激活对象
        return obj;
    }

    /// <summary>
    /// 将对象回收至池中
    /// </summary>
    /// <param name="obj">要回收的对象</param>
    public void Return(T obj)
    {
    
    
        if (obj == null) return;

        // 调用回收回调(如果已设置)
        onReturn?.Invoke(obj);

        obj.gameObject.SetActive(false); // 设置为非活动
        pool.Enqueue(obj);              // 放回池中
    }

    // 内部方法:创建新对象实例
    private T CreateNewObject(bool setActive = false)
    {
    
    
        T newObj = GameObject.Instantiate(prefab, parentTransform);
        newObj.gameObject.SetActive(setActive); // 根据需要设置初始活动状态
        return newObj;
    }

    // 获取当前池中可用对象数量
    public int GetPooledCount()
    {
    
    
        return pool.Count;
    }
}

关键点解释:

  • where T : MonoBehaviour: 约束了池只能管理 MonoBehaviour 或其子类,这样可以安全地访问 gameObjecttransform 等成员,并且可以使用 Instantiate
  • Queue<T>: 使用队列可以方便地实现先进先出(FIFO)的获取逻辑。
  • Action<T>: 使用委托 Action<T> 来定义获取和回收时的回调方法,增加了灵活性。调用者可以传入自定义的初始化和清理逻辑。
  • parentTransform: 提供一个父节点可以帮助在 Hierarchy 窗口中整理池化对象,避免混乱。
  • onGet?.Invoke(obj): 使用 null 条件运算符 ?.,只有当 onGet 委托不为 null 时才调用 Invoke

5.3 如何使用泛型对象池

假设你有一个 Bullet 脚本(继承自 MonoBehaviour)和一个对应的预制体。

using UnityEngine;

// 示例:子弹脚本
public class Bullet : MonoBehaviour
{
    
    
    public float speed = 10f;
    private Rigidbody rb;
    private GenericObjectPool<Bullet> pool; // 持有对象池引用,方便回收

    void Awake()
    {
    
    
        rb = GetComponent<Rigidbody>();
        if (rb == null) rb = gameObject.AddComponent<Rigidbody>();
        rb.useGravity = false;
    }

    // 设置所属的对象池 (通常在 Get 时由外部设置)
    public void SetPool(GenericObjectPool<Bullet> pool)
    {
    
    
        this.pool = pool;
    }

    // 子弹的初始化逻辑(在 Get 时被调用)
    public void Initialize(Vector3 startPosition, Quaternion rotation)
    {
    
    
        transform.position = startPosition;
        transform.rotation = rotation;
        rb.velocity = transform.forward * speed;
        // 可以在这里启动一个协程,N秒后自动回收
        StartCoroutine(ReturnToPoolAfterDelay(3f));
    }

    // 子弹的清理逻辑(在 Return 时被调用)
    public void ResetState()
    {
    
    
        rb.velocity = Vector3.zero;
        rb.angularVelocity = Vector3.zero;
        // 可能需要重置其他状态...
    }

    void OnCollisionEnter(Collision collision)
    {
    
    
        // 碰撞到物体后回收
        ReturnToPool();
    }

    private System.Collections.IEnumerator ReturnToPoolAfterDelay(float delay)
    {
    
    
        yield return new WaitForSeconds(delay);
        ReturnToPool();
    }

    private void ReturnToPool()
    {
    
    
        // 确保 StopAllCoroutines 在 SetActive(false) 之前调用,或者在 ResetState 中处理
        StopAllCoroutines();
        pool?.Return(this);
    }
}


// 示例:玩家射击脚本
public class PlayerShooting : MonoBehaviour
{
    
    
    public Bullet bulletPrefab; // 在 Inspector 中指定子弹预制体
    public Transform firePoint; // 子弹发射点
    public int initialPoolSize = 20;

    private GenericObjectPool<Bullet> bulletPool;

    void Start()
    {
    
    
        // 创建子弹对象池
        // 传入初始化和重置方法作为回调
        bulletPool = new GenericObjectPool<Bullet>(
            bulletPrefab,
            initialPoolSize,
            OnGetBullet,      // 获取子弹时的回调
            OnReturnBullet,   // 回收子弹时的回调
            null              // 可选:指定父 Transform
        );
    }

    void Update()
    {
    
    
        if (Input.GetButtonDown("Fire1")) // 按下鼠标左键或Ctrl
        {
    
    
            Shoot();
        }
    }

    void Shoot()
    {
    
    
        // 从池中获取子弹
        Bullet bulletInstance = bulletPool.Get();

        // 设置子弹属于哪个池(用于自我回收)
        bulletInstance.SetPool(bulletPool);

        // 注意:初始化逻辑现在由 OnGetBullet 回调处理了
        // bulletInstance.Initialize(firePoint.position, firePoint.rotation);
    }

    // --- 对象池回调方法 ---

    // 当从池中获取子弹时调用
    void OnGetBullet(Bullet bullet)
    {
    
    
        Debug.Log("Getting bullet from pool.");
        // 在这里进行初始化设置
        bullet.transform.position = firePoint.position;
        bullet.transform.rotation = firePoint.rotation;
        // bullet.GetComponent<Rigidbody>().velocity = firePoint.forward * bullet.speed; // 移到 Bullet.Initialize 里更好
        bullet.Initialize(firePoint.position, firePoint.rotation); // 调用子弹自身的初始化方法
    }

    // 当子弹返回池中时调用
    void OnReturnBullet(Bullet bullet)
    {
    
    
        Debug.Log("Returning bullet to pool.");
        // 在这里进行清理(如果 Bullet 自身的 ResetState 不够的话)
        bullet.ResetState(); // 调用子弹自身的重置方法
    }
}

通过这种方式,我们创建了一个可以管理 Bullet 对象的池。如果需要池化其他类型的对象(如 Enemy, Effect),只需创建新的 GenericObjectPool<Enemy>GenericObjectPool<Effect> 实例即可,无需重写对象池的核心逻辑。

5.4 泛型事件管理器基类的思路(备选实践)

另一个常见的泛型应用是创建一个泛型的事件管理器基类。事件系统常用于模块解耦。我们可以定义一个泛型的事件基类,或者一个泛型的事件参数类。

// 泛型事件参数
public class GameEventArgs<T>
{
    
    
    public T Data {
    
     get; private set; }
    public GameEventArgs(T data) {
    
     Data = data; }
}

// 泛型事件管理器(简化示例)
public static class EventManager<TEvent> where TEvent : struct // 约束为值类型,例如用枚举定义事件类型
{
    
    
    private static Dictionary<TEvent, Action<object>> eventDictionary = new Dictionary<TEvent, Action<object>>();

    public static void Subscribe(TEvent eventType, Action<object> listener)
    {
    
    
        if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
        {
    
    
            thisEvent += listener;
            eventDictionary[eventType] = thisEvent;
        }
        else
        {
    
    
            eventDictionary.Add(eventType, listener);
        }
    }

    public static void Unsubscribe(TEvent eventType, Action<object> listener)
    {
    
    
        if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
        {
    
    
            thisEvent -= listener;
            eventDictionary[eventType] = thisEvent;
        }
    }

    public static void Trigger(TEvent eventType, object eventArgs = null)
    {
    
    
        if (eventDictionary.TryGetValue(eventType, out Action<object> thisEvent))
        {
    
    
            thisEvent?.Invoke(eventArgs);
        }
    }
}

// 使用示例
public enum GameplayEvent {
    
     PlayerDied, ScoreChanged, EnemySpawned }

public class UIManager : MonoBehaviour
{
    
    
    void OnEnable() {
    
     EventManager<GameplayEvent>.Subscribe(GameplayEvent.ScoreChanged, UpdateScoreUI); }
    void OnDisable() {
    
     EventManager<GameplayEvent>.Unsubscribe(GameplayEvent.ScoreChanged, UpdateScoreUI); }
    void UpdateScoreUI(object data) {
    
     /* 更新UI */ }
}

public class PlayerHealth : MonoBehaviour
{
    
    
    void Die() {
    
     EventManager<GameplayEvent>.Trigger(GameplayEvent.PlayerDied); }
}

这个简单的泛型事件管理器允许使用枚举等值类型作为事件标识,提供了一定程度的类型安全和代码复用。更复杂的事件系统可能会使用更复杂的泛型结构。

六、总结

今天我们深入学习了 C# 中的泛型编程及其在 Unity 开发中的应用,这是提升代码质量和开发效率的关键一步。核心要点回顾:

  1. 泛型的核心价值:通过类型参数化实现代码的高度复用,同时保证类型安全性能
  2. 泛型类与泛型方法:掌握了使用 <T> 定义泛型类和泛型方法的语法,以及如何在实例化或调用时提供类型实参
  3. 泛型约束 (where):理解了约束的必要性,并学习了常见的约束类型(class, struct, new(), 基类, 接口),特别是 Unity 中常用的 where T : Component
  4. Unity API 中的泛型:熟悉了 GetComponent<T>, FindObjectOfType<T>, AddComponent<T>, Resources.Load<T> 等常用泛型 API,它们让代码更简洁、更安全。
  5. 实践应用:通过实现一个泛型对象池 GenericObjectPool<T>,我们看到了泛型在解决实际问题(如性能优化)中的强大威力,也了解了泛型在事件系统设计中的潜力。

掌握泛型是 C# 进阶的重要标志。它鼓励我们思考更抽象、更通用的解决方案,从而编写出更优雅、更健壮、更易于维护的 Unity 项目代码。在后续的学习和实践中,请有意识地寻找应用泛型的机会,你会发现它能为你节省大量时间和精力。明天,我们将学习另一个处理异步和时序任务的利器——协程(Coroutine)