瞭解C#的協變和逆變

RyzenAdorer發表於2022-01-06

前言

在引用型別系統時,協變、逆變和不變性具有如下定義。 這些示例假定一個名為 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

參考

相關文章