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

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

二、泛型的基本概念

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

三、泛型方法

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

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

有了约束,编译器就知道 TMonoBehaviour,可以调用 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,也能提升自己架构代码的能力。建议多写几个泛型类或方法练手,尤其是尝试给对象池、单例等加上泛型,会对理解有很大帮助。

相关推荐
tju新生代魔迷2 小时前
Verilog HDL 学习笔记(十三)| 第13章 编程语言接口(PLI)
笔记·学习
我爱cope5 小时前
【计算机网络 | 网络层11:ICMP 协议:Ping 和 Traceroute 的底层原理】
网络·学习·计算机网络
公子小六5 小时前
基于.NET的Windows窗体编程之WinForms滑块与上下控件
windows·microsoft·c#·.net·winforms
问天_观心5 小时前
大模型微调学习(一)
开发语言·人工智能·学习·语言模型·github
苏生Susheng6 小时前
【软件实施】Linux系统Shell脚本教程
linux·运维·服务器·chrome·spring boot·学习·实施
曹牧6 小时前
C#:文本文件读取
服务器·前端·c#
2601_967338716 小时前
C#上位机开发零基础入门到精通全套视频
开发语言·c#
我命由我123456 小时前
Photoshop - Photoshop 把两个 PSD 文件合并
学习·ui·职场和发展·产品运营·产品经理·学习方法·photoshop
小雪崩8 小时前
嵌入式学习 day39:TCP并发服务器模型
linux·服务器·c语言·网络·学习·tcp/ip