泛型与协变逆变
泛型约束与型变
概述
泛型是 C# 的核心特性之一,允许你编写类型参数化的代码,即代码中的某些类型在编写时不指定,而在使用时才确定。协变和逆变(合称型变)则定义了泛型类型参数之间的继承关系如何传递。理解泛型和型变,是写出灵活、可复用代码的关键。
为什么需要泛型?没有泛型时,你要么为每种类型写一套重复代码,要么使用 object 类型(丢失类型安全、产生装箱开销)。泛型让你写一套代码适用于多种类型,同时保持类型安全。协变和逆变则解决了泛型类型赋值兼容性的问题,比如为什么 IEnumerable<string> 可以赋值给 IEnumerable<object>。
基础概念
类型参数:泛型定义中的占位符类型,如 List<T> 中的 T。使用时用具体类型替换,如 List<int>。
泛型约束:限制类型参数必须满足的条件,如必须实现某个接口、必须有无参构造函数等。
协变(Covariance):用 out 关键字标记,表示类型参数只能作为输出(返回值)。协变允许将 IEnumerable<Derived> 赋值给 IEnumerable<Base>。
逆变(Contravariance):用 in 关键字标记,表示类型参数只能作为输入(参数)。逆变允许将 IComparer<Base> 赋值给 IComparer<Derived>。
不变(Invariance):既不是协变也不是逆变,类型参数既可作输入也可作输出。此时 List<Derived> 不能赋值给 List<Base>。
快速上手
最简单的泛型示例:
// 泛型方法
T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// 使用
int maxInt = Max(3, 5); // T 推断为 int
string maxStr = Max("abc", "xyz"); // T 推断为 string
// 泛型类
public class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
public override string ToString() => $"Box[{Value}]";
}
var intBox = new Box<int>(42);
var strBox = new Box<string>("hello");
详细用法
泛型约束
约束限制了类型参数的范围,让你可以在泛型代码中使用约束保证的功能:
// 1. class 约束:T 必须是引用类型
public class RefRepository<T> where T : class
{
private readonly List<T> _items = new();
// 可以使用 null 比较
public T? FindById(int id) => _items.FirstOrDefault();
}
// 2. struct 约束:T 必须是值类型
public class ValueHolder<T> where T : struct
{
public T Value { get; set; }
// 值类型不能为 null,但可以判断默认值
public bool HasValue => !Value.Equals(default(T));
}
// 3. new() 约束:T 必须有无参构造函数
public class Factory<T> where T : new()
{
public T Create()
{
// 可以调用 new T()
return new T();
}
}
// 4. 接口约束:T 必须实现指定接口
public class Sorter<T> where T : IComparable<T>
{
public T FindMax(IEnumerable<T> items)
{
// 可以调用 IComparable<T> 的方法
return items.Max()!;
}
}
// 5. 基类约束:T 必须继承指定基类
public class AnimalService<T> where T : Animal
{
public void MakeSound(T animal)
{
// 可以调用 Animal 的成员
Console.WriteLine(animal.Sound);
}
}
// 6. 多个约束
public class Repository<T> where T : class, IEntity, new()
{
// T 必须是引用类型、实现 IEntity 接口、有无参构造函数
public T CreateAndSave()
{
var entity = new T();
entity.Id = Guid.NewGuid();
return entity;
}
}
// 7. 多个类型参数的约束
public class Pair<TFirst, TSecond>
where TFirst : class
where TSecond : struct
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
协变
协变用 out 关键字标记,类型参数只能出现在输出位置:
// 协变接口:T 只能作为返回值
public interface IProducer<out T>
{
T GetItem();
// 以下写法会编译错误,因为 T 不能作为输入
// void SetItem(T item);
}
// 实现
public class Producer<T> : IProducer<T>
{
private T _item;
public Producer(T item) => _item = item;
public T GetItem() => _item;
}
// 协变允许的赋值
IProducer<string> stringProducer = new Producer<string>("hello");
IProducer<object> objectProducer = stringProducer; // 合法!string 是 object 的子类
// 为什么安全?因为 objectProducer 只能"取出"数据
// 取出 string 当作 object 使用是完全安全的
object obj = objectProducer.GetItem(); // 实际是 string,作为 object 使用没问题
协变在 .NET 中的经典例子是 IEnumerable<out T>:
// IEnumerable 是协变的
// IEnumerable<out T> 中 T 只出现在返回值位置
// 因此以下赋值是合法的
IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings; // 协变允许
// 遍历时把 string 当作 object 使用是安全的
foreach (object obj in objects)
{
Console.WriteLine(obj); // 输出 a, b, c
}
逆变
逆变用 in 关键字标记,类型参数只能出现在输入位置:
// 逆变接口:T 只能作为参数
public interface IConsumer<in T>
{
void Process(T item);
// 以下写法会编译错误,因为 T 不能作为返回值
// T GetResult();
}
// 实现
public class Consumer<T> : IConsumer<T>
{
public void Process(T item)
{
Console.WriteLine($"处理: {item}");
}
}
// 逆变允许的赋值
IConsumer<object> objectConsumer = new Consumer<object>();
IConsumer<string> stringConsumer = objectConsumer; // 合法!方向反转了
// 为什么安全?因为 stringConsumer 只能"传入"数据
// 传入 string 给一个能处理 object 的消费者是安全的
// (string 是 object,消费者能处理任何 object,自然能处理 string)
stringConsumer.Process("hello");
逆变在 .NET 中的经典例子是 IComparer<in T>:
// IComparer 是逆变的
// IComparer<in T> 中 T 只出现在参数位置
// 定义一个通用的 object 比较器
IComparer<object> objectComparer = Comparer<object>.Default;
// 可以将 object 比较器用于 string 比较
IComparer<string> stringComparer = objectComparer; // 逆变允许
// 为什么安全?比较器接收 string 参数
// 内部按 object 比较,string 是 object,所以没问题
int result = stringComparer.Compare("apple", "banana");
协变和逆变的对比
// 协变:子类型关系保持方向
// string : object => IEnumerable<string> : IEnumerable<object>
// "输出"位置,类型关系方向一致
// 逆变:子类型关系反转方向
// string : object => IComparer<object> : IComparer<string>
// "输入"位置,类型关系方向反转
// 不变:没有子类型关系
// string : object 但 List<string> 和 List<object> 没有关系
// 因为 List<T> 既可读又可写,T 同时出现在输入和输出位置
List<string> strings = new List<string>();
// List<object> objects = strings; // 编译错误!
// 原因:如果允许,objects.Add(42) 会把 int 放进 string 列表
泛型方法
// 泛型方法:在方法级别定义类型参数
public class Utility
{
// 泛型方法,类型参数由调用时推断
public static T[] CreateArray<T>(params T[] items)
{
return items;
}
// 泛型方法带约束
public static T DefaultIfNull<T>(T? value) where T : struct
{
return value ?? default;
}
// 多个类型参数
public static TResult Convert<TInput, TResult>(
TInput input, Func<TInput, TResult> converter)
{
return converter(input);
}
}
// 使用
var arr = Utility.CreateArray(1, 2, 3); // int[]
var names = Utility.CreateArray("a", "b", "c"); // string[]
int? nullable = null;
int value = Utility.DefaultIfNull(nullable); // 0
var length = Utility.Convert("hello", s => s.Length); // 5
泛型与反射
// 运行时获取泛型类型信息
var listType = typeof(List<>); // 开放泛型类型
var intListType = typeof(List<int>); // 封闭泛型类型
var constructed = listType.MakeGenericType(typeof(int)); // 等价于 List<int>
// 检查泛型类型
Console.WriteLine(intListType.IsGenericType); // True
Console.WriteLine(intListType.GetGenericTypeDefinition() == listType); // True
// 获取类型参数
var args = intListType.GetGenericArguments(); // [int]
Console.WriteLine(args[0].Name); // Int32
常见场景
泛型仓储模式
// 泛型仓储接口
public interface IRepository<T> where T : class, IEntity
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// 通用实现
public class Repository<T> : IRepository<T> where T : class, IEntity
{
private readonly AppDbContext _context;
public Repository(AppDbContext context)
{
_context = context;
}
public async Task<T?> GetByIdAsync(int id) =>
await _context.Set<T>().FindAsync(id);
public async Task<List<T>> GetAllAsync() =>
await _context.Set<T>().ToListAsync();
public async Task<T> AddAsync(T entity)
{
_context.Set<T>().Add(entity);
await _context.SaveChangesAsync();
return entity;
}
public async Task UpdateAsync(T entity)
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity is not null)
{
_context.Set<T>().Remove(entity);
await _context.SaveChangesAsync();
}
}
}
// 注册泛型仓储
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
事件系统
// 泛型事件系统
public interface IEventHandler<in TEvent>
{
Task HandleAsync(TEvent event);
}
public class EventBus
{
private readonly IServiceProvider _provider;
public EventBus(IServiceProvider provider)
{
_provider = provider;
}
// 泛型发布方法
public async Task PublishAsync<TEvent>(TEvent @event)
{
// 获取所有处理该事件类型的处理器
var handlers = _provider.GetServices<IEventHandler<TEvent>>();
foreach (var handler in handlers)
{
await handler.HandleAsync(@event);
}
}
}
注意事项
类型擦除与具体化:C# 的泛型是具体化的(reified),运行时保留完整的类型信息。这与 Java 的类型擦除不同,C# 泛型不需要装箱,值类型的泛型有独立的实现。
协变和逆变的限制:只有接口和委托可以声明型变参数,类不行。因为类可以同时包含输入和输出位置的类型参数。
默认值问题:泛型类型参数不能直接赋值为 null(除非有 class 约束)。使用 default(T) 获取默认值。
静态成员属于封闭类型:List<int>.Count 和 List<string>.Count 是不同的静态成员。每个封闭泛型类型有独立的静态数据。
进阶用法
使用泛型数学(.NET 7+)
// .NET 7 引入了泛型数学接口,可以对数字类型写泛型算法
public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>
{
T result = T.Zero;
foreach (var value in values)
{
result += value;
}
return result;
}
// 使用
int intSum = Sum(new[] { 1, 2, 3 }); // 6
double doubleSum = Sum(new[] { 1.1, 2.2, 3.3 }); // 6.6
decimal decSum = Sum(new[] { 1.5m, 2.5m }); // 4.0m
高阶类型模拟
// 使用泛型模拟函子(Functor)模式
public interface IFunctor<F>
{
// 无法直接在 C# 中实现高阶类型,但可以通过约定模拟
}
public static class FunctorExtensions
{
// 为 IEnumerable 实现映射
public static IEnumerable<TResult> Map<T, TResult>(
this IEnumerable<T> source, Func<T, TResult> selector)
{
return source.Select(selector);
}
// 为 Nullable 实现映射
public static TResult? Map<T, TResult>(
this T? source, Func<T, TResult> selector) where T : struct
{
return source.HasValue ? selector(source.Value) : null;
}
}