程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#泛談 —— 變體(協變/逆變)

C#泛談 —— 變體(協變/逆變)

編輯:C#入門知識

有如下四個類。

          

 

    =;

這樣的賦值肯定是沒問題的,但這只是多態。

變體的大概意思是:有T和U兩個類型,並且T = U (此處的等號為賦值)成立,如果T和U經過某種操作之後分別得到T’和U’,並且T’ = U’也成立,則稱此操作為協變;如果U’ = T’,則稱此操作為逆變。


T = ↓ Operation(T) = Operation(U);


T = U; = Operation2(T);

 

一、特殊的協變——數組

我們常說協變和逆變是.net 4.0中引入的概念,但實際上並不是。其實只要符合上面定義的,都是變體。我們先來看一個.net 1.0中就包含的一個協變:

    Animal[] animalArray =  Dog[];

這個不是多態,因為Dog[]的父類不是Animal[],而是object。
我們對照變體的定義來看一下,首先Animal = Dog,這個是成立的,因為Dog是Animal的子類;然後經過Array這個操作後,等式左右兩邊分別變成了Animal[]和dog[],並且這個等式仍然成立。這已經是滿足協變的定義了。

可能有人會困惑,這為什麼等號就成立了呢?
我們有一點要明確的是,因為C#語言規定了Array操作是協變,並且Compiler支持了,所以等式就成立了。變體都是人為定的,你甚至可以規定任何操作都是協變或者逆變,無非就是使編譯和在運行期變體處的賦值通過。

我們再看一下Array的應用:

    Animal[] animalArray =  Dog[
    animalArray[] =  Bird(); 

上面的代碼能編譯通過,Line1處也能運行通過,但是到了Line2處就會拋異常,所以說雖然Array這個操作是一個協變,但並不是安全的,在某些時候還是會出錯。

至於說為什麼要支持Array這樣的協變,據Eric Lippert在Covariance and Contravariance in C#, Part Two: Array Covariance說,是為了兼容Java的語法,雖然他本人也不是很滿意這樣的設計。

 

二、委托中的變體

在.net 2.0中委托也支持了協變,不過暫時還只是支持方法的賦值。

考慮下面的代碼

    
     
     OnAnimalCatched(Animal animal) {}  
     OnDogCatched(Dog dog) {}  
= OnDogCatched; 
    catchDog = OnAnimalCatched;  

同樣的,下面就是一個協變。

    
    
    Animal CatchAnAnimal() {   Animal(); } 
    Dog CatchADog() {   Dog(); } 
= CatchAnAnimal; 
    animalCatching = CatchADog; 

 

至於Action<T>和Func<TResult>(.net 3.5)等泛型委托,其實也是如此,同樣只局限於方法給委托實例賦值,而不支持委托實例賦值給委托實例。下面的例子編譯時會報錯。

    Action<Animal> aa = animal =><Dog> ad = aa;  

 

三、泛型中的變體

我們常說的協變和逆變,大多數指的是.net 4.0中引入的針對泛型委托和泛型接口的變體。

泛型委托

 我們發現,到了.net 4.0,之前不能編譯的這段代碼通過了

    Action<Animal> aa = animal =><Dog> ad = aa;  

 

其實是Action的簽名變了,多了in這個關鍵字。

       Action<T>(T obj); 
       Action< T>(T obj); 

 

類似的,Func的簽名也變了,多了out關鍵字

      TResult Func<TResult>(); 
      TResult Func< TResult>(); 

in和out就是C# 4.0中用於在泛型中顯式的表示協變和逆變的關鍵字。in表示逆變參數,out表示協變參數。

對於泛型委托的變體這一塊上,.net 4.0相對於之前的版本主要增強的就是委托實例賦值委托實例(方法賦值給委托實例是.net 2.0就支持的)。

泛型接口

在.net 4.0以前,Array是協變的(盡管它不安全),但IList<T>卻不是,IEnumerable<T>也不是。而到了.net 4.0,我們終於可以這樣干了:

    IEnumerable<Animal> animals =  List<Dog>();  

 

不過以下的操作還是會造成編譯失敗:

    <Animal> a2 =  List<Dog>(); 

 

究其原因,當然還是因為<T>在.net 4.0中是協變的,IList<T>不是:

      IEnumerable< T>  IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable

 

那泛型接口既然有協變的,同樣也有逆變的,如<T>。

 

四、一些疑問

1,問:我們自定義的泛型接口和泛型委托是否可以隨便加上in/out關鍵字,來表明它是逆變或者協變的?

答:這個當然是不可能的,編譯器會校驗。

一般來說,如果一個泛型接口中,所有用到T的方法,都只將其用於輸入參數,則T可以是逆變參數;如果用到T的方法,都只將其用於返回值,則T可以是協變參數。

委托的

2,問:既然in/out不能亂加,為什麼還要加呢?完全由編譯器來決定協變或者逆變的賦值不可以麼?

答:這個理論上應該是可以的,不過in/out關鍵字就像是一個泛型委托和泛型接口定義者同使用者之間的契約,必須顯式的指定使用方式,否則,程序中出現一些既不是多態,又沒有標明是協變或逆變,卻可以賦值成功的代碼,看起來比較混亂。

3,問:是不是所有的泛型委托和接口都遵從輸入參數是協變的,輸出參數是逆變的這一規律呢?

答:我們定義一個泛型委托Operate<T>,它的輸入參數是一個Action<T>

  Operate<T>(Action<T> action);

Action<Mammal> MammalEat = mammal => Console.WriteLine(<Panda> PandaEat = panda => Operate<Mammal> MammalOperation = action => action( Dog()); 這裡是允許的。

然後我們可以執行下面的操作

    
MammalOperation(MammalEat);

如果我們想讓這個泛型委托是一個變體,按照我們通常的理解,T是用作輸入參數的,那肯定就是逆變,應該加上in關鍵字。我們不考慮編譯器的提示,假設定義成這樣:

      Operate< T>(Action<T> action);

因為是逆變,所以,我們可以將Operate<Mammal>賦給Operate<Panda>

    Operate<Panda> PandaOperate = MammalOperation;

由於上面這個Operate的T已經改成了Panda,所以其對應參數Action的T也應該改為Panda,所以上面的“操作1”可以改成這樣:

    
    MammalOperation(PandaEat);

最終變成了PandaOperate = (new Dog())

 

    Operate<Animal> AnimalOperate = MammalOperation;

 

上面這個例子似乎說明了,也並不是所有的輸入參數都是逆變的?其實這已經不完全是一個輸入參數了,由於有Action<T>的影響,似乎就變成了“逆逆得協”?如果把Action<T>換成Func<T>,則Operate<T>就應該用in關鍵字了。是不是比較費腦?還好平時工作中很少碰到這種情況,更何況還有編譯器給我們把關。

 

以上內容參考自Eric Lippert的Covariance and Contravariance In C#系列,對.net中協變逆變的進化做了很詳細的描述,有興趣可以看一下。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved