有如下四個類。
=;
這樣的賦值肯定是沒問題的,但這只是多態。
變體的大概意思是:有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中協變逆變的進化做了很詳細的描述,有興趣可以看一下。