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

[C#]淺談協變與逆變,

編輯:C#入門知識

[C#]淺談協變與逆變,


看過幾篇說協變與逆變的博客,雖然都是正確無誤的,但是感覺都沒有說得清晰明了,沒有切中要害。
那麼我也試著從我的理解角度來談一談協變與逆變吧。


什麼是協變與逆變

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方法,完全的類型安全轉換。

 

最後想說的是,所有死記硬背來的知識,都遠遠不如充分理解的知識來得可靠。

  1. 上一頁:
  2. 下一頁: