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

来源:http://www.cnblogs.com/zhaopei/archive/2016/08/29/variability.html
-Advertisement-
Play Games

簡述什麼是協變性、逆變性、不變性 協變性,如:string->object (子類到父類的轉換) 逆變性,如:object->string (父類到子類的轉換) 不變性,基於上面兩種情況,不可變。具體下麵再做分析。 泛型委托的可變性 先使用框架定義的泛型委托Func和Action做例子(不瞭解的請戳 ...


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

  • 協變性,如: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;

上面代碼沒有任何問題。

接著我們自己定義委托試試:

我X,看人不來哦。為什麼自定義的委托卻不能協變呢。

我看看系統定義的Func到底和我們自定義的有什麼不同:

public delegate TResult Func<out TResult>();

多了一個out,什麼鬼:

  • 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

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...