Unity C# 脚本学习笔记:泛型

今天要聊的是泛型(Generics),它在 Unity 脚本里其实非常常见,比如 GetComponent<T>()、List<T> 这些尖括号里的东西就是泛型。理解泛型能让你写出更通用、更安全的代码,也能帮你更好地看懂 Unity API。

一、没有泛型会怎样?

假设你想写一个方法,用来交换两个变量的值。如果不用泛型,针对不同类型要写多个版本:

csharp 复制代码
void SwapInt(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

void SwapFloat(ref float a, ref float b)
{
    float temp = a;
    a = b;
    b = temp;
}

void SwapString(ref string a, ref string b)
{
    string temp = a;
    a = b;
    b = temp;
}

类型一多代码就非常冗余。而且 int、float 这些只是类型不同,逻辑完全一样,有没有办法写一个通用的方法,让编译器自动适配不同类型?泛型就是干这个的。

二、泛型的基本概念

泛型允许你在定义类、方法、接口或委托时,先把类型当做一个"占位符",等到真正使用时再指定具体类型。占位符通常用 T 表示(Type 的首字母),当然也可以用其他名字,比如 TKey、TValue 等。

三、泛型方法

先看上面交换两个变量的例子,用泛型写:

csharp 复制代码
void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

调用时,可以明确指定类型,也可以让编译器自动推断:

csharp 复制代码
int x = 1, y = 2;
Swap<int>(ref x, ref y);   // 明确指定 int

float m = 1.5f, n = 2.5f;
Swap(ref m, ref n);         // 自动推断为 float

这样一份代码就搞定了所有类型,而且不会像 object 那样装箱拆箱,类型安全。

四、泛型类

除了方法,类也可以是泛型的。最常见的例子就是 List<T>,它就是一个泛型类。我们也可以自己写一个简单的泛型类,比如一个通用的存储容器:

csharp 复制代码
public class MyContainer<T>
{
    private T item;

    public void SetItem(T value)
    {
        item = value;
    }

    public T GetItem()
    {
        return item;
    }
}

使用时指定类型:

csharp 复制代码
MyContainer<int> intContainer = new MyContainer<int>();
intContainer.SetItem(100);
int value = intContainer.GetItem();

MyContainer<string> strContainer = new MyContainer<string>();
strContainer.SetItem("Hello");
string s = strContainer.GetItem();

同一个类定义,可以适用于不同类型,而且类型安全。

Unity 中的泛型类

Unity 里大量使用了泛型。比如 GetComponent<T>() 就是一个泛型方法,T 代表组件类型:

csharp 复制代码
Rigidbody rb = GetComponent<Rigidbody>();

再比如 List<T>、Dictionary<TKey, TValue> 这些集合,都是泛型类。

五、泛型接口与泛型委托

泛型接口

接口也可以定义泛型。比如 Unity 中常见的 IComparable<T>、IEnumerable<T> 等。

csharp 复制代码
public interface IRepository<T>
{
    void Add(T item);
    T Get(int id);
}

实现时指定具体类型:

csharp 复制代码
public class PlayerRepository : IRepository<Player>
{
    public void Add(Player item) { /* ... */ }
    public Player Get(int id) { /* ... */ return null; }
}

泛型委托

委托也可以定义泛型,最常见的就是 Action<T> 和 Func<T, TResult>。

csharp 复制代码
Action<string> print = s => Debug.Log(s);
print("Hello");

Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 4);

这些在事件系统、回调里经常用到。

六、泛型约束

泛型虽然通用,但有时候你希望 T 必须是某种类型的子类,或者必须有无参构造函数,或者必须是值类型/引用类型。这时候就需要泛型约束。

约束用 where 关键字:

csharp 复制代码
// T 必须是 class(引用类型)
public class MyClass<T> where T : class { }

// T 必须是 MonoBehaviour 或其子类(在 Unity 中很常用)
public class Singleton<T> where T : MonoBehaviour { }

// T 必须有无参构造函数
public class Factory<T> where T : new() { }

// T 必须是某个接口的实现
public class Manager<T> where T : IManager { }

约束可以组合:

csharp 复制代码
public class ComponentPool<T> where T : Component, new()

在 Unity 里,我们常写泛型单例:

csharp 复制代码
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();
            }
            return instance;
        }
    }
}

有了约束,编译器就知道 T 是 MonoBehaviour,可以调用 FindObjectOfType<T>() 等 Unity API。

七、协变与逆变(简单了解)

这部分略微高级,但了解即可。协变(out)和逆变(in)主要用在泛型接口和委托中,让泛型类型参数具有继承兼容性。

  • 协变:IEnumerable<out T>,可以把 IEnumerable<string> 赋值给 IEnumerable<object>,因为 string 继承自 object。
  • 逆变:Action<in T>,可以把 Action<object> 赋值给 Action<string>。

在 Unity 里不太常用,知道有这回事就行,需要时再深究。

八、为什么要用泛型?优点总结

  1. 类型安全:编译时就确定类型,避免运行时类型转换错误。
  2. 代码复用:同一套逻辑适用多种类型,减少重复代码。
  3. 性能 :避免装箱拆箱,对于频繁调用的方法(如 GetComponent)效率更好。
  4. 可读性 :API 清晰,比如 List<Enemy> 比 ArrayList 明确多了。

九、实战:在 Unity 里写一个泛型对象池

前面提到对象池可以用泛型实现,这里给个简单版本,帮助理解泛型在实际项目中的应用:

csharp 复制代码
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool<T> where T : Component
{
    private T prefab;
    private Queue<T> pool = new Queue<T>();

    public ObjectPool(T prefab, int initialSize)
    {
        this.prefab = prefab;
        for (int i = 0; i < initialSize; i++)
        {
            T obj = GameObject.Instantiate(prefab);
            obj.gameObject.SetActive(false);
            pool.Enqueue(obj);
        }
    }

    public T Get()
    {
        if (pool.Count > 0)
        {
            T obj = pool.Dequeue();
            obj.gameObject.SetActive(true);
            return obj;
        }
        else
        {
            return GameObject.Instantiate(prefab);
        }
    }

    public void Return(T obj)
    {
        obj.gameObject.SetActive(false);
        pool.Enqueue(obj);
    }
}

使用:

csharp 复制代码
public class Bullet : MonoBehaviour { }

public class Gun : MonoBehaviour
{
    public Bullet bulletPrefab;
    private ObjectPool<Bullet> bulletPool;

    void Start()
    {
        bulletPool = new ObjectPool<Bullet>(bulletPrefab, 10);
    }

    void Fire()
    {
        Bullet bullet = bulletPool.Get();
        // 设置位置等
    }
}

这个例子展示了泛型约束(where T : Component)以及泛型类的实际应用。

十、总结

泛型是 C# 里非常强大的特性,它让代码更通用、更安全、更高效。在 Unity 中,泛型无处不在,掌握它不仅能让你更好地使用引擎 API,也能提升自己架构代码的能力。建议多写几个泛型类或方法练手,尤其是尝试给对象池、单例等加上泛型,会对理解有很大帮助。

相关推荐
一隅论数智3 天前
给AI一张“业务概念地图“:本体如何从哲学走向企业智能
大数据·人工智能·经验分享·笔记·学习·学习方法·政务
小羊没烦恼!3 天前
初探性能优化——2个月到4小时的性能提升
java·开发语言·windows·算法·c#
XiHongShi20163 天前
STM32F407 RTC定时器例程,建议保存
stm32·单片机·学习
神仙别闹3 天前
基于C#+MySQL实现(WinForm)个人聊天室软件
c#
爱吃苹果的日记本3 天前
离散数学第六课
学习·离散数学
AI职业加油站3 天前
AI智能体应用工程师证书:政策红利下的职业新风口
大数据·运维·人工智能·学习·职场发展
旖旎夜光3 天前
力控面试题 01.01: 判定字符是否唯一(位运算) —— 题解
c++·学习·算法·leetcode·力控
奇思妙想聪明勤奋的小羊3 天前
DeepAgents第5章:子Agent 与上下文隔离—让 Agent学会委派
人工智能·python·学习·语言模型
霍霍的袁3 天前
【C++】map 和 set 的使用 | 从用法到底层
开发语言·c++·学习·visual studio