• 了解C#的协变和逆变


    前言

    在引用类型系统时,协变、逆变和不变性具有如下定义。 这些示例假定一个名为 Base 的基类和一个名为 Derived的派生类。

    • Covariance

    使你能够使用比原始指定的类型派生程度更大的类型。

    你可以将 IEnumerable 的实例分配给 IEnumerable 类型的变量。

    • Contravariance

    使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。

    你可以将 Action 的实例分配给 Action 类型的变量。

    • Invariance

    表示只能使用最初指定的类型。 固定泛型类型参数既不是协变,也不是逆变。

    你无法将 List 的实例分配给 List 类型的变量,反之亦然。

    以上来自于官方文档对协变、逆变、不变性的解释

    为啥C#需要协变和逆变?

    我们首先来看一段代码:

    
    class FooBase{ }
    
    class Foo : FooBase 
    {
    
    }
    
    var foo = new Foo();
    FooBase fooBase = foo;
    
    //以下代码在.NET 4.0之前是不被支持的
    IEnumerable<Foo> foo = new List<Foo>();
    IEnumerable<FooBase> fooBase = foo;
    
    

    因此,在这里实际上可以回答,C#的协变和逆变就是主要有两种目的:

    • 兼容性:.NET2.0就推出了泛型,而从.NET 2.0到.NET 3.5期间不支持对泛型接口中的占位符T支持隐式转换,因此在.NET4.0推出协变和逆变
    • 为了支持更广泛的隐式类型的转换,在这里就是在泛型体系中支持

    在C#中,目前只有泛型接口和泛型委托可以支持协变和逆变,

    协变(Covariance)

    内置的泛型协变接口,IEnumerator<T>IQuerable<T>IGrouping<Tkey, TElement>:

    
        public interface IEnumerable<out T> : IEnumerable
        {
            new IEnumerator<T> GetEnumerator();
        }
    
    
        public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable
        {
    
        }
    
    
        public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>, IEnumerable
        {
          TKey Key { get; }
        }
    
    
    
    

    因此这段代码在.NET4.0及以上版本将不会编译报错:

    IEnumerable<Foo> foo = new List<Foo>();
    IEnumerable<FooBase> fooBase = foo;
    
    

    实际上,对于协变,有下面的约束,否则则会在编译时报错:

    • 泛型参数占位符以out关键子标识,并且占位符T只能用于只读属性、方法或者委托的返回值,out简而易懂,就是输出的意思
    • 当要进行类型转换,占位符T要转换的目标类型也必须是其基类,上述例子则是Foo隐式转为FooBase

    逆变(Contravariance)

    内置的泛型逆变委托ActionFunc Predicate,内置的泛型逆变接口IComparable<T>IEquatable<T>:

    
      public delegate void Action<in T>(T obj);
    
      public delegate TResult Func<in T, out TResult>(T arg);
    
      public delegate bool Predicate<in T>(T obj);
    
    
      public interface IComparable<in T>
      {
        int CompareTo(T? other);
      }
    
      public interface IEquatable<T>
      {
        bool Equals(T? other);
      }
    
    

    而逆变的用法则是这样:

    Action<FooBase> fooBaseAction = new Action<FooBase>((a)=>Console.WriteLine(a));
    
    Action<Foo> fooAction = fooBaseAction;
    
    

    而对于逆变,则跟协变相反,有下面的约束,否则也是编译时报错:

    • 要想标识为逆变,应该是要在占位符T前标识in,只能用于只写属性、方法或者委托的输入参数
    • 当要进行类型转换,占位符T要转换的目标类型也必须是其子类,上述例子则是FooBase转为Foo

    总结

    • 协变和逆变只对泛型委托和泛型接口有效,对普通的泛型类和泛型方法无效
    • 协变和逆变的类型必须是引用类型,因为值类型不具备继承性,因此类型转换存在不兼容性
    • 泛型接口和泛型委托可同时存在协变和逆变的类型参数,即占位符T

    参考

  • 相关阅读:
    「搬运」影魔
    「不会」斯特林数
    「不会」二项式反演
    「不会」插头dp
    「不会」主定理
    「不会」网络流
    接口和抽象类的区别
    TestNG常用注解
    九九乘法表-Java
    冒泡排序和选择排序--Java
  • 原文地址:https://www.cnblogs.com/ryzen/p/15771954.html
Copyright © 2020-2023  润新知