使用 IL 實現類型轉換


在之前的文章中,我大致介紹過一些類型間的隱式和顯式類型轉換規則。但當時並未很仔細的研究過《CSharp Language Specification》,因此實現並不完整。而且只部分解決了類型間能否進行類型轉換,仍未解決到底該如何進行類型轉換,尤其是在定義泛型類型時,我們明明知道泛型類型的參數是什么類型,但就是不能直接進行類型轉換:

if (typeof(T) == typeof(int)) {
    int intValue = (int)value; // 錯誤:無法將類型“T”轉換為“int”
}

只能通過 object 類型“中轉”一下才行:

if (typeof(T) == typeof(int)) {
    int intValue = (int)(object)value;
}

這里是利用了值類型的裝箱/拆箱操作規避了錯誤。但如果想更通用些呢?比如,我知道 char 類型是可以隱式轉換為 int 類型的,那我能不能也這么寫呢:

if (typeof(T) == typeof(int) || typeof(T) == typeof(char)) {
    int intValue = (int)(object)value;
}

可惜,如果 value 是 char 類型,那么在運行時會報異常: System.InvalidCastException: 指定的轉換無效。必須把不同類型分開寫的。這是因為大部分類型轉換的 IL 代碼都是在編譯期就完全確定了的,在運行時只能進行兼容的引用類型轉換(CastClass)和裝箱/拆箱(Box/Unbox)轉換。

為了增強和簡化運行時的類型轉換,我仔細研究了一下《CSharp Language Specification》和 IL,利用 System.Reflection.Emit 實現了一套在運行時動態生成 IL 進行類型轉換的框架,能夠在運行時實現與編譯器基本相同的類型轉換支持,並對泛型類型提供了完整的支持,例如下面的將任意數字類型轉換為ulong

// 假設這里的 TValue 保證是數字類型。
public ulong ToUInt64<TValue>(TValue value) {
    return Convert.ChangeType<TValue, ulong>(value);
}

類型轉換的主要接口是 Convert 類,可以完整兼容各種數值類型轉換、隱式/顯式引用類型轉換和用戶自定義類型轉換,主要包含的功能有:

  • 獲取類型轉換器:GetConverter<TInput, TOutput>() 和 GetConverter(Type inputType, Type outputType),得到的 Converter<TInput, TOutput> 委托可以直接用於類型轉換。
  • 直接進行類型轉換:ChangeType<TInput, TOutput>(TInput value)ChangeType<TOutput>(object value) 和ChangeType(object value, Type outputType)
  • 判斷能否進行類型轉換:CanChangeType(Type inputType, Type outputType)
  • 運行時添加類型轉換方法:AddConverter<TInput, TOutput>(Converter<TInput, TOutput> converter) 和AddConverterProvider(IConverterProvider provider)

所有的類型轉換,都是利用 System.Reflection.Emit 動態生成 IL 實現的,保證了類型轉換的效率。因此,也得以同時提供了 ILGenerator 類的擴展方法EmitConversion,可以在生成 IL 代碼時也能夠進行類型轉換。

以上的所有代碼,都可以在 Cyjb.Conversions 和 Cyjb.Reflection 命名空間中找到。

接下來,我會簡要介紹一下是如何使用 IL 實現類型轉換的。

一、預定義的類型轉換

根據《CSharp Language Specification》,預定義的類型轉換主要包括:標識轉換、隱式數值轉換、隱式枚舉轉換、可空類型(Nullable<T>)的隱式轉換、隱式引用轉換、裝箱轉換、顯式數值轉換、顯式枚舉轉換、可空類型的顯式轉換、顯式引用轉換和拆箱轉換這 11 類。由 implicit 和 explicit 關鍵字聲明的用戶自定義類型轉換會在下一節介紹。

規范中都給出了這些類型轉換的處理流程,但如果簡單的按順序判斷這些類型轉換,其效率是非常低的。因此我使用下圖所示的算法來進行判斷:

圖 1 預定義類型轉換判斷算法

預定義類型轉換用到的 IL 指令一般比較簡單,基本就是 castclassbox 和 unbox 指令,復雜一些的就是隱式/顯式數值轉換和可空類型的轉換。

隱式/顯式數值轉換我總結了下面的表格,其實現基本就是查表格的過程。表格的上方是不進行溢出檢查的 IL 指令,下方是進行溢出檢查的 IL 指令,空格表示無需插入 IL 指令即可進行類型轉換;綠色背景表示隱式數值轉換,黃色背景表示顯式數值轉換:

圖 2 隱式/顯式數值轉換

注意數值轉換有溢出檢查的區分(checked/unchecked),而且表格中並未列出 Decimal 類型,因為 Decimal 類型與其它數值類型間的轉換依靠的是使用 implicit/explicit 定義的類型轉換方法,不適合使用查表的方法。

可空類型的轉換,可以分為三種情況(設 ST 都是非可空的值類型):

  1. 從 S? 到 T? 的顯式類型轉換,其過程為:
    • 如果輸入值是 null,那么結果為 T? 類型的 null
    • 否則將 S? 解包為 S,然后執行從 S 到 T 的類型轉換,最后從 T 包裝為 T?
  2. 從 S? 到 T 的隱式/顯式類型轉換,其過程為:
    • 若輸入值是 null,那么引發異常。
    • 否則將 S? 解包為 S,然后執行從 S 到 T 的類型轉換。
  3. 從 S 到 T? 的隱式/顯式類型轉換,先執行從 S 到 T 的類型轉換,然后從 T 包裝為T?

可空類型的轉換,可參見 BetweenNullableConversion.csFromNullableConversion.cs 和 ToNullableConversion.cs

二、用戶自定義類型轉換

這里指的就是由 implicit 和 explicit 關鍵字聲明的用戶自定義類型轉換方法。下面介紹的算法來自《CSharp Language Specification》6.4.5 User-defined explicit conversions,我並不會區分是隱式類型轉換還是顯式類型轉換,因為在運行時這樣的區分並不重要。

首先需要明確一些概念。

提升轉換運算符:如果存在從不可空值類型 S 到不可空值類型 T 的用戶自定義類型轉換運算符,那么存在從 S? 轉換為 T? 的提升轉換運算符。這個提升轉換運算符執行從 S? 到 S 的解包,接着是從 S 到 T 的用戶自定義類型轉換,然后是從 T 到 T? 的包裝;若是 S? 的值為 null,那么直接轉換為值為 null 的T? 。

包含/被包含:若 A 類型可以隱式類型轉換(指預定義的類型轉換)為 B 類型,而且 A 和 B 都不是接口,那么就稱 A 被 B 包含,而 B 包含 A

包含程度最大:在給定類型集合中,包含程度最大的類型可以包含集合中的所有其它類型。如果沒有某個類型可以包含集合中的所有其它類型,那么就不存在包含程度最大的類型。更直觀的說,包含程度最大的類型就是集合中最“廣泛”的類型——其它類型都可以隱式轉換為它。

被包含程度最大:在給定類型集合中,被包含程度最大的類型可以被集合中的所有其它類型包含。如果沒有某個類型可以被集合中的所有其它類型包含,那么就不存在被包含程度最大的類型。更直觀的說,被包含程度最大的類型就是集合中最“精確”的類型——它可以隱式轉換為其它類型。

從 S 類型到 T 類型的用戶自定義顯式類型轉換按下面這樣處理:

  1. 確定類型 S0 和 T0。如果 S 或 T 是可空類型,則 S0 和 T0 就是它們的基礎類型;否則 S0 和 T0 分別等於 S 和 T。得到 S0 和 T0 是為了在其中查找用戶自定義的隱式/顯式類型轉換運算符。
  2. 找到類型集合 D,將從該集合中查找用戶自定義類型轉換運算符。此集合由 S0(如果 S0 是類或結構體)、S0 的所有基類(如果 S0 是類)、T0(如果 T0 是類或結構體)和 T0 的所有基類(如果 T0 是類)組成。這里包含 S0 和 T0 的基類,是因為 S 和 T 也可以使用基類中聲明的類型轉換運算符。
  3. 查找適用的用戶自定義類型轉換運算符和提升轉換運算符集合 U。此集合由在 D 中的類或結構內聲明的隱式/顯式用戶自定義類型轉換運算符和提升轉換運算符組成,用於從包含 S 或被 S 包含的類型(即 SS 的基類、S 實現的接口或 S 的子類)轉換為包含 T 或被 T 包含的類型。如果 U 為空,則產生未定義轉換的錯誤。
  4. 在 U 中查找運算符的最精確的源類型 SX
    • 如果 U 中存在某一運算符從 S 轉換,則 SX 為 S
    • 否則,如果 U 中存在某一運算符從包含 S 的類型轉換,那么 SX 是這類運算符的源類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離 S 最近的包含 S 的類型。
    • 否則,U 中的運算符都是從被 S 包含的類型轉換的,那么 SX 是 U 中運算符的源類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離 S 最近的被 S 包含的類型。
  5. 在 U 中查找運算符的最精確的目標類型 TX
    • 如果 U 中存在某一運算符轉換為 T,則 TX 為 T
    • 否則,如果 U 中存在某一運算符轉換到被 T 包含的類型,那么 TX 是這類運算符的目標類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是上距離 T 最近的被 T 包含的類型。
    • 否則,U 中的運算符都是轉換到包含 T 的類型,那么 TX 是 U 中運算符的目標類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離 T 最近的包含 T 的類型。
  6. 查找最精確的轉換運算符:
    • 如果 U 中只包含一個從 SX 轉換到 TX 的用戶自定義類型轉換運算符,那么這就是最精確的轉換運算符。
    • 否則,如果 U 只包含一個從 SX 轉換到 TX 的提升轉換運算符,則這就是最精確的轉換運算符。
    • 否則產生不明確的轉換的錯誤。
  7. 最后,應用轉換:
    • 如果 S 不是 SX,則執行從 S 到 SX 的標准顯式轉換。
    • 調用最精確轉換運算符,以從 SX 轉換到 TX
    • 如果 TX 不是 T,則執行從 TX 到 T 的標准顯式轉換。

該算法可參見 UserConversionCache.cs

三、額外的用戶自定義類型轉換

上面所述的兩類方法,都是在編譯時已經完全確定的類型轉換方法。Convert 類額外提供了兩個接口,可以提供任意的類型轉換方法。

AddConverter<TInput, TOutput>(Converter<TInput, TOutput> converter) 方法可以將任意類型轉換方法注冊進來,而AddConverterProvider(IConverterProvider provider) 方法可以注冊類型轉換方法的提供者,可以批量提供與某一類型相關的類型轉換方法(示例可以參見StringConverterProvider.cs,提供了與字符串相關的類型轉換方法)。

注意:優先級最高的是上面的預定義類型轉換方法和用戶自定義類型轉換方法,其次是由 AddConverter 方法注冊的類型轉換方法,然后是IConverterProvider 的 GetConverterTo 提供的類型轉換方法,最后是 IConverterProvider 的 GetConverterFrom 提供的類型轉換方法,且后設置的優先級更高。

本文提到的內容的完整代碼源文件可見 Cyjb.Conversions 和 Cyjb.Reflection


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM