在之前的文章中,我大致介紹過一些類型間的隱式和顯式類型轉換規則。但當時並未很仔細的研究過《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 指令一般比較簡單,基本就是 castclass
、box
和 unbox
指令,復雜一些的就是隱式/顯式數值轉換和可空類型的轉換。
隱式/顯式數值轉換我總結了下面的表格,其實現基本就是查表格的過程。表格的上方是不進行溢出檢查的 IL 指令,下方是進行溢出檢查的 IL 指令,空格表示無需插入 IL 指令即可進行類型轉換;綠色背景表示隱式數值轉換,黃色背景表示顯式數值轉換:
圖 2 隱式/顯式數值轉換
注意數值轉換有溢出檢查的區分(checked/unchecked),而且表格中並未列出 Decimal 類型,因為 Decimal 類型與其它數值類型間的轉換依靠的是使用 implicit/explicit 定義的類型轉換方法,不適合使用查表的方法。
可空類型的轉換,可以分為三種情況(設 S
、T
都是非可空的值類型):
可空類型的轉換,可參見 BetweenNullableConversion.cs、FromNullableConversion.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
類型的用戶自定義顯式類型轉換按下面這樣處理:
該算法可參見 UserConversionCache.cs。
上面所述的兩類方法,都是在編譯時已經完全確定的類型轉換方法。Convert 類額外提供了兩個接口,可以提供任意的類型轉換方法。
AddConverter<TInput, TOutput>(Converter<TInput, TOutput> converter)
方法可以將任意類型轉換方法注冊進來,而AddConverterProvider(IConverterProvider provider)
方法可以注冊類型轉換方法的提供者,可以批量提供與某一類型相關的類型轉換方法(示例可以參見StringConverterProvider.cs,提供了與字符串相關的類型轉換方法)。
注意:優先級最高的是上面的預定義類型轉換方法和用戶自定義類型轉換方法,其次是由 AddConverter
方法注冊的類型轉換方法,然後是IConverterProvider
的 GetConverterTo
提供的類型轉換方法,最後是 IConverterProvider
的 GetConverterFrom
提供的類型轉換方法,且後設置的優先級更高。
本文提到的內容的完整代碼源文件可見 Cyjb.Conversions 和 Cyjb.Reflection。