了解C#的协变和逆变

语言: CN / TW / HK

前言

在引用类型系统时,协变、逆变和不变性具有如下定义。 这些示例假定一个名为 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#的协变和逆变就是主要有两种目的:

T

在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)

内置的泛型逆变委托 ActionFuncPredicate ,内置的泛型逆变接口 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

参考