【轉】那些年搞不懂的術語、概念:協變、逆變、不變體

【轉】那些年搞不懂的術語、概念:協變、逆變、不變體html

簡述什麼是協變性、逆變性、不變性

  • 協變性,如:string->object (子類到父類的轉換)
  • 逆變性,如:object->string (父類到子類的轉換)
  • 不變性,基於上面兩種狀況,不可變。具體下面再作分析。

泛型委託的可變性

先使用框架定義的泛型委託Func和Action作例子(不瞭解的請戳安全

協變:(string->object)框架

Func<string> func1 = () => "農碼一輩子"; Func<object> func2 = func1;

逆變:(object->string)測試

Action<object> func3 = t => { }; Action<string> func4 = func3;

上面代碼沒有任何問題。url

接着咱們本身定義委託試試:spa

我X,看人不來哦。爲何自定義的委託卻不能協變呢。3d

我看看系統定義的Func到底和咱們自定義的有什麼不一樣:code

public delegate TResult Func<out TResult>();

多了一個out,什麼鬼:htm

  • out:對於泛型類型參數,out 關鍵字指定該類型參數是協變的。 能夠在泛型接口和委託中使用 out 關鍵字。來源
  • in:對於泛型類型參數,in 關鍵字指定該類型參數是逆變的。 能夠在泛型接口和委託中使用 in 關鍵字。來源

那麼咱們能夠修改自定義委託:對象

完美!

那若是咱們要實現逆變性呢:

直接逆變是不可行的,咱們須要修改泛型類型參數:

咱們發現整個委託參數都變了。原本的返回值,改爲輸入參數才行。

結論:

  • in->輸入參數->可逆變(父類到子類的轉變[如 object->string])
  • out->返回值->可協變(子類到父類的轉變[如 string->object])

 

假設:若是泛型參數中既存在in又存在out改如何:

delegate Tout MyFunc<in Tin, out Tout>(Tin obj);
MyFunc<object, string> str1 = t => "農碼一輩子"; MyFunc<string, string> str2 = str1;//第一個泛型的逆變(object->string)
MyFunc<object, object> str3 = str1;//第二個泛型的協變(string->object)
MyFunc<string, object> str4 = str1;//第一個泛型的逆變和第二個泛型的協變

以上都是沒有問題的。 

而後咱們看看編譯後的C#代碼:

結論:

  • 所謂的逆變其實只是編譯後進行了強制類型轉換而已。

以上代碼也能夠直接寫成:

//delegate Tout MyFunc<in Tin, out Tout>(Tin obj);
MyFunc<string, string> str5 = t => "農碼一輩子"; MyFunc<object, object> str6 = t => "農碼一輩子"; MyFunc<string, object> str7 = t => "農碼一輩子";

泛型接口的可變性

接着看框架默認接口:

協變:(子類->父類)

IEnumerable<string> list = new List<string>(); IEnumerable<object> list2 = list;

逆變:(父類-> 子類)

IComparable<object> list3 = null; IComparable<string> list4 = list3;

接下來咱們試試自定泛型接口:

首先定義測試類型、接口:

//
public class People { } //老師(繼承People[人])
public class Teacher : People { } //運動
public interface IMotion<T> { } //跑步
public class Run<T> : IMotion<T> { }

而後咱們測試協變性:

一樣咱們須要把接口 interface IMotion<T> 定義爲 interface IMotion<out T> 

//運動
public interface IMotion<out T>{}
IMotion<Teacher> x = new Run<Teacher>(); IMotion<People> y = x;

若是咱們要測試逆變性,則須要把 interface IMotion<T>  定義爲 interface IMotion<in T> 

//運動
public interface IMotion<in T>{}
IMotion<People> x2 = new Run<People>(); IMotion<Teacher> y2 = x2;

泛型接口的逆變,編譯後一樣進行了強制轉換:

固然,咱們也能夠直接寫成:

IMotion<Teacher> y3 = new Run<People>();

不變性

從上面咱們知道逆變性的代碼編譯後都會進行強制轉換。假設:那咱們不用out、in直接手動強制轉換是否能夠?:

//
public class People { } //老師(繼承People[人])
public class Teacher : People { } //運動
public interface IMotion<T> { } //跑步
public class Run<T> : IMotion<T> { }
//協變
IMotion<Teacher> x = new Run<Teacher>(); IMotion<People> y = (IMotion<People>)x; //逆變
IMotion<People> x2 = new Run<People>(); IMotion<Teacher> y2 = (IMotion<Teacher>)x2; IMotion<Teacher> y3 = (IMotion<Teacher>)new Run<People>();

天才的我發現編譯成功了,沒有任何問題!且還能夠同時協變、逆變??不對,真的天才了嗎?咱們運行試試:

看來我仍是太單純了,若是真的這麼容易繞過去,Microsoft又何須去搞個out、in關鍵字。

對於同一個泛型參數,咱們既想有協變性又想逆變性,咋辦?答案是不可行。這就會出現第三種狀況,既不能夠協變又不能夠逆變。稱爲不變性。

(咱們在IMotion定義兩個方法)

//運動
public interface IMotion<T> { T Show(); void Match(T t); }

上面咱們測試過,代碼直接強制轉換是不能實現協變、逆變的。那麼咱們只能經過out、in來實現。若是如今咱們在泛型參數添加out或in屬性會如何?:

咱們發現out和in都不能用。在用out時,有個傳入參數爲泛型 void Match(T t) 的方法。使用in時,有個返回參數爲泛型 T Show() 的方法。如今就出現了是矛更鋒利仍是盾更堅硬的問題了。

最後結果是:都不能用,既不能協變,也不能逆變。此爲不變體

小知識:

C#4.0以前 IEnumerable<T> 、 IComparable<T> 、 IQueryable<T> 等接口都不支持可變性,在4.0及以後才支持。由於4.0以前定義的泛型接口沒有添加out、in關鍵字,有興趣能夠切換版本看看。

延伸思考

爲何in[輸入參數]就只能逆變?分析以下:

//
public class People { } //老師(繼承People[人])
public class Teacher : People { //薪水
    public decimal Salary { get; set; } } //運動
public interface IMotion<in T> { void Match(T t); } //跑步
public class Run<T> : IMotion<T> { public void Match(T t) { //假設中間有不少邏輯..... 
 } }

爲何out[返回值]只能協變?分析以下:

//
public class People { } //老師(繼承People[人])
public class Teacher : People { //薪水
    public decimal Salary { get; set; } } //運動
public interface IMotion<out T> { T Show(); //void Match(T t);
} //跑步
public class Run<T> : IMotion<T> { public T Show() { return default(T); } //public void Match(T t) //{ //    //假設中間有不少邏輯..... //}
}

這裏有兩個關鍵點:

  • 傳入參數(in)是把參數當成父類來用,顯然能夠逆變(子類當成父類來用[里氏替換原則]),可是卻不能夠把父類當子類來用(如:子類存在有而父類沒有的方法或屬性)
  • 返回值(out)返回值類型用父類來接收,顯然能夠協變(父類能夠接收一切子類),但卻不可用子類接收父類數據(如:父類表明的對象不能強制轉給子類[string str = (string)objcet])

。。。是否是有點越想越頭暈,想不明白就慢慢想。本身動動手。

若是實在想的頭大,就把它當成是烏龜的屁股(龜腚\規定)吧,知道是C#作的一種安全限制!

總結

關於泛型接口、泛型委託的可變性:

  • 協變 -> 比較和諧正常的變化 -> 子類轉父類 [如 string轉object] -> 必須有out標識 [返回值]
  • 逆變 -> 逆天的變化 -> 父類轉子類 [如object轉string] -> 必須有in標識 [傳入參數]  (父親變兒子,越活越年輕,還不夠逆天嗎?)
  • 所謂的逆變,會在編譯後的C#代碼中進行強制類型轉換。
  • 示例:
    • IEnumerable<string> list = new List<string>();  
      IEnumerable<object> list2 = list; //協變
      IEnumerable<object> list2 = new List<string>();  //(也能夠直接寫成這樣)

    • IComparable<object> list3 = null;
      IComparable<string> list4 = list3; //逆變  編譯後 [ IComparable<string> list4 = (IComparable<string>) list3;]

注意:

  • 不支持類的類型參數的可變性
  • 只有泛型接口和泛型委託能夠擁有可變的類型參數(out、in)
  • 可變性只支持引用轉換。(不能用於值類型)
  • 類型參數使用了 out 或者 ref 將禁止可變性

 

好了,今天就到這裏。沒啥高深的技術知識,主要爲理解協變、逆變、不變體等術語和概念。

本文已同步至索引目錄:《C#基礎知識鞏固

 

同類文章推薦:

http://www.cnblogs.com/haoyifei/p/5760959.html

http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

相關文章
相關標籤/搜索