在之前的文章中,我大致介紹過一些類型間的隱式和顯式類型轉換規則。但當時並未很仔細的研究過《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
都是非可空的值類型):
- 從
S?
到T?
的顯式類型轉換,其過程為:- 如果輸入值是
null
,那么結果為T?
類型的null
。 - 否則將
S?
解包為S
,然后執行從S
到T
的類型轉換,最后從T
包裝為T?
。
- 如果輸入值是
- 從
S?
到T
的隱式/顯式類型轉換,其過程為:- 若輸入值是
null
,那么引發異常。 - 否則將
S?
解包為S
,然后執行從S
到T
的類型轉換。
- 若輸入值是
- 從
S
到T?
的隱式/顯式類型轉換,先執行從S
到T
的類型轉換,然后從T
包裝為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
類型的用戶自定義顯式類型轉換按下面這樣處理:
- 確定類型
S0
和T0
。如果S
或T
是可空類型,則S0
和T0
就是它們的基礎類型;否則S0
和T0
分別等於S
和T
。得到S0
和T0
是為了在其中查找用戶自定義的隱式/顯式類型轉換運算符。 - 找到類型集合
D
,將從該集合中查找用戶自定義類型轉換運算符。此集合由S0
(如果S0
是類或結構體)、S0
的所有基類(如果S0
是類)、T0
(如果T0
是類或結構體)和T0
的所有基類(如果T0
是類)組成。這里包含S0
和T0
的基類,是因為S
和T
也可以使用基類中聲明的類型轉換運算符。 - 查找適用的用戶自定義類型轉換運算符和提升轉換運算符集合
U
。此集合由在D
中的類或結構內聲明的隱式/顯式用戶自定義類型轉換運算符和提升轉換運算符組成,用於從包含S
或被S
包含的類型(即S
、S
的基類、S
實現的接口或S
的子類)轉換為包含T
或被T
包含的類型。如果U
為空,則產生未定義轉換的錯誤。 - 在
U
中查找運算符的最精確的源類型SX
:- 如果
U
中存在某一運算符從S
轉換,則SX
為S
。 - 否則,如果
U
中存在某一運算符從包含S
的類型轉換,那么SX
是這類運算符的源類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離S
最近的包含S
的類型。 - 否則,
U
中的運算符都是從被S
包含的類型轉換的,那么SX
是U
中運算符的源類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離S
最近的被S
包含的類型。
- 如果
- 在
U
中查找運算符的最精確的目標類型TX
:- 如果
U
中存在某一運算符轉換為T
,則TX
為T
。 - 否則,如果
U
中存在某一運算符轉換到被T
包含的類型,那么TX
是這類運算符的目標類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是上距離T
最近的被T
包含的類型。 - 否則,
U
中的運算符都是轉換到包含T
的類型,那么TX
是U
中運算符的目標類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產生不明確的轉換的錯誤。這里找到的是距離T
最近的包含T
的類型。
- 如果
- 查找最精確的轉換運算符:
- 如果
U
中只包含一個從SX
轉換到TX
的用戶自定義類型轉換運算符,那么這就是最精確的轉換運算符。 - 否則,如果
U
只包含一個從SX
轉換到TX
的提升轉換運算符,則這就是最精確的轉換運算符。 - 否則產生不明確的轉換的錯誤。
- 如果
- 最后,應用轉換:
- 如果
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。