泛型(Generics)是 .NET 核心特性之一,它允许开发者在定义类、方法、接口或委托时使用类型占位符,从而在不牺牲类型安全的前提下提高代码的重用性和性能。以下将从基础概念到高级应用,系统性地介绍 .NET Core 中的泛型机制。
1. 泛型方法 (Generic Methods)
泛型方法允许在方法签名中定义类型参数,使得同一个方法可以处理多种数据类型,而无需为每种类型编写重载版本。
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)
泛型类将类型参数提升到类级别,类中的字段、属性、方法返回值及参数均可使用该类型参数。
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)
泛型接口定义了契约,允许实现类针对特定类型提供具体实现,常用于集合框架和服务抽象。
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>。
// 自定义泛型委托 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: 5Action<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 |
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 中,泛型常与依赖注入和缓存结合使用,以提供类型安全的缓存访问。
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):允许将基类型替换为更派生的类型。适用于输入位置(参数)。
// 协变接口: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) { } }协变和逆变仅适用于引用类型,不适用于值类型。且类型参数只能出现在输出或输入位置,不能同时出现在两者中(除非不使用方差修饰符)。