前置知识: C#

泛型与协变逆变

00:00
4 min Advanced 2026/6/14

泛型约束与型变

概述

泛型是 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>.CountList<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;
    }
}

知识检测

学习进度

-- 已学文档
--% 知识覆盖率

学习推荐

专注模式