You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

面向对象编程中何为非协变?结合C#实例的疑问解答请求

协变与非协变的概念解析(基于C#代码实例)

示例代码

实体类与仓储类

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

public class EmployeeExtendedDetail : Employee
{
    public string Address { get; set; }
    public string Email { get; set; }
}

public class EmployeeRepository
{
    public virtual Employee GetEmployeeDetialById(int id)
    {
        return new Employee();
    }
}

public class EmployeeExtendedDetailRepository : EmployeeRepository
{
    public override EmployeeExtendedDetail GetEmployeeDetialById(int id)
    {
        return new EmployeeExtendedDetail();
    }
}

调用代码示例

// 父类型引用指向子类实例
L1. EmployeeRepository empRepo1 = new EmployeeExtendedDetailRepository();
L2. Employee employee2 = empRepo1.GetEmployeeDetialById(1); // OK
L3. EmployeeExtendedDetail employee3 = empRepo1.GetEmployeeDetialById(1); //ERROR - 无法隐式转换
L4. EmployeeExtendedDetail employee4 = (EmployeeExtendedDetail)empRepo1.GetEmployeeDetialById(1); // OK

// 子类类型引用指向子类实例
L5. EmployeeExtendedDetailRepository empRepo2 = new EmployeeExtendedDetailRepository();
L6. Employee employee5 = empRepo2.GetEmployeeDetialById(1); // OK
L7. EmployeeExtendedDetail employee6 = empRepo2.GetEmployeeDetialById(1); // OK

核心疑问与解答

1. L2的操作属于协变吗?

是的。

首先,你代码里EmployeeExtendedDetailRepository重写父类方法时,返回Employee的子类EmployeeExtendedDetail,这本身就是C#允许的返回值协变——子类重写方法可以返回比父类方法更具体的类型。

而L2中把EmployeeExtendedDetail(子类实例)赋值给Employee(父类型变量),是典型的向上转型,完全符合协变的核心逻辑:子类型可以安全地替代父类型,方向和继承关系一致(从具体到抽象)。

至于L3报错,是因为编译期empRepo1的类型是EmployeeRepository,编译器只知道这个方法返回Employee,哪怕运行时实际返回子类,也不会自动做向下转型——向下转型必须显式强制转换,这是编译期类型检查的规则,和协变本身无关。

2. 什么是非协变?

非协变就是不允许子类型替代父类型,或者类型转换方向与继承逻辑相悖,导致类型安全被破坏的情况,举几个直观的例子:

  • 返回值非协变:如果父类方法返回EmployeeExtendedDetail,子类重写时返回Employee——这会直接编译报错,因为子类返回的类型比父类更抽象,违反了协变的「子换父」规则,也破坏了Liskov替换原则(父类调用者期望拿到更具体的子类实例,子类却返回了父类实例)。
  • 集合非协变:尝试把List<Employee>赋值给List<EmployeeExtendedDetail>变量——C#不允许这种操作,因为List<T>是不变类型。如果允许,就可能出现把Employee实例塞进本该存EmployeeExtendedDetail的集合里,导致后续操作抛出类型转换异常。
  • 参数非协变:父类方法参数是Employee,子类重写时把参数改成EmployeeExtendedDetail——这也违反Liskov替换原则,父类能接受任意Employee实例,子类却只能接受更具体的子类,导致父类的调用者用子类实例时,无法传入合法的Employee参数。

内容的提问来源于stack exchange,提问作者riki

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 21:12:47