C#泛谈 —— 变体(协变/逆变)
有如下四個類。
public class Animal{}public class Mammal : Animal{}public class Dog : Mammal{public void EatBone(){}}public class Panda : Mammal{public void EatBamboo(){}}?
Animal animal = new Dog();這樣的賦值肯定是沒問題的,但這只是多態。
變體的大概意思是:有T和U兩個類型,并且T = U (此處的等號為賦值)成立,如果T和U經過某種操作之后分別得到T’和U’,并且T’ = U’也成立,則稱此操作為協變;如果U’ = T’,則稱此操作為逆變。
//以下代碼能通過,則說明Operation是協變。T = U; //=表示賦值↓ Operation(T) = Operation(U);
//類似的,以下操作為逆變。
T = U; ↓ Operation2(U) = Operation2(T);
?
一、特殊的協變——數組
我們常說協變和逆變是.net 4.0中引入的概念,但實際上并不是。其實只要符合上面定義的,都是變體。我們先來看一個.net 1.0中就包含的一個協變:
Animal[] animalArray = new Dog[10];這個不是多態,因為Dog[]的父類不是Animal[],而是object。
我們對照變體的定義來看一下,首先Animal = Dog,這個是成立的,因為Dog是Animal的子類;然后經過Array這個操作后,等式左右兩邊分別變成了Animal[]和dog[],并且這個等式仍然成立。這已經是滿足協變的定義了。
可能有人會困惑,這為什么等號就成立了呢?
我們有一點要明確的是,因為C#語言規定了Array操作是協變,并且Compiler支持了,所以等式就成立了。變體都是人為定的,你甚至可以規定任何操作都是協變或者逆變,無非就是使編譯和在運行期變體處的賦值通過。
我們再看一下Array的應用:
Animal[] animalArray = new Dog[10]; //Line1animalArray[0] = new Bird(); //Line2上面的代碼能編譯通過,Line1處也能運行通過,但是到了Line2處就會拋異常,所以說雖然Array這個操作是一個協變,但并不是安全的,在某些時候還是會出錯。
至于說為什么要支持Array這樣的協變,據Eric Lippert在Covariance and Contravariance in C#, Part Two: Array Covariance說,是為了兼容Java的語法,雖然他本人也不是很滿意這樣的設計。
?
二、委托中的變體
在.net 2.0中委托也支持了協變,不過暫時還只是支持方法的賦值。
考慮下面的代碼
//一個入參為Dog的委托。抓住了一只Dog,應該怎么處理?delegate void DogCatched(Dog d); //定義兩個方法void OnAnimalCatched(Animal animal) {} //處理抓到的Animalvoid OnDogCatched(Dog dog) {} //處理抓到的Dog Catch catchDog = OnDogCatched; //把抓到的Dog交給處理Dog的方法catchDog = OnAnimalCatched; //把抓到的Dog交給處理Animal的方法以上兩個賦值都可以成功,其中第一個為符合委托原型的賦值。第二個則可以看做是Operate(Dog) = Operate(Animal),那這是一個逆變。
同樣的,下面就是一個協變。
//一個返回值為Animal的委托,一個需要抓到一只Animal的任務delegate Animal AnimalCatching();//兩個方法Animal CatchAnAnimal() { return new Animal(); } //抓到一個AnimalDog CatchADog() { return new Dog(); } //抓到一個Dog AnimalCatching animalCatching = CatchAnAnimal; //把抓Animal的任務交給能抓到Animal的方法animalCatching = CatchADog; //把抓Animal的任務交給能抓到Dog的方法?
至于Action<T>和Func<TResult>(.net 3.5)等泛型委托,其實也是如此,同樣只局限于方法給委托實例賦值,而不支持委托實例賦值給委托實例。下面的例子編譯時會報錯。
Action<Animal> aa = animal => { };Action<Dog> ad = aa; //編譯錯誤?
三、泛型中的變體
我們常說的協變和逆變,大多數指的是.net 4.0中引入的針對泛型委托和泛型接口的變體。
泛型委托
?我們發現,到了.net 4.0,之前不能編譯的這段代碼通過了
Action<Animal> aa = animal => { };Action<Dog> ad = aa; //編譯通過?
其實是Action的簽名變了,多了in這個關鍵字。
public delegate void Action<T>(T obj); //.net 4 之前public delegate void Action<in T>(T obj); //.net 4?
類似的,Func的簽名也變了,多了out關鍵字
public delegate TResult Func<TResult>(); //.net 4 之前public delegate TResult Func<out TResult>(); //.net 4in和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 = new List<Dog>(); //.net 4正確?
不過以下的操作還是會造成編譯失敗:
IList<Animal> a2 = new List<Dog>(); //錯誤?
究其原因,當然還是因為IEnumerable<T>在.net 4.0中是協變的,IList<T>不是:
public interface IEnumerable<out T> : IEnumerablepublic interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable?
那泛型接口既然有協變的,同樣也有逆變的,如IComparable<T>。
?
四、一些疑問
1,問:我們自定義的泛型接口和泛型委托是否可以隨便加上in/out關鍵字,來表明它是逆變或者協變的?
答:這個當然是不可能的,編譯器會校驗。
一般來說,如果一個泛型接口中,所有用到T的方法,都只將其用于輸入參數,則T可以是逆變參數;如果用到T的方法,都只將其用于返回值,則T可以是協變參數。
委托的輸入參數可以是逆變參數;返回值可以是協變參數。
2,問:既然in/out不能亂加,為什么還要加呢?完全由編譯器來決定協變或者逆變的賦值不可以么?
答:這個理論上應該是可以的,不過in/out關鍵字就像是一個泛型委托和泛型接口定義者同使用者之間的契約,必須顯式的指定使用方式,否則,程序中出現一些既不是多態,又沒有標明是協變或逆變,卻可以賦值成功的代碼,看起來比較混亂。
3,問:是不是所有的泛型委托和接口都遵從輸入參數是協變的,輸出參數是逆變的這一規律呢?
答:我們定義一個泛型委托Operate<T>,它的輸入參數是一個Action<T>
delegate void Operate<T>(Action<T> action); //兩個Action<T>的實例Action<Mammal> MammalEat = mammal => Console.WriteLine("mammal eat");Action<Panda> PandaEat = panda => panda.EatBamboo();
//Operate<T>的實例Operate<Mammal> MammalOperation = action => action(new Dog()); //Action<T>是逆變,所以這里是允許的。
然后我們可以執行下面的操作
//操作1MammalOperation(MammalEat);
如果我們想讓這個泛型委托是一個變體,按照我們通常的理解,T是用作輸入參數的,那肯定就是逆變,應該加上in關鍵字。我們不考慮編譯器的提示,假設定義成這樣:
delegate void Operate<in T>(Action<T> action);因為是逆變,所以,我們可以將Operate<Mammal>賦給Operate<Panda>
Operate<Panda> PandaOperate = MammalOperation;由于上面這個Operate的T已經改成了Panda,所以其對應參數Action的T也應該改為Panda,所以上面的“操作1”可以改成這樣:
//操作2MammalOperation(PandaEat);最終變成了PandaOperate = (new Dog()).EatBamboo()。這是個啥?完全不合常理。
實際上,當我們給Operate<T>加上in的時候,編譯器就已經告訴我們,這是不對的了。寫成out就可以了,說明這是一個協變,下面的操作也是可以的:
?
Operate<Animal> AnimalOperate = MammalOperation;?
上面這個例子似乎說明了,也并不是所有的輸入參數都是逆變的?其實這已經不完全是一個輸入參數了,由于有Action<T>的影響,似乎就變成了“逆逆得協”?如果把Action<T>換成Func<T>,則Operate<T>就應該用in關鍵字了。是不是比較費腦?還好平時工作中很少碰到這種情況,更何況還有編譯器給我們把關。
?
以上內容參考自Eric Lippert的Covariance and Contravariance In C#系列,對.net中協變逆變的進化做了很詳細的描述,有興趣可以看一下。
?
轉載于:https://www.cnblogs.com/joecheung/p/3139124.html
總結
以上是生活随笔為你收集整理的C#泛谈 —— 变体(协变/逆变)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Rocket - tilelink -
- 下一篇: 我对模板模式和策略模式的理解