簡述 C# 泛型介面的變異性設計時機

這篇文章討論設計泛型介面的時候,共變與逆變的設計時機。

 

決戰設計模式第七梯招生中決戰設計模式第七梯招生中

 

變異性向來是個不太好理解的問題,扯到泛型的時候就更容易讓人摸不著頭緒。

先來聊聊變異性,變異性 (Variance) 分為共變 (Covariant) 與 逆變 (Contravariant,微軟文件稱之為反變)。簡單來說,共變的意思就是『在使用衍生型別的地方可以使用基底型別替代』;而逆變則是反過來『在使用基底型別的地方可以使用衍生型別替代』。

C# 中只有泛型介面和泛型委派的型別參數容許變異性,泛型類別是不允許的。不談太多的深刻道理,簡單聊一下設計泛型介面的時候,哪種情況可以將泛型參數設計成共變或逆變。

共變 Covariant

C# 中泛型參數要設定為共變的宣告是 out 泛型修飾詞,典型的例子就是 IEnumerable<T>,參考 Microsoft Docs IEnumerable<T> Interface 可以看到它的宣告如下,這表示該 T 型別具有共變性:

public interface IEnumerable<out T> : System.Collections.IEnumerable

當 T 型別在該泛型介面的內部成員中僅用於回傳值時,該 T 型別可以宣告為共變性,例如:

 public interface ICovariance<out T>
 {

     // 作為只有 getter 的屬性型別
     T Data { get; }

     // 作為方法的回傳型別
     T GetResult();
 }
逆變

對於逆變是使用 in 泛型修飾詞,典型的例子就是 IComparable<T>,參考 Microsoft Docs IComparable<T> Interface 可以看到它的宣告如下,這表示該 T 型別具有逆變性: 

public interface IComparable<in T>

當 T 型別在該泛型介面的內部成員中僅用於方法的傳入參數時,該 T 型別可以宣告為逆變性,例如:

 public interface IContravariance<in T>
 {
     // 作為只有 setter 的屬性型別
     T Data { set; }

     // 作為方法的傳入型別
     bool EqualsTo(T other);
 }

很容易判斷對吧;另外呢,同一個 T 型別不可以同時是 in 又是 out,若一個 T 型別在方法內部同時作為回傳值與傳入參數的型別使用,則該 T 型別只能保持不變性,例如:

  public interface IInvariance1<T>
  {
      T Data { get; set; }
  }

  public interface IInvariance2<T>
  {
      T GetResult();

      bool EqualsTo(T other);
  }

如果泛型介面擁有兩個以上的泛型參數,那有可能 T1 是 in , T2 是 out (或其他排列組合):

 public interface IDual<out T1,in T2>
 {
     T1 Data { get; }

     bool EqualsTo(T2 other);
 }