什麼是協變?

假設我們有兩個類型,AnimalDogDogAnimal的子類,我們定義以下兩個集合:

List<Animal>
List<Dog>

由於DogAnimal的子類,所以Dog可以隱式轉換為Animal,但C#中,List<Dog>無法隱式轉換為List<Animal>List<Dog>List<Animal>沒有任何繼承關係,他們是兩個完全獨立的對象,我們稱之為是不可變的。

Animal animal = new Dog(); //合法
List<Animal> animals = new List<Dog>(); //編譯錯誤
//Cannot convert initializer type 'List<Dog>' to target type 'List<Animal>'
//Type 'Dog' doesn't match expected type 'Animal'.

為什麼會有這種限制?因為這種轉換看似很合理,卻存在著嚴重的安全問題,舉個例子:我們假設允許List<Dog>List<Animal>的轉換,順便我們將這種「派生類的集合能夠隱式轉換為基類的集合」稱之為是協變的,就會出現以下情況:

//在C#中,數組是協變的,這裡用數組代替List<T>演示協變性
//由於string可以隱式轉換為object,所以數組的協變性允許string數組隱式轉換為object數組
object[] array = new string[] {"C#", "C++"};
//而對於一個object數組,以下操作在編譯期是合法的,可以通過編譯
array[0] = new Animal();
//但運行時會拋出異常:System.ArrayTypeMismatchException: 
//Attempted to access an element as a type incompatible with the array.
//因為往一個string數組添加一個Animal對象顯然是不合法的
//而數組的協變性導致我們無法在編譯期檢查出此類錯誤,只有運行時拋異常了才會發現問題

所以對於List<T>來講,不支援協變可以保證類型安全,不會出現數組的協變性所導致的這種安全問題。那為什麼C#的數組要設計成協變的?stackoverflow上有人提到一個解釋是:

Eric Lippert says:
Unfortunately, this particular kind of covariance is broken. It was added to the CLR because Java requires it and the CLR designers wanted to be able to support Java-like languages. We then up and added it to C# because it was in the CLR. This decision was quite controversial at the time and I am not very happy about it, but there』s nothing we can do about it now.

意思就是,數組的協變性被添加到了CLR里是因為Java需要它,而CLR的設計者希望可以支援Java-like的語言,而把它添加到C#里則是因為CLR支援了這個特性,這個決定在當時是很有爭議的,而且我個人不是很贊同這種做法,不過現在我們已經什麼也做不了了。
那Java又為什麼要把數組設計成協變的呢?答案是Java早期沒有泛型,而為了實現「泛型」的需求,就允許了任何類型的數組都可以轉換成object類型的數組,結果就是這樣了。總而言之,數組協變是一個歷史遺留缺陷了,很顯然現在回過頭來看,數組協變是一個很危險的設計,所以我們在使用數組時一定要注意類型安全的問題,一定要搞清楚所使用的數組的真實類型。

Tags: