C# 泛型:<T> 类型参数与约束
泛型是 C# 中强大的工具,它允许你编写灵活且可重用的代码。核心在于类型参数 <T>,它像一个占位符,在使用时才会被具体的类型替换。然而,由于编译器在编译阶段并不知道 T 到底是什么,它会限制你对 T 的操作。为了解决这个问题,必须使用“约束”来告诉编译器 T 必须具备的特征。
1. 定义基础泛型类型
打开你的开发环境,创建一个新的控制台应用程序。定义一个简单的泛型类,T 代表待定的类型。
public class Box
{
public T Content { get; set; }
public Box(T content)
{
Content = content;
}
}
实例化该类:
var intBox = new Box<int>(123);
var stringBox = new Box<string>("Hello");
在这个阶段,T 可以是任何类型。编译器仅允许你调用 object 类型自带的方法(如 ToString(), Equals()),因为它对 T 一无所知。
2. 遇到的限制:为什么需要约束?
尝试在 Box 类中添加一个方法,用于比较两个 Box 的内容。
public bool IsEqual(Box other)
{
return Content.CompareTo(other.Content) == 0;
}
编译代码。此时编译器会报错,提示 'T' 不包含 CompareTo 的定义。这是因为 T 可能是 int,也可能是 Image 或自定义类,而并非所有类型都有 CompareTo 方法。
3. 添加接口约束
为了让上述代码工作,修改类定义,添加 where T : IComparable 约束。这告诉编译器:T 必须是实现了 IComparable 接口的类型。
public class Box where T : IComparable
{
public T Content { get; set; }
public Box(T content)
{
Content = content;
}
public bool IsEqual(Box other)
{
// 现在 Content 保证拥有 CompareTo 方法
return Content.CompareTo(other.Content) == 0;
}
}
再次编译代码,错误消失。现在 Box 只能接受实现了 IComparable 的类型(如 int, string)。
4. 常见的泛型约束列表
C# 提供了多种约束类型。参考下表了解常用约束及其作用。
| 约束类型 | 描述 | 示例 |
|---|---|---|
where T : struct |
T 必须是值类型(如 int, bool, struct) |
public void Process() where T : struct |
where T : class |
T 必须是引用类型(如 class, interface, string, delegate) |
public void Process() where T : class |
where T : new() |
T 必须有一个无参的公共构造函数 |
public void Create() where T : new() |
where T : <BaseClass> |
T 必须是指定的基类或其派生类 |
public void Process() where T : Person |
where T : <Interface> |
T 必须是指定的接口 |
public void Sort() where T : IComparable |
where T : U |
T 必须是另一个泛型参数 U 或其派生类 |
public void Process(List list) where T : U |
5. 组合使用约束
在 new() 约束与其他约束(如类或接口)同时存在时,new() 必须放在约束列表的最后一位。
编写以下代码,演示如何要求 T 既是引用类型,又实现了 IDisposable 接口,并且有无参构造函数:
public class ResourceManager where T : class, IDisposable, new()
{
public void Execute()
{
// 1. 因为有 new() 约束,可以创建实例
T resource = new T();
try
{
// 执行操作...
}
finally
{
// 2. 因为有 IDisposable 约束,可以调用 Dispose
resource.Dispose();
}
}
}
如果写错顺序(例如 where T : new(), class),编译器将报错。
6. 约束检查流程逻辑
当编译器遇到带有约束的泛型方法调用时,它会执行严格的检查流程。下图描述了当尝试使用 new T() 以及将 T 赋值为 null 时的编译器逻辑:
这个流程解释了为什么你不能在值类型(struct)泛型中直接赋值 null,除非该类型本身是可空类型(如 int?),或者你显式添加了 class 约束。
7. 实战步骤:创建带约束的缓存工具类
动手创建一个实用的例子:一个只能存储引用类型的简单内存缓存。
- 定义类
SimpleCache,添加class约束确保键是引用类型,new()约束确保值可以被实例化。 - 声明一个私有字典字段用于存储数据。
- 实现
GetOrAdd方法:如果缓存中没有数据,则创建一个新的。
using System;
using System.Collections.Generic;
public class SimpleCache<TKey, TValue>
where TKey : class // TKey 必须是引用类型
where TValue : new() // TValue 必须有无参构造函数
{
private Dictionary<TKey, TValue> _storage = new Dictionary<TKey, TValue>();
public TValue GetOrAdd(TKey key)
{
// 检查字典中是否存在该键
if (_storage.TryGetValue(key, out TValue value))
{
return value;
}
// 如果不存在,利用 new() 约束创建新实例
value = new TValue();
// 添加到字典
_storage[key] = value;
return value;
}
}
使用这个工具类:
var cache = new SimpleCache<string, List<int>>();
// 第一次调用,创建新的 List<int> 并存入
var list1 = cache.GetOrAdd("myKey");
list1.Add(100);
// 第二次调用,直接取出已存在的 List<int>
var list2 = cache.GetOrAdd("myKey");
Console.WriteLine(list2.Count); // 输出 1
在这个例子中,如果尝试使用 SimpleCache<int, string>(int 不是 class),编译器会直接拦截并报错,从而在编译期就保证了类型安全。

暂无评论,快来抢沙发吧!