簡體   English   中英

C#中的協方差有哪些? (或者,協方差:例如)

[英]What are the kinds of covariance in C#? (Or, covariance: by example)

協方差(大致)是在使用它們的復雜類型中鏡像 “簡單”類型的繼承的能力。
例如,我們總是可以將Cat的實例視為Animal一個實例。 如果ComplexType是協變的,則ComplexType<Cat>可以被視為ComplexType<Animal>

我想知道:協方差的“類型”是什么,它們如何與C#相關(它們是否受支持?)
代碼示例會很有幫助。

例如,一種類型是返回類型協方差 ,由Java支持,但不支持C#。

我希望有功能編程印章的人也可以加入!

這是我能想到的:

更新

在閱讀了Eric Lippert提出的建設性意見和大量文章之后,我改進了答案:

  • 更新了數組協方差的破壞性
  • 添加了“純”代理方差
  • 從BCL添加了更多示例
  • 添加了指向深入解釋概念的文章的鏈接。
  • 添加了關於高階函數參數協方差的全新部分。

返回類型協方差

可用Java(> = 5) [1]和C ++ [2] ,C#不支持(Eric Lippert解釋了為什么不能以及你可以做些什么 ):

class B {
    B Clone();
}

class D: B {
    D Clone();
}

接口協方差 [3] - 在C#中受支持

BCL將通用IEnumerable接口定義為協變:

IEnumerable<out T> {...}

因此以下示例有效:

class Animal {}
class Cat : Animal {}

IEnumerable<Cat> cats = ...
IEnumerable<Animal> animals = cats;

請注意,根據定義, IEnumerable是“只讀” - 您無法向其添加元素。
IList<T>的定義形成對比,可以使用.Add()進行修改:

public interface IEnumerable<out T> : ...  //covariant - notice the 'out' keyword
public interface IList<T> : ...            //invariant

通過方法組[4] 委派協方差 - 在C#中支持

class Animal {}
class Cat : Animal {}

class Prog {
    public delegate Animal AnimalHandler();

    public static Animal GetAnimal(){...}
    public static Cat GetCat(){...}

    AnimalHandler animalHandler = GetAnimal;
    AnimalHandler catHandler = GetCat;        //covariance

}

“純粹的”代表協方差 [5 - pre-variance-release article] - 在C#中得到支持

不帶參數並返回內容的委托的BCL定義是協變的:

public delegate TResult Func<out TResult>()

這允許以下內容:

Func<Cat> getCat = () => new Cat();
Func<Animal> getAnimal = getCat; 

數組協方差 - 在C# 中以破碎的方式支持 [6] [7]

string[] strArray = new[] {"aa", "bb"};

object[] objArray = strArray;    //covariance: so far, so good
//objArray really is an "alias" for strArray (or a pointer, if you wish)


//i can haz cat?
object cat == new Cat();         //a real cat would object to being... objectified.

//now assign it
objArray[1] = cat                //crash, boom, bang
                                 //throws ArrayTypeMismatchException

最后 - 令人驚訝且有點令人費解
委托參數協方差 (是的,那是方差) - 用於高階函數。 [8]

帶有一個參數並且不返回任何內容的委托的BCL定義是逆變的

public delegate void Action<in T>(T obj)

忍受我。 讓我們定義一個馬戲團的動物訓練師 - 他可以被告知如何訓練動物(通過給他一個與該動物一起工作的Action )。

delegate void Trainer<out T>(Action<T> trainingAction);

我們有培訓師的定義,讓我們找一個培訓師並讓他去工作。

Trainer<Cat> catTrainer = (catAction) => catAction(new Cat());

Trainer<Animal> animalTrainer = catTrainer;  
// covariant: Animal > Cat => Trainer<Animal> > Trainer<Cat> 

//define a default training method
Action<Animal> trainAnimal = (animal) => 
   { 
   Console.WriteLine("Training " + animal.GetType().Name + " to ignore you... done!"); 
   };

//work it!
animalTrainer(trainAnimal);

輸出證明這有效:

訓練貓忽略你......完成!

為了理解這一點,一個笑話是有序的。

有一天,一位語言學教授正在為他的班級講課。
“在英語中,”他說,“雙重否定形成了積極的一面。
然而,“他指出,”沒有一種語言,其中雙陽性可以形成負面。“

房間后面傳來一個聲音,“是的,沒錯。”

與協方差有什么關系?!

讓我嘗試一下背面的演示。

Action<T>是逆變的,即它“翻轉”類型的關系:

A < B => Action<A> > Action<B> (1)

使用Action<A>Action<B>更改上面的AB並獲取:

Action<A> < Action<B> => Action<Action<A>> > Action<Action<B>>  

or (flip both relationships)

Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (2)     

將(1)和(2)放在一起,我們有:

,-------------(1)--------------.
 A < B => Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (4)
         `-------------------------------(2)----------------------------'

但我們的Trainer<T>委托實際上是一個Action<Action<T>>

Trainer<T> == Action<Action<T>> (3)

所以我們可以將(4)重寫為:

A < B => ... => Trainer<A> < Trainer<B> 

- 根據定義,這意味着培訓師是協變的。

簡而言之,應用Action 兩次得到反對方差,即類型之間的關系翻轉兩次 (見(4)),所以我們回到協方差。

最好用更通用的結構類型來解釋。 考慮:

  1. 元組類型:(T1,T2),一對類型T1和T2(或更一般地,n元組);
  2. 函數類型:T1 - > T2,參數類型為T1且結果為T2的函數;
  3. 可變類型:Mut(T),一個持有T的可變變量。

元組在它們的組件類型中是協變的,即(T1,T2)<(U1,U2)iff T1 <U1和T2 <U2(其中'<'表示is-subtype-of)。

函數在它們的結果中是協變的,在它們的參數中是逆變的,即(T1 - > T2)<(U1 - > U2)iff U1 <T1和T2 <U2。

可變類型是不變的,即只有當T = U時Mut(T)<Mut(U)。

所有這些規則都是最通用的正確子類型規則。

現在,像主流語言中所知的對象或接口類型可以被解釋為包含其作為函數的方法的元組的奇特形式。 例如,界面

interface C<T, U, V> {
  T f(U, U)
  Int g(U)
  Mut(V) x
}

基本上代表了類型

C(T, U, V) = ((U, U) -> T, U -> Int, Mut(V))

其中f,g和x分別對應於元組的第一,第二和第三分量。

從上述規則可以得出,C(T,U,V)<C(T',U',V')iff T <T'且U'<U且V = V'。 這意味着泛型類型C在T中是協變的,在U中是逆變的,在V中是不變的。

另一個例子:

interface D<T> {
  Int f(T)
  T g(Int)
}

D(T) = (T -> Int, Int -> T)

這里,D(T)<D(T')僅在T <T'和T'<T時。一般情況下,只有在T = T'的情況下,所以D實際上在T中是不變的。

還有第四種情況,有時稱為“雙變量”,它同時意味着共變和反變。 例如,

interface E<T> { Int f(Int) }

在T中是雙變量的,因為它實際上並未使用。

Java對通用類型使用了使用站點方差的概念:在每個使用站點指定所需的方差。 這就是為什么Java程序員需要熟悉所謂的PECS規則的原因。 是的,它很笨拙,已經受到了很多批評。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM