泛型(Generics)是 .NET 核心特性之一,它允许开发者在定义类、方法、接口或委托时使用类型占位符,从而在不牺牲类型安全的前提下提高代码的重用性和性能。以下将从基础概念到高级应用,系统性地介绍 .NET Core 中的泛型机制。
1. 泛型方法 (Generic Methods)
泛型方法允许在方法签名中定义类型参数,使得同一个方法可以处理多种数据类型,而无需为每种类型编写重载版本。
cs
public class GenericHelper
{
// T 是类型参数,由调用者决定
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static T GetDefault<T>()
{
return default(T);
}
}
// 使用示例
int x = 10, y = 20;
GenericHelper.Swap<int>(ref x, ref y); // 显式指定类型
GenericHelper.Swap(ref x, ref y); // 编译器自动推断类型
- 类型推断 :C# 编译器通常可以根据传入参数的类型自动推断出
T的具体类型,因此调用时往往可以省略<int>。 - 默认值 :
default(T)对于引用类型返回null,对于值类型返回其零值(如0,false)。
2. 泛型类 (Generic Classes)
泛型类将类型参数提升到类级别,类中的字段、属性、方法返回值及参数均可使用该类型参数。
cs
public class SimpleGenericClass<T>
{
public T Field;
public SimpleGenericClass(T value)
{
Field = value;
}
public void Display()
{
Console.WriteLine($"Value: {Field}, Type: {typeof(T).Name}");
}
}
// 使用示例
var stringBox = new SimpleGenericClass<string>("Hello");
stringBox.Display(); // Output: Value: Hello, Type: String
var intBox = new SimpleGenericClass<int>(42);
intBox.Display(); // Output: Value: 42, Type: Int32
- 构造泛型类 :当实例化
SimpleGenericClass<string>时,CLR 会创建一个特定的封闭构造类型,其中所有的T都被替换为string。 - 类型安全:一旦指定了类型,后续操作必须遵循该类型约束,避免了装箱/拆箱操作带来的性能损耗和运行时类型错误。
3. 泛型接口 (Generic Interfaces)
泛型接口定义了契约,允许实现类针对特定类型提供具体实现,常用于集合框架和服务抽象。
cs
public interface IGenericInterface<T>
{
T GetItem();
void SetItem(T item);
}
public class StringRepository : IGenericInterface<string>
{
private string _data;
public string GetItem() => _data;
public void SetItem(string item) => _data = item;
}
public class GenericRepository<T> : IGenericInterface<T>
{
private T _data;
public T GetItem() => _data;
public void SetItem(T item) => _data = item;
}
- 接口可以是泛型的,实现类可以选择继续保留泛型(如
GenericRepository<T>),也可以指定具体类型(如StringRepository)。 - .NET 标准库中的
IEnumerable<T>,ICollection<T>,IList<T>都是典型的泛型接口。
4. 泛型委托 (Generic Delegates)
泛型委托允许定义具有类型参数的方法签名,广泛用于事件处理和回调机制。.NET 内置了 Action<T> 和 Func<T, TResult>。
cs
// 自定义泛型委托
public delegate T ResultDelegate<T>(T input);
// 使用内置泛型委托
public class DelegateExample
{
public static void Process<T>(T data, Action<T> action)
{
action(data);
}
public static TResult Transform<T, TResult>(T data, Func<T, TResult> func)
{
return func(data);
}
}
// 使用示例
DelegateExample.Process("Hello", s => Console.WriteLine(s.ToUpper()));
int length = DelegateExample.Transform("World", s => s.Length);
Console.WriteLine(length); // Output: 5
Action<T>:无返回值,接受一个或多个参数。Func<T, TResult>:有返回值,最后一个类型参数为返回类型。- 泛型委托极大地简化了 LINQ 查询和异步编程中的回调定义。
5. 泛型约束 (Generic Constraints)
默认情况下,泛型类型参数 T 可以是任何类型。为了在泛型内部调用特定成员或保证类型特性,需要使用 where 关键字添加约束。
常用约束类型:
| 约束 | 说明 |
|---|---|
where T : struct |
T 必须是值类型(包括 enum) |
where T : class |
T 必须是引用类型 |
where T : new() |
T 必须有一个公共的无参构造函数 |
where T : BaseClass |
T 必须继承自 BaseClass |
where T : Interface |
T 必须实现 Interface |
cs
public class ConstraintExample
{
// T 必须是引用类型,实现 ICollection<string>,且有无参构造函数
public static TOutput Factory<TOutput>()
where TOutput : class, ICollection<string>, new()
{
TOutput collection = new TOutput();
collection.Add("Item1");
return collection;
}
// T 必须是值类型
public static void PrintValueType<T>(T value) where T : struct
{
Console.WriteLine($"Value: {value}, IsValueType: true");
}
}
- 约束增加了泛型内部可执行的操作数量。例如,只有添加了
new()约束,才能使用new T()。 - 多个约束可以同时存在,
new()约束必须放在最后。
6. 泛型原理 (Generic Principles)
泛型不仅仅是语法糖,它在 CLR(公共语言运行时)层面有专门的支持。
编译时处理:
- C# 编译器将泛型代码编译为 IL(中间语言),此时类型参数仍为占位符。
- JIT(即时编译器)在运行时根据实际使用的类型参数生成具体的本地代码。
代码共享与特异性:
- 引用类型:对于所有引用类型(如 List<string>, List<object>),CLR 通常共享同一份本机代码,因为引用在内存中的表示形式相同(指针)。
- 值类型:对于每个不同的值类型(如 List<int>, List<double>),CLR 会生成独立的本机代码副本,因为值类型的大小和布局不同。
优势:
- 类型安全:编译时检查类型匹配,避免运行时 InvalidCastException。
- 性能提升:避免了值类型的装箱(Boxing)和拆箱(Unboxing)操作。
- 代码复用:一套逻辑适用于多种类型。
7. 扩展泛型缓存 (Extended Generic Cache)
在 ASP.NET Core 中,泛型常与依赖注入和缓存结合使用,以提供类型安全的缓存访问。
cs
using Microsoft.Extensions.Caching.Memory;
public static class CacheExtensions
{
// 泛型方法:获取缓存,如果不存在则通过工厂函数创建并缓存
public static T GetOrCreate<T>(this IMemoryCache cache, string key, Func<ICacheEntry, T> factory)
{
if (!cache.TryGetValue(key, out T result))
{
var entry = cache.CreateEntry(key);
result = factory(entry);
entry.SetValue(result);
// 可选:设置过期策略
entry.SetSlidingExpiration(TimeSpan.FromMinutes(5));
entry.Dispose(); // 必须释放以提交缓存项
}
return result;
}
}
// 在 Controller 或 Service 中使用
public class DataService
{
private readonly IMemoryCache _cache;
public DataService(IMemoryCache memoryCache)
{
_cache = memoryCache;
}
public List<string> GetUsers()
{
// 类型安全地获取缓存,无需手动转换类型
return _cache.GetOrCreate("users_key", entry =>
{
// 模拟耗时操作
return new List<string> { "User1", "User2" };
});
}
}
- 通过泛型扩展方法,调用者无需关心缓存内部存储的是
object,直接获得强类型的结果。 IMemoryCache是 ASP.NET Core 推荐的内存缓存接口,支持依赖注入。
8. 协变与逆变 (Covariance and Contravariance)
协变和逆变描述了泛型类型参数在继承关系中的转换规则,主要应用于接口和委托。
- **协变 (Covariance,
out)**:允许将更派生的类型替换为基类型。适用于输出位置(返回值)。 - **逆变 (Contravariance,
in)**:允许将基类型替换为更派生的类型。适用于输入位置(参数)。
cs
// 协变接口:T 只用于输出
public interface ICovariant<out T>
{
T GetItem();
}
// 逆变接口:T 只用于输入
public interface IContravariant<in T>
{
void SetItem(T item);
}
// 测试类
public class Base { }
public class Derived : Base { }
public class VarianceTest
{
public static void Run()
{
// 协变:可以将 ICovariant<Derived> 赋值给 ICovariant<Base>
ICovariant<Derived> derivedSource = new CovariantImpl<Derived>();
ICovariant<Base> baseTarget = derivedSource; // 合法
// 逆变:可以将 IContravariant<Base> 赋值给 IContravariant<Derived>
IContravariant<Base> baseSource = new ContravariantImpl<Base>();
IContravariant<Derived> derivedTarget = baseSource; // 合法
}
}
public class CovariantImpl<T> : ICovariant<T>
{
public T GetItem() => default(T);
}
public class ContravariantImpl<T> : IContravariant<T>
{
public void SetItem(T item) { }
}
协变和逆变仅适用于引用类型,不适用于值类型。且类型参数只能出现在输出或输入位置,不能同时出现在两者中(除非不使用方差修饰符)。