面向对象编程中何为非协变?结合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
相关产品推荐
相关产品推荐

