看過幾篇說協變與逆變的博客,雖然都是正確無誤的,但是感覺都沒有說得清晰明了,沒有切中要害。
那麼我也試著從我的理解角度來談一談協變與逆變吧。
什麼是協變與逆變
MSDN的解釋:
https://msdn.microsoft.com/zh-cn/library/dd799517.aspx
協變和逆變都是術語,前者指能夠使用比原始指定的派生類型的派生程度更小(不太具體的)的類型,後者指能夠使用比原始指定的派生類型的派生程度更大(更具體的)的類型。
泛型類型參數支持協變和逆變,可在分配和使用泛型類型方面提供更大的靈活性。
一開始我總是分不清協變和逆變,因為MSDN的解釋實在是嚴謹有余而易讀不足。
其實從中文的字面上來理解這兩個概念就挺容易的了:
"協變"即"協調的轉變","逆變"即"逆向的轉變"。
為什麼說"能夠使用比原始指定的派生類型的派生程度更小(不太具體的)的類型"是協調的,而"能夠使用比原始指定的派生類型的派生程度更大(更具體的)的類型"是逆向的呢,看這兩行代碼:
object o = ""; string s = (string) o;
string類型到object類型,也就是派生類到基類,是可以隱式轉換的,因為任何類型向基類的轉換都是類型安全的,所以認為這一轉變是協調的。
object類型到string類型,也就是基類到派生類,就只能是顯式轉換,因為對象o的實際類型不一定是string,強制轉換不是類型安全的,所以認為這一轉變是逆向的。
再看協變與逆變的常見場合:
IEnumerable<object> o = new List<string>();//協變 Action<string> s = new Action<object>((arg)=>{...});//逆變
上例的泛型參數就是分別發生了協調的與逆向的轉變。
協變與逆變的作用對象
從定義中可以看到,協變與逆變都是針對的泛型參數,而且
在.NET Framework 4中,Variant類型參數僅限於泛型接口和泛型委托類型。
為什麼是接口和委托?先看IEnumerable<T>和Action<T>的聲明:
public interface IEnumerable<out T> : IEnumerable { new IEnumerator<T> GetEnumerator(); } public delegate void Action<in T>(T obj);
IEnumerable中的out關鍵字給泛型參數提供了協變的能力,Action中的in關鍵字給泛型參數提供了逆變的能力。
這裡的out和in是相對於誰的入和出?不是相對於接口和委托,而是相對於方法體!
看它們的實現:
class MyEnumerable<T> : IEnumerable<T> { public IEnumerator<T> GetEnumerator() { yield return default(T); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } Action<string> myAction = new Action<object>( (o) => { Console.WriteLine(o.ToString()); });
這樣是不是能看出來泛型參數是怎麼入和出的了?
那麼接口和委托,它們和方法是什麼關系呢,它們兩個之間又是什麼關系,以下純屬個人理解:
接口類型定義了一組方法簽名,委托類型定義了一個方法結構(方法簽名刨除方法名)。
接口實例和委托實例都包含了一組方法入口。
綜上所述,協變與逆變的作用對象是方法體中的泛型參數。
為什麼允許協變與逆變
協變和逆變都是類型發生了轉換,一旦涉及到類型轉換當然就要想類型安全的問題。
協變和逆變之所以可以正常的運轉,就是因為這裡所涉及到的所有類型轉換都是類型安全的!
回頭看最開始的四行代碼:
1 object o1 = "";//類型安全 2 string s1 = (string) o1;//非類型安全 3 IEnumerable<object> o2 = new List<string>();//協變 4 Action<string> s2 = new Action<object>((arg)=>{...});//逆變
顯然第二行的object到string是非類型安全的,那為什麼第四行的object到string就是類型安全的呢?
結合上一個方法體的示例,來看這段代碼:
1 Action<List<int>> myAction = new Action<IList<int>>( 2 (list) => 3 { 4 Console.WriteLine(list.Count); 5 }); 6 myAction(new List<int> {1, 2, 3});
第一行貌似是把IList轉換成了List,但是實際上是這樣的:
第六行傳入的實參是一個List,進入方法體,List被轉換成了IList,然後使用了IList的Count屬性。
所以傳參的時候其實發生的是派生類到基類的轉換,自然也就是類型安全的了。
List<string>到IEnumerable<object>的協變其實也是類似的過程:
1 IEnumerable<Delegate> myEnumerable = new List<Action> 2 { 3 new Action(()=>Console.WriteLine(1)), 4 new Action(()=>Console.WriteLine(2)), 5 new Action(()=>Console.WriteLine(3)), 6 }; 7 foreach (Delegate dlgt in myEnumerable) 8 { 9 dlgt.DynamicInvoke(); 10 }
實參是三個Action,調用的是Delegate的DynamicInvoke方法,完全的類型安全轉換。
最後想說的是,所有死記硬背來的知識,都遠遠不如充分理解的知識來得可靠。