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 实现父类→子类转换,用于输入场景(如比较器、处理器委托)。
  • 核心价值:在保证类型安全的前提下,增强泛型类型的兼容性,提升代码复用性与扩展性。

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

相关推荐
Victor3563 分钟前
MySQL(59)如何使用查询缓存?
后端
百度智能云13 分钟前
零依赖本地调试:VectorDB Lite +VectorDB CLI 高效构建向量数据库全流程
后端
SimonKing19 分钟前
吊打面试官系列:深入理解Spring的IOC容器
java·后端·架构
flzjkl41 分钟前
【Spring】【事务】初学者直呼学会了的Spring事务入门
后端
aneasystone本尊1 小时前
使用 OpenMemory MCP 跨客户端共享记忆
后端
花千烬1 小时前
云原生之Docker, Containerd 与 CRI-O 全面对比
后端
tonydf1 小时前
还在用旧的认证授权方案?快来试试现代化的OpenIddict!
后端·安全
Wo3Shi4七1 小时前
消息积压:业务突然增长,导致消息消费不过来怎么办?
后端·kafka·消息队列
风象南2 小时前
SpringBoot实现简易直播
java·spring boot·后端
这里有鱼汤2 小时前
有人说10日低点买入法,赢率高达95%?我不信,于是亲自回测了下…
后端·python