C#协变与逆变:数组、委托及泛型的实战原理解析

江岸
江岸
发布时间:2026-10-13 | 4人浏览过
原创

在C#编程中,协变(Covariance)与逆变(Contravariance)是简化类型赋值的关键技术。简单来说,由子类向父类方向转变的过程称为协变,通常用于返回值场景;而由父类向子类方向转变则称为逆变,多应用于方法参数处理。本文将结合具体代码,深入探讨它们在数组、委托以及泛型接口中的实际应用与底层原理。

一、数组的协变特性

C#中数组天然支持协变。这是基于引用类型的继承关系,允许将派生类数组直接赋值给基类数组变量,编译器会确保这一转换的安全性。

第1步:执行数组协变操作

  • 入口与对象:定义一个基类 Animal 和它的子类 Dog。
  • 动作:创建一个 Dog 数组,并将其隐式转换为 Animal 数组。
Animal[] animalArray = new Dog[]{new Dog()};
//每个Dog对象都可以安全的转变为Animal类型

结果说明:声明的数组数据类型是 Animal,实际赋值时给的是 Dog 数组。这种沿着继承链向上(从子类到父类)的转变就是协变。

二、委托中的协变与逆变(非泛型)

在早期的C#版本中,委托通过其定义的特性自动推导是否支持协变或逆变。我们主要关注返回值和参数类型的匹配规则。

第1步:实现委托中的协变

  • 目的:利用子类的返回值满足基类返回值的委托需求。
  • 定义:
    public delegate Animal GetAnimal();
    static Dog GetDog(){ return new Dog(); }

  • 赋值操作:
    GetAnimal getMethod = GetDog;

原理解析:GetDog 返回的是 Dog 类型,因为 Dog 继承了 Animal,所以将其返回结果视为一个 Animal 是完全安全的。这就是委托中的协变表现。

第2步:实现委托中的逆变

  • 目的:使用参数范围更广的方法去满足特定参数的委托。
  • 定义:
    public delegate void FeedDog(Dog target);
    static void FeedAnimal(Animal target){}

  • 赋值操作:
    FeedDog feedDogMethod = FeedAnimal;

原理解析:虽然 FeedDog 要求传入一个具体的 Dog,但 FeedAnimal 能接受任何 Animal。既然所有 Dog 都是 Animal,那么用更通用的方法来处理特定的狗是安全的。这种从宽泛类型指向特定类型的过程即为逆变。

三、泛型委托的显式协变与逆变

在引入泛型后,为了明确控制类型参数的行为,C#提供了 in(逆变)和 out(协变)关键字。如果不加指定,泛型参数默认是不可变的。

第1步:配置泛型委托的逆变 (in)

  • 代码定义:
    public delegate void Feed<in T>(T target);
  • 应用实例:
    // 创建一个接收 Animal 的委托
    Feed<Animal> feedAnimalMethod = a => Console.WriteLine("Feed animal lambda");
    // 将 T=Animal 的委托赋值给 T=Dog 的委托变量
    Feed<Dog> feedDogMethod = feedAnimalMethod;

注意:由于尖括号中使用了 in 关键字,编译器知道这是一个逆变场景。因此,可以将更宽的 Animal 委托赋给更窄的 Dog 委托变量。若去掉 in 关键字,编译器将报错。

第2步:配置泛型委托的协变 (out)

  • 代码定义:
    public delegate T Find<out T>();
  • 应用实例:
    // 创建一个返回 Dog 的委托
    Find<Dog> findDog = () => new Dog();
    // 将 T=Dog 的委托赋值给 T=Animal 的委托变量
    Find<Animal> findAnimal = findDog;

原理解析:out 关键字表明该泛型仅作为输出(返回值)。因为 Dog 可以被视为 Animal,所以 Find<Dog> 可以被赋给 Find<Animal>,实现了从子类到父类的协变。

四、泛型接口中的协变与逆变

泛型接口的类型参数同样需要显式标记以支持类型转换规则。

第1步:构建支持逆变的泛型接口

  • 接口定义:
    public interface IFeedable<in T>
    {
        void Feed(T t);
    }

  • 实现类:
    public class FeedImp<T> : IFeedable<T>
    {
        public void Feed(T t)
        {
            Console.WriteLine($"Feed Animal: {t.GetType()}");
        }
    }

  • 调用示例:
    // 创建接收 Animal 的实例,赋予 Dog 接口的变量
    IFeedable<Dog> feedDog = new FeedImp<Animal>();

总结:这里将更广泛的 IFeedable<Animal> 实例赋给了特定的 IFeedable<Dog> 变量,属于典型的逆变应用。

第2步:构建支持协变的泛型接口

  • 接口定义:
    public interface IFinder<out T>
    {
        T Find();
    }

  • 实现类:
    public class Finder<T> : IFinder<T> where T : new()
    {
        public T Find()
        {
            return new T();
        }
    }

  • 调用示例:
    // 创建返回 Dog 的实例,赋予 Animal 接口的变量
    IFinder<Animal> finder = new Finder<Dog>();

总结:此处 IFinder<Animal> 接受了 Finder<Dog> 的值,即从具体的子类转到了抽象的父类,符合协变规范。

五、完成判断与排错

  • 编译检查:上述所有赋值语句若在缺少 in 或 out 修饰的情况下执行,编译器会抛出错误提示“无法将类型X隐式转换为Y”。加上对应关键字后即可顺利通过。
  • 运行状态:程序能够正常运行并打印预期的日志或返回正确的类型对象,证明类型系统已按预期完成隐式转换。
  • 排错建议:如果在定义接口时同时使用了 in 和 out,必须确保泛型类型参数既不作为方法的输入也不作为输出的属性(即完全纯函数式的只读/只写),否则会出现类型不匹配的冲突,这是最常见的开发陷阱。

以上就是C#协变与逆变在各类应用场景中的详细内容,更多关于C#泛型高级用法的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表