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

10分鐘了解C#中的協變和逆變

編輯:C#入門知識

說十分鐘可能有嘩眾取寵的嫌疑,本人寫這個博客,話了半天時間,查閱了很多資料才完成,因此要弄懂協變逆變的本質,可能要多花點時間。


--------------------------------------------------------------------------------
很多文章中對於協變的描述大致如下:
 協變是一個細節化程度高的類型賦值給細節化程度低的類型類型。例如一個方法M,返回值是Giraffe(長頸鹿),你可以把M的返回值賦值給Animal(動物)類型,因為Animal類型是細節化程度底的類型,和Giraffe類兼容。那麼方法是協變的,因為當你創建了協變接口,需要用到關鍵字out,返回值從方法中out。

這根本就不是協變的意思。這只是描述了賦值兼容性,兩個類型可以兼容。


--------------------------------------------------------------------------------

協變的准確定義是什麼呢?

先不考慮類型,我們考慮數學意義上的整數。考慮整數之間的小於關系——≤。這裡關系實際上就是一個方法,接受2個數,返回布爾值。

現在我們考慮一下對於整數的投影,投影指的是一個函數,接受一個整數,返回另一個整數。比如 z → z + z 我們可以定義為D(double), z → 0 - z定義為N for (negate), z → z * z,定義為S(square).

問題就出來了,是不是所有的情況下, (x ≤ y) = (D(x) ≤ D(y))?事實上就是,如果x小於等於y,那麼x的2倍也小於等於y的2倍。投影D保留了不等號的方向。

對於N,很顯然,1 ≤ 2 但是 -1 ≥ -2。即(x ≤ y) = (N(y) ≤ N(x)),投影N反轉了不等號的方向。

對於S, -1 ≤ 0, S(0) ≤ S(-1),但是 1 ≤ 2, S(2)≥ S(1)。可見投影S即沒保留不等號方向,也沒反轉不等號方向。

投影D就是協變的,它保留了整數上的次序關系。投影N是逆變的,它反轉了整數上的次序關系,投影S兩者都不是,所以是不變的。

所以這裡很清楚,整數自身不是變體,小於關系也不是變體。投影才是協變或者逆變——接受一個整數生成一個新整數。 


--------------------------------------------------------------------------------

現在再來看類型,替換整數上的小於關系,我們在引用類型上也有一個小於關系。如果引用類型X的值可以存儲在類型Y上,那麼稱一個引用類型X小於或等於引用類型Y。

再考慮對於類型的投影,假設一個投影是把T變成IEnumerable<T>,即,如果接受一個參數Giraffe類型,返回一個新類型 IEnumerable<Giraffe>。那麼這個投影在C# 4.0中是協變嗎?是的,它保留了次序的方向。Giraffe可以賦值給Animal,因此Giraffes序列也可以賦值給IEnumerable<Giraffe>。

精煉的說,對於一個投影,如果A可以賦值給B,經過投影後的值A'可以賦值給B',那麼就可以說這個投影是一個協變。
我們可以認為 接受類型T,生成出類型IEnumerable<T>看作是一個投影,並稱這個投影為"IEnumerable<T>”。因此根據上下文,當我們說IEnumerable<T>是協變的,意思就是對於類型T生成類型IEnumerable<T>的投影是一個協變的投影。由於IEnumerable<T>只有一個類型參數,因此很明確我們說的參數就是T。

因此我們可以定義協變,逆變和不變。如果一個泛型類型I<T>,根據類型參數得出的結構,保留了賦值的兼容方向,那麼這個泛型類型是協變的。即如果一個泛型類型I<T>,對於類型A和B,如果A能賦值給B,而且I<A>也能賦值給I<B>,即保留了賦值的兼容方向,那麼說I<T>這個泛型類就是協變的。相反,逆變就是反轉了賦值的兼容方向。不變就是既不是協變也不是逆變。簡單准確的說,接受T,生成 I<T>的這樣的一個投影就是協變/逆變/不變的投影。


--------------------------------------------------------------------------------


在 C# 中,協變和逆變允許數組類型、委托類型和泛型類型參數進行隱式引用轉換。 協變保留分配兼容性,逆變與之相反。

數組是支持協變的,string兼容於object,string數組化後依然兼容於object。但是協變可能會導致類型不安全,如下例子:
object[] array = new String[10];
// 這裡會報錯,array[0]已經先分配給string了,無法再接受整型。 // array[0] = 10;

委托類型也支持協變。如下代碼,string兼容於object,返回值為string的fun兼容於返回值為object的委托。

public delegate object mydelege();

        static string fun2()
        {
            return "";
        }
       
        static void Main(string[] args)
        {
            mydelege md1 = fun2; //string兼容於object,返回值為string的fun兼容於返回值為object的委托
             }
對於泛型接口,在Framework4.0之前,不支持協變。例如: IEnumerable<object> b = new List<string>(); 無法編譯過。string兼容於object,但是List<string>無法兼容IEnumerable<object>,屬於不變。 但在Framework4.0之後,上述例子就能編譯過了,也就是說IEnumerable<object>這個泛型接口支持協變了。 Framework4.0後,支持協變的接口有很多。IEnumerable<T>、IEnumerator<T>、IQueryable<T> 和 IGrouping<TKey, TElement>

委托類型支持逆變。如下代碼:

兒子繼承父親

class Father
    { }

    class Son : Father
    { }

    class Program
    {
        public delegate void mydelege1(Father f);
                public delegate void mydelege2(Son s);

        static void fun1(Father s)
        {
        }
        static void fun2(Son s)
       {
        }

        static void Main(string[] args)
        {
            Father f = new Father();
            Son s = new Son();

            f = s;//ok,兒子可以賦值給父親

            mydelege1 md1 = fun2;//error,輸入參數不支持協變,兒子類型的方法不能賦值給父親類型的委托             mydelege2 md2 = fun1;//ok,父親類型的方法可以賦值給兒子類型的委托,逆變了。
        }
 
對於一個委托mydelege1(Father f),定義的輸入參數類型是Father。
可以看到son是可以賦值給father的,而經過委托定義這樣一個投影,發現son類型為參數的方法不能賦值給father類型為參數的委托。即經過這樣的委托投影,son類型方法無法賦值給father類型委托了。
而相反的,father類型為參數的方法卻可以賦值給son類型為參數的委托。即經過這樣的委托投影,father類型的方法可以賦值給son類型的委托了。這就逆天了,因此也就是逆變了。
簡單的來看,即投影前Son可以賦值給Fahter,投影(轉成委托類型)後Father可以賦值給Son,是一種逆變。

為什麼轉成委托後,Son類型的方法不能賦值給Father類型的委托了,很簡單,這個方法要接受的是Son的方法要處理的自然是Son類型的值,而Father為參數的委托可能接受Daughter類型的參數,(假設Daughter和Son並列的繼承了Father)。
因此Son方法就無法處理了。因此不允許這樣操作。 換成代碼來說,假設這樣的代碼合法了:
mydelege1 md1 = fun2;//假設是合法的,
那麼 md1(new Daughter())的代碼要處理的時候,肯定無法處理了。
所以,不允許存在這樣的協變,而只允許逆變。通過上面的一些例子,可以看出對於委托,協變只存在與返回值中,逆變只存在與輸入值中。


--------------------------------------------------------------------------------


再來看泛型委托中的協變和逆變 我們可以看微軟自定義的泛型委托,Func和Action這兩個。(Func必須有返回類型,Action返回void) 在Framework3.5的時候,Func,Action是不支持協變也不支持逆變。如下代碼:

class Father
    { }

    class Son : Father
    { }

    class Program
{
        static void Main(string[] args)
        {
            Func<Father> fatherfun = () => new Father();
            Func<Son> sonfun = () => new Son();
            fatherfun = sonfun;//無法將類型“System.Func<ConsoleApplication2.Son>”隱式轉換為“System.Func<ConsoleApplication2.Father>
            sonfun = fatherfun;//無法將類型“System.Func<ConsoleApplication2.Father>”隱式轉換為“System.Func<ConsoleApplication2.Son>”       }
    }

上面的代碼編譯不過。因此很是不方便,所以在Framework4.0後,允許其可以協變和逆變。在Framework4.0中,如下
 
static void Main(string[] args)
        {
            Func<Father> fatherfun = () => new Father();
            Func<Son> sonfun = () => new Son();
            fatherfun = sonfun;//ok協變成功,Son可以賦值給Father,Sonfun也可以賦值給Fatherfun了;
            sonfun = fatherfun;//無法將類型“System.Func<ConsoleApplication2.Father>”隱式轉換為“System.Func<ConsoleApplication2.Son>”。存在一個顯式轉換(是否缺少強制轉換?)

      }

對於逆變,如下代碼,同樣的,在Framework3.5時代,仍然是不可協變逆變。
 
 class Father
    { }

    class Son : Father
    { }
       class Program
    {
        static void Main(string[] args)
        {
            Action<Father> fatherfun;
            Action<Son> sonfun;

            fatherfun = sonfun;//無法將類型“System.Action<ConsoleApplication2.Son>”隱式轉換為“System.Action<ConsoleApplication2.Father>
            sonfun = fatherfun;//無法將類型“System.Action<ConsoleApplication2.Father>”隱式轉換為“System.Action<ConsoleApplication2.Son>”
        }
    }

在Framework4.0後,允許其可以逆變。
 
    static void Main(string[] args)
        {
            Action<Father> fatherfun;
            Action<Son> sonfun;

            fatherfun = sonfun;//無法將類型“System.Action<ConsoleApplication2.Son>”隱式轉換為“System.Action<ConsoleApplication2.Father>”。存在一個顯式轉換(是否缺少強制轉換?)           
           sonfun = fatherfun;//ok 逆變成功 

        }

以上是微軟提供的泛型委托,事實上自己也可以定義自己的泛型委托,也可以知道這個委托是否支持協變或者逆變。在C#中,通過對參數標注in和out來標注此參數是逆變類型參數還是協變類型參數。
 


--------------------------------------------------------------------------------


微軟MSDN中的定義。

協變類型參數用 out 關鍵字(在 Visual Basic 中為 Out 關鍵字,在 MSIL 匯編程序中為 +)標記。 可以將協變類型參數用作屬於接口的方法的返回值,或用作委托的返回類型。 但不能將協變類型參數用作接口方法的泛型類型約束。 逆變類型參數用 in 關鍵字(在 Visual Basic 中為 In 關鍵字,在 MSIL 匯編程序中為 -)標記。 可以將逆變類型參數用作屬於接口的方法的參數類型,或用作委托的參數類型。 也可以將逆變類型參數用作接口方法的泛型類型約束。事實上Action和Func的定義在Framework4.0中如下:

public delegate void Action<in T>(
T obj )
public delegate TResult Func<in T, out TResult>(
T arg )
 
從定義可以看出,協變out主要用在返回值上,逆變in用在輸入參數。如果把out由於輸入參數,編譯不會通過。原因前面也已經解釋過了。這裡在說明一下:假設我們把out用於輸入參數,並假設編譯能夠通過,那麼這樣做的目的是使該參數能夠協變。因此假設如下代碼能成功編譯。
        class Father
    { }
 
    class Son : Father
    { }
 
    class Daught : Father
    { }
 
    class Program
    {
        public delegate void myAction<out T>(T t);//該委托和Action<in T>類似,就差了一個in,一個out,假設此段代碼編譯通過。
 
 
        static void f_father(Father f)
        { }
 
        static void f_son(Son f)
        { }
 
        static void Main(string[] args)
        {
            myAction<Father> fatheract = f_father;
            myAction<Son> sonact = f_son;
 
            fatheract = sonact;//假設上面的委托能編譯成功,那麼就是支持協變。因此這段代碼也能成立。
 
            fatheract(new Daught())//這段代碼就無法准確運行了。
 
             //如果這段代碼成立了,
            //那麼fatheract(new Daught())運行的時候,發現
            //f_son裡面接受了Daughter類型,無法運行。
 
        }
    }
同樣的,如果對於輸出類型用in來修飾,即允許其可以逆變,也會出現類似的類型問題
class Father
    { }
 
    class Son : Father
    { }
 
    class Daught : Father
    { }
 
    class Program
    {
        public delegate T myFunc<in T>();//假設這個代碼可以編譯成功。即假設是支持逆變
         static void Main(string[] args)
        {
            myFunc<Father> fatherfunc = ()=> new Father();
            myFunc<Son> sonfunc = () => new Son(); ;
 
            sonfunc = fatherfunc;//假設上面的代碼編譯成功,支持了逆變,即支持了fatherfunc賦值給sonfunc那麼下面的代碼就會引起異常。
              Son ason=  sonfunc();//ason被迫接受father類型了,導致異常
         }
    }所以,in對應逆變,只能用於輸出參數,out對應協變,只能用於輸出參數。否則會出現問題。
因此,在泛型委托中用允許輸入類型協變會引起類型問題,因此只允許逆變。

基於上面的敘說,很多人會有疑問為何還要顯示的標注in或者out,編譯器完全可以推斷泛型參數是協變還是逆變。事實上,編譯器確實可以自動推斷,但C#團隊認為你需要明確的定義了一個契約,並且遵守這個契約。比如,如果編譯器替你決定了某個泛型類型參數是逆變的,但是,你卻在接口上加了個成員,並使用了out標記。這到後來可能會導致一些類型的錯誤,因此,一開始編譯器就要求你顯示的聲明泛型類型參數。如果你不按照你定義的規則那樣,協變或者逆變,編譯器會報錯,提示你違法了你定義的契約。

 作者:cnn237111

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved