C# 协变(Covariance)和逆变(Contravariance)的完整解析

一、协变与逆变的核心概念

1. 定义与公式

  • 协变(Covariance) :允许将派生程度更高的类型(子类)隐式转换为派生程度较低的类型(父类)。
    公式IFoo<父类> = IFoo<子类>

    示例:IEnumerable<Person> persons = new List<Student>();
    类比 :子类对象可安全视为父类对象(如 StudentPerson),符合直觉的"向上转型"。

  • 逆变(Contravariance) :允许将派生程度较低的类型(父类)隐式转换为派生程度更高的类型(子类)。
    公式IBar<子类> = IBar<父类>

    示例:Action<Student> studentAction = (Action<Person>)p => { };
    类比 :父类处理逻辑可安全用于子类对象(如 Person 处理器处理 Student),符合"更宽泛的输入"。

2. 设计目的

  • 解决泛型类型转换限制
    传统泛型(如 List<T>)不支持子类与父类集合的隐式转换(如 List<Person> persons = new List<Student>() 编译失败)。
    协变/逆变通过类型参数修饰符(out/in)扩展兼容性,增强代码灵活性。

二、实现机制与关键字作用

1. 类型参数修饰符

  • out 关键字(协变)

    • 约束泛型参数仅作为输出位置(返回值)。
    • 确保类型安全:子类返回值可安全视为父类。
      示例接口
    csharp 复制代码
    public interface IEnumerable<out T> {
        IEnumerator<T> GetEnumerator(); // T 仅作为返回值
    }
  • in 关键字(逆变)

    • 约束泛型参数仅作为输入位置(方法参数)。
    • 确保类型安全:父类参数可安全处理子类输入。
      示例接口
    csharp 复制代码
    public interface IComparer<in T> {
        int Compare(T x, T y); // T 仅作为参数
    }

2. 底层安全原理

  • 协变安全性
    IEnumerable<out T> 仅支持读取(如 GetEnumerator),无法添加元素,避免向 List<Student> 插入 Person 导致类型错误。

  • 逆变安全性
    IComparer<in T> 仅消费输入(如 Compare),不返回 T,避免将 Person 强制转为 Student


三、支持协变/逆变的常见类型

1. 协变接口(out T

接口 说明 示例应用场景
IEnumerable<T> 只读集合遍历 多态集合处理
IQueryable<T> LINQ 查询数据源 跨层级数据查询
IReadOnlyList<T> 不可变列表 安全传递子类集合
Func<out TResult> 无参返回值委托 工厂方法委托

2. 逆变接口/委托(in T

类型 说明 示例应用场景
IComparer<T> 对象比较器 父类比较器用于子类排序
Action<in T> 单参数无返回值委托 通用事件处理器
Predicate<in T> 条件判断委托 过滤不同子类对象
IEquatable<T> 对象相等性比较 跨层级对象比对

四、典型应用场景与代码示例

1. 协变:集合多态处理

kotlin 复制代码
class Person {}
class Student : Person {}

// 协变允许子类集合视为父类集合
IEnumerable<Person> people = new List<Student>();
foreach (Person p in people) { /* 安全操作 */ }

意义:统一处理多种子类集合,无需强制转换。

2. 逆变:泛型委托兼容

ini 复制代码
// 逆变允许父类委托接受子类输入
Action<Person> logPerson = p => Console.WriteLine(p.Name);
Action<Student> logStudent = logPerson; // 逆变转换
logStudent(new Student()); // 安全执行

意义:复用父类逻辑处理子类,减少重复代码。

3. 混合变体:Func 委托

ini 复制代码
// Func<in T, out TResult> 支持参数逆变 + 返回值协变
Func<Student, string> getStudentName = s => s.Name;
Func<Person, object> getPersonInfo = getStudentName; // 安全转换

object info = getPersonInfo(new Student()); // 返回 string → object

意义:最大化委托灵活性,适配不同输入/输出类型。


五、重要限制与注意事项

  1. 仅支持引用类型

    值类型(如 int)不支持协变/逆变,因值类型转换涉及装箱拆箱。

  2. 修饰符约束

    • out T 禁止作为方法参数(避免写入)。
    • in T 禁止作为返回值(避免读取)。
      违反将导致编译错误。
  3. 不适用于普通类

    仅泛型接口、委托、数组支持变体(如 List<T> 无修饰符,故不支持协变)。

  4. 数组协变的风险

    ini 复制代码
    object[] arr = new string[10];
    arr[0] = 123; // 编译通过,运行时抛出 ArrayTypeMismatchException

    需谨慎操作避免类型错误。


六、总结

  • 协变 :通过 out T 实现子类→父类转换,用于输出场景(如集合遍历、工厂委托)。
  • 逆变 :通过 in T 实现父类→子类转换,用于输入场景(如比较器、处理器委托)。
  • 核心价值:在保证类型安全的前提下,增强泛型类型的兼容性,提升代码复用性与扩展性。

通过合理应用变体特性,可显著简化多态设计(如统一处理异构集合、构建灵活委托链),是高级泛型编程的核心工具之一。

相关推荐
极客悟道3 分钟前
巧解 Docker 镜像拉取难题:无需梯子和服务器,拉取数量无限制
后端·github
aiopencode23 分钟前
iOS 出海 App 安全加固指南:无源码环境下的 IPA 加固与防破解方法
后端
liangdabiao27 分钟前
AI一人公司?先搞定聚合支付!一天搞定全能的聚合支付系统
后端
AillemaC32 分钟前
三分钟看懂回调函数
后端
yeyong34 分钟前
越学越糟心,今天遇到又一种新的服务控制方式 snap,用它来跑snmpd
后端
喷火龙8号36 分钟前
深入理解MSC架构:现代前后端分离项目的最佳实践
后端·架构
Java技术小馆1 小时前
GitDiagram如何让你的GitHub项目可视化
java·后端·面试
星星电灯猴1 小时前
iOS 性能调试全流程:从 Demo 到产品化的小团队实战经验
后端
程序无bug2 小时前
手写Spring框架
java·后端
JohnYan2 小时前
模板+数据的文档生成技术方案设计和实现
javascript·后端·架构