今天要聊的是泛型(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 里不太常用,知道有这回事就行,需要时再深究。
八、为什么要用泛型?优点总结
- 类型安全:编译时就确定类型,避免运行时类型转换错误。
- 代码复用:同一套逻辑适用多种类型,减少重复代码。
- 性能 :避免装箱拆箱,对于频繁调用的方法(如
GetComponent)效率更好。 - 可读性 :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,也能提升自己架构代码的能力。建议多写几个泛型类或方法练手,尤其是尝试给对象池、单例等加上泛型,会对理解有很大帮助。