C#-协变与逆变.md

目录
1. 核心概念
1.1 什么是变体 (Variance)
变体描述的是:当存在类型继承关系时,由这些类型构造出的泛型类型之间是否也能保持某种兼容关系。
假设 Dog : Animal,那么:
| 变体类型 | 关键字 | 方向 | 含义 | 示例 |
|---|---|---|---|---|
| 协变 (Covariance) | out |
子 → 父(保持方向) | I<Dog> 可赋值给 I<Animal> |
IEnumerable<out T> |
| 逆变 (Contravariance) | in |
父 → 子(逆转方向) | I<Animal> 可赋值给 I<Dog> |
Action<in T> |
| 不变 (Invariance) | 无 | 不允许转换 | List<Dog> 与 List<Animal> 互不兼容 |
List<T> |
1.2 一句话记忆法
out= 输出 = 协变 = 生产者(只读)in= 输入 = 逆变 = 消费者(只写)
1.3 直觉理解
继承关系: Dog ──────► Animal (Dog 是 Animal 的子类)
协变: I<Dog> ────► I<Animal> 方向相同 ✔ (out)
逆变: I<Animal> ───► I<Dog> 方向相反 ✔ (in)
不变: 无法转换 (默认)2. 类型安全基础
2.1 为什么需要变体限制
协变和逆变的限制本质上是为了保证类型安全。考虑以下假设场景:
// ⚠️ 假设 List<T> 支持协变(实际不支持)
List<Dog> dogs = new List<Dog>();
List<Animal> animals = dogs; // 假设允许
animals.Add(new Cat()); // 💥 运行时灾难!dogs 列表里被放入了 Cat正因为 List<T> 既有读(输出)又有写(输入)操作,所以它必须是不变的。
2.2 安全性规则
| 类型参数位置 | 允许的变体 | 原因 |
|---|---|---|
| 仅出现在返回值(输出位置) | 协变 out |
取出的永远是"至少是 T"的类型,向上转型安全 |
| 仅出现在参数(输入位置) | 逆变 in |
传入的永远是"至多是 T"的类型,接收更宽的类型安全 |
| 既出现在输入又出现在输出 | 不变 | 无法同时保证两个方向的安全性 |
3. 泛型接口中的协变与逆变
3.1 协变接口 (out T)
// 定义:T 只能用于输出位置(返回值、只读属性)
public interface IProducer<out T>
{
T Produce();
T Current { get; } // ✔ 只读属性 OK
// void Consume(T item); ✘ 编译错误!T 不能出现在输入位置
// T Value { get; set; } ✘ 编译错误!set 让 T 出现在输入位置
}使用示例:
public class AnimalShelter : IProducer<Animal>
{
public Animal Produce() => new Animal();
public Animal Current => new Animal();
}
public class DogBreeder : IProducer<Dog>
{
public Dog Produce() => new Dog();
public Dog Current => new Dog();
}
// 协变:IProducer<Dog> → IProducer<Animal> ✔
IProducer<Dog> dogBreeder = new DogBreeder();
IProducer<Animal> animalProducer = dogBreeder; // ✔ 合法!
Animal animal = animalProducer.Produce(); // 返回的 Dog 当然也是 Animal3.2 逆变接口 (in T)
// 定义:T 只能用于输入位置(方法参数)
public interface IConsumer<in T>
{
void Consume(T item);
// T Produce(); ✘ 编译错误!T 不能出现在输出位置
// T Current { get; } ✘ 编译错误!
}使用示例:
public class AnimalHandler : IConsumer<Animal>
{
public void Consume(Animal animal)
{
Console.WriteLine($"Handling {animal.GetType().Name}");
}
}
// 逆变:IConsumer<Animal> → IConsumer<Dog> ✔
IConsumer<Animal> animalHandler = new AnimalHandler();
IConsumer<Dog> dogHandler = animalHandler; // ✔ 合法!
dogHandler.Consume(new Dog()); // AnimalHandler 能处理任何 Animal,当然能处理 Dog3.3 同时使用协变与逆变
// 一个接口中可以有多个类型参数,各自独立指定变体
public interface IConverter<in TInput, out TOutput>
{
TOutput Convert(TInput input);
}
public class AnimalToStringConverter : IConverter<Animal, string>
{
public string Convert(Animal input) => input.ToString();
}
// in Animal → in Dog (逆变), out string → out object (协变)
IConverter<Animal, string> converter = new AnimalToStringConverter();
IConverter<Dog, object> dogConverter = converter; // ✔ 同时协变+逆变3.4 接口继承中的变体
// 协变接口可以继承协变接口
public interface IReadOnlyRepository<out T> : IEnumerable<T>
{
T GetById(int id);
}
// 逆变接口可以继承逆变接口
public interface ISpecializedComparer<in T> : IComparer<T>
{
// 额外方法...
}
// ⚠️ 变体方向必须一致,以下会编译错误:
// public interface IBad<out T> : IConsumer<T> { } ✘ IConsumer 需要 in T4. 泛型委托中的协变与逆变
4.1 委托中的协变(返回值)
public delegate TResult MyFunc<out TResult>();
MyFunc<Dog> getDog = () => new Dog();
MyFunc<Animal> getAnimal = getDog; // ✔ 协变
Animal a = getAnimal(); // 实际返回 Dog,作为 Animal 完全安全4.2 委托中的逆变(参数)
public delegate void MyAction<in T>(T obj);
MyAction<Animal> handleAnimal = a => Console.WriteLine(a.Name);
MyAction<Dog> handleDog = handleAnimal; // ✔ 逆变
handleDog(new Dog()); // 传入 Dog,处理 Animal 的逻辑当然能处理 Dog4.3 .NET 内置委托的变体签名
// 标准库定义(简化)
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
public delegate int Comparison<in T>(T x, T y);
public delegate TOutput Converter<in TInput, out TOutput>(TInput input);实际应用:
Func<Dog> createDog = () => new Dog();
Func<Animal> createAnimal = createDog; // ✔ out TResult 协变
Action<Animal> printAnimal = a => Console.WriteLine(a);
Action<Dog> printDog = printAnimal; // ✔ in T 逆变
Func<Animal, string> describeAnimal = a => a.Name;
Func<Dog, object> describeDog = describeAnimal; // ✔ in + out 同时5. 数组的协变
5.1 C# 数组协变(历史遗留)
Dog[] dogs = new Dog[3];
Animal[] animals = dogs; // ✔ 编译通过(数组协变)
animals[0] = new Animal(); // 💥 运行时 ArrayTypeMismatchException!5.2 为什么数组协变不安全
数组协变是 C# 1.0 时代从 Java 借鉴的设计(为了在没有泛型时支持多态集合操作),它违反了类型安全:
// 只读场景是安全的
void PrintAll(Animal[] animals) // ✔ 只读安全
{
foreach (var a in animals)
Console.WriteLine(a);
}
PrintAll(new Dog[] { new Dog(), new Dog() }); // ✔
// 写入场景是危险的
void AddCat(Animal[] animals) // ✘ 运行时可能崩溃
{
animals[0] = new Cat(); // 如果实际是 Dog[],则 💥
}5.3 最佳实践:用泛型接口替代数组协变
// ✘ 不推荐:依赖数组协变
void Process(Animal[] animals) { ... }
// ✔ 推荐:使用协变接口
void Process(IEnumerable<Animal> animals) { ... } // 只读
void Process(IReadOnlyList<Animal> animals) { ... } // 只读 + 索引访问6. .NET 内置协变/逆变接口与委托
6.1 常用协变接口 (out T)
| 接口 | 说明 |
|---|---|
IEnumerable<out T> |
可枚举序列(最常用) |
IEnumerator<out T> |
枚举器 |
IReadOnlyList<out T> |
只读列表 |
IReadOnlyCollection<out T> |
只读集合 |
IReadOnlyDictionary<TKey, out TValue> |
只读字典(仅 Value 协变) |
IGrouping<out TKey, out TElement> |
LINQ 分组 |
Lazy<out T> |
延迟初始化(.NET 4.0+,实际是类但体现了协变思想) |
6.2 常用逆变接口 (in T)
| 接口 | 说明 |
|---|---|
IComparer<in T> |
比较器 |
IComparable<in T> |
可比较接口 |
IEqualityComparer<in T> |
相等比较器 |
IEquatable<in T> |
可判等(注意:.NET Core 中非变体,需确认版本) |
IParsable<in T> |
.NET 7+ 解析接口 |
6.3 常用变体委托
| 委托 | 变体 |
|---|---|
Func<out TResult> |
协变 |
Func<in T, out TResult> |
T 逆变, TResult 协变 |
Action<in T> |
逆变 |
Predicate<in T> |
逆变 |
Comparison<in T> |
逆变 |
Converter<in TInput, out TOutput> |
TInput 逆变, TOutput 协变 |
EventHandler<in TEventArgs> |
逆变(.NET 4.5+) |
7. 实战设计模式与应用场景
7.1 仓储模式中的协变
public interface IReadRepository<out T> where T : Entity
{
T GetById(int id);
IEnumerable<T> GetAll();
}
public interface IWriteRepository<in T> where T : Entity
{
void Add(T entity);
void Delete(T entity);
}
// 完整仓储:不变(同时读写)
public interface IRepository<T> : IReadRepository<T>, IWriteRepository<T>
where T : Entity
{
}
// 使用
public class DogRepository : IRepository<Dog> { /* ... */ }
IReadRepository<Dog> dogRepo = new DogRepository();
IReadRepository<Animal> animalRepo = dogRepo; // ✔ 协变:可以统一查询
IWriteRepository<Animal> animalWriter = new AnimalRepository();
IWriteRepository<Dog> dogWriter = animalWriter; // ✔ 逆变:Animal 写入器能写 Dog7.2 事件/消息处理中的逆变
public interface IEventHandler<in TEvent> where TEvent : IEvent
{
Task HandleAsync(TEvent @event);
}
public class BaseEventLogger : IEventHandler<IEvent>
{
public Task HandleAsync(IEvent @event)
{
Console.WriteLine($"Event: {@event.GetType().Name}");
return Task.CompletedTask;
}
}
// 逆变:IEventHandler<IEvent> → IEventHandler<OrderCreatedEvent>
IEventHandler<IEvent> logger = new BaseEventLogger();
IEventHandler<OrderCreatedEvent> orderHandler = logger; // ✔7.3 工厂模式中的协变
public interface IFactory<out T>
{
T Create();
}
public class DogFactory : IFactory<Dog>
{
public Dog Create() => new Dog();
}
// 统一管理不同工厂
List<IFactory<Animal>> factories = new()
{
new DogFactory(), // ✔ IFactory<Dog> → IFactory<Animal>
new CatFactory(), // ✔ IFactory<Cat> → IFactory<Animal>
};
foreach (var factory in factories)
{
Animal animal = factory.Create();
}7.4 管道/中间件模式
public interface IValidator<in T>
{
bool Validate(T item);
IEnumerable<string> GetErrors(T item);
}
public class AnimalValidator : IValidator<Animal>
{
public bool Validate(Animal item) => !string.IsNullOrEmpty(item.Name);
public IEnumerable<string> GetErrors(Animal item)
{
if (string.IsNullOrEmpty(item.Name))
yield return "Name is required";
}
}
// 逆变:能验证 Animal 的验证器当然能验证 Dog
IValidator<Dog> dogValidator = new AnimalValidator(); // ✔7.5 LINQ 与协变的协同
public interface ISpecification<in T>
{
bool IsSatisfiedBy(T entity);
}
public class IsAdultAnimalSpec : ISpecification<Animal>
{
public bool IsSatisfiedBy(Animal entity) => entity.Age >= 2;
}
// 配合 LINQ 使用
ISpecification<Dog> spec = new IsAdultAnimalSpec(); // ✔ 逆变
IEnumerable<Dog> dogs = GetDogs();
IEnumerable<Animal> animals = dogs; // ✔ IEnumerable 协变
var adultDogs = dogs.Where(d => spec.IsSatisfiedBy(d));7.6 依赖注入中的实际应用
// 在 DI 容器中注册
services.AddSingleton<IComparer<Animal>>(new AnimalByNameComparer());
// 由于 IComparer<in T> 是逆变的,理论上可以这样用
// 但注意:大多数 DI 容器不自动支持变体解析,需要手动注册
services.AddSingleton<IComparer<Dog>>(sp => sp.GetRequiredService<IComparer<Animal>>());8. 常见陷阱与最佳实践
8.1 陷阱一:值类型不支持变体
// ✘ 值类型不参与协变/逆变(因为涉及装箱,不是"真正的"引用转换)
IEnumerable<int> ints = new List<int> { 1, 2, 3 };
// IEnumerable<object> objects = ints; ✘ 编译错误!
// ✔ 解决方案:显式转换
IEnumerable<object> objects = ints.Cast<object>();
// 或
IEnumerable<object> objects = ints.Select(i => (object)i);原理:协变和逆变仅适用于引用类型之间的转换。
int→object需要装箱(boxing),不是引用保持(reference-preserving)转换。
8.2 陷阱二:类(class)不支持变体
// ✘ 只有接口和委托支持 in/out 关键字,类不行
// public class MyList<out T> { } ✘ 编译错误
// ✔ 通过接口暴露变体能力
public interface IReadOnlyBox<out T>
{
T Value { get; }
}
public class Box<T> : IReadOnlyBox<T>
{
public T Value { get; set; }
}8.3 陷阱三:泛型约束与变体的交互
// out T 可以用于 class/接口约束(协变引用约束)
public interface IProducer<out T> where T : class // ✔
{
T Produce();
}
// out T 不能用于具体类型约束的某些场景
// 需要注意约束是否与变体冲突
public interface ISomething<out T> where T : IDisposable // ✔ 接口约束 OK
{
T Get();
}8.4 陷阱四:变体与方法重载
public interface IProcessor<in T>
{
void Process(T item);
}
public class Processor : IProcessor<Animal>, IProcessor<Dog>
{
public void Process(Animal item) => Console.WriteLine("Animal");
public void Process(Dog item) => Console.WriteLine("Dog");
}
var p = new Processor();
IProcessor<Dog> dogProcessor = p;
dogProcessor.Process(new Dog()); // 输出 "Dog"
// 但如果通过逆变赋值:
IProcessor<Animal> animalProcessor = new GeneralProcessor();
IProcessor<Dog> asDogProcessor = animalProcessor; // 逆变
asDogProcessor.Process(new Dog()); // 调用的是 Process(Animal) 8.5 陷阱五:协变接口中的泛型方法参数
public interface IMyEnumerable<out T>
{
IEnumerator<T> GetEnumerator(); // ✔ T 在输出位置(IEnumerator<out T>)
// ⚠️ 以下情况需要小心
// bool Contains(T item); ✘ T 在输入位置,编译错误
// ✔ 可以用泛型方法绕过(但丧失了变体保证)
bool Contains<TItem>(TItem item);
}8.6 最佳实践总结
| 实践 | 说明 |
|---|---|
| 🎯 接口职责分离 | 将读写拆分为独立接口,分别应用 out / in |
🎯 优先使用 IEnumerable<T> |
作为方法参数时,比 List<T> 更灵活(协变) |
| 🎯 返回具体类型,接受抽象类型 | 利用协变返回、逆变参数最大化灵活性 |
| 🎯 设计公共 API 时考虑变体 | 新建泛型接口/委托时,评估是否可以标记 in/out |
| ⚠️ 不要依赖数组协变 | 用 IReadOnlyList<T> 替代 |
| ⚠️ 注意值类型限制 | int、struct 等不参与变体 |
| ⚠️ DI 容器不自动解析变体 | 需要手动注册变体映射 |
9. 速查表
9.1 语法速查
// 协变(只输出)
interface IXxx<out T> { T Method(); }
delegate T DXxx<out T>();
// 逆变(只输入)
interface IYyy<in T> { void Method(T arg); }
delegate void DYyy<in T>(T arg);
// 混合
interface IZzz<in TIn, out TOut> { TOut Convert(TIn input); }
delegate TOut DZzz<in TIn, out TOut>(TIn input);9.2 判断决策流程
你的泛型类型参数 T 在接口/委托中的位置?
│
├── 只在返回值 / out 参数 / 只读属性?
│ └── ✔ 使用 out T(协变)
│
├── 只在方法参数 / 写入位置?
│ └── ✔ 使用 in T(逆变)
│
├── 同时在输入和输出位置?
│ └── ✘ 不变(默认,不加关键字)
│ └── 考虑拆分为两个接口
│
└── T 是值类型?
└── ✘ 变体不适用,无需标记9.3 赋值方向速查
假设: Dog : Animal : object
协变 (out): 具体 → 抽象(和继承方向一致)
IEnumerable<Dog> → IEnumerable<Animal> ✔
IEnumerable<Animal> → IEnumerable<object> ✔
Func<Dog> → Func<Animal> ✔
逆变 (in): 抽象 → 具体(和继承方向相反)
Action<Animal> → Action<Dog> ✔
IComparer<object> → IComparer<Animal> ✔
IComparer<Animal> → IComparer<Dog> ✔
不变: 无法转换
List<Dog> → List<Animal> ✘
List<Animal> → List<Dog> ✘9.4 编译器检查清单
在接口或委托上标记 out T 时,编译器确保 T 不出现在:
- ❌ 方法参数类型
- ❌ 属性的
set访问器 - ❌ 逆变泛型类型的类型参数中(嵌套情况)
在接口或委托上标记 in T 时,编译器确保 T 不出现在:
- ❌ 方法返回类型
- ❌ 属性的
get访问器 - ❌ 协变泛型类型的类型参数中(嵌套情况)
附录:完整示例
using System;
using System.Collections.Generic;
// === 类型层次 ===
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString() => $"{GetType().Name}: {Name}";
}
public class Dog : Animal
{
public string Breed { get; set; }
}
public class Cat : Animal
{
public bool IsIndoor { get; set; }
}
// === 协变接口 ===
public interface IReadOnlyRepository<out T>
{
T GetById(int id);
IEnumerable<T> GetAll();
}
// === 逆变接口 ===
public interface IEntityValidator<in T>
{
bool IsValid(T entity);
}
// === 混合变体接口 ===
public interface IMapper<in TSource, out TDestination>
{
TDestination Map(TSource source);
}
// === 实现 ===
public class DogRepository : IReadOnlyRepository<Dog>
{
private readonly List<Dog> _dogs = new()
{
new Dog { Name = "Buddy", Age = 3, Breed = "Golden Retriever" },
new Dog { Name = "Max", Age = 5, Breed = "German Shepherd" }
};
public Dog GetById(int id) => _dogs[id];
public IEnumerable<Dog> GetAll() => _dogs;
}
public class AnimalValidator : IEntityValidator<Animal>
{
public bool IsValid(Animal entity)
=> !string.IsNullOrEmpty(entity.Name) && entity.Age > 0;
}
public class DogToAnimalMapper : IMapper<Dog, Animal>
{
public Animal Map(Dog source) => new Animal { Name = source.Name, Age = source.Age };
}
// === 使用演示 ===
public class Program
{
public static void Main()
{
// 1. 协变:IReadOnlyRepository<Dog> → IReadOnlyRepository<Animal>
IReadOnlyRepository<Dog> dogRepo = new DogRepository();
IReadOnlyRepository<Animal> animalRepo = dogRepo; // ✔ 协变
foreach (Animal animal in animalRepo.GetAll())
Console.WriteLine(animal);
// 2. 逆变:IEntityValidator<Animal> → IEntityValidator<Dog>
IEntityValidator<Animal> animalValidator = new AnimalValidator();
IEntityValidator<Dog> dogValidator = animalValidator; // ✔ 逆变
Console.WriteLine(dogValidator.IsValid(new Dog { Name = "Rex", Age = 2 }));
// 3. 委托变体
Func<Dog> createDog = () => new Dog { Name = "Spot" };
Func<Animal> createAnimal = createDog; // ✔ 协变
Action<Animal> logAnimal = a => Console.WriteLine($"Logged: {a}");
Action<Dog> logDog = logAnimal; // ✔ 逆变
logDog(new Dog { Name = "Fido" });
// 4. IEnumerable 协变(最常见场景)
List<Dog> dogs = new() { new Dog { Name = "A" }, new Dog { Name = "B" } };
PrintAnimals(dogs); // ✔ List<Dog> → IEnumerable<Dog> → IEnumerable<Animal>
}
static void PrintAnimals(IEnumerable<Animal> animals)
{
foreach (var a in animals)
Console.WriteLine(a);
}
}