在之前的文章中,我大致介紹過一些類型間的隱式和顯式類型轉(zhuǎn)換規(guī)則。但當(dāng)時并未很仔細(xì)的研究過《CSharp Language Specification》,因此實(shí)現(xiàn)并不完整。而且只部分解決了類型間能否進(jìn)行類型轉(zhuǎn)換,仍未解決到底該如何進(jìn)行類型轉(zhuǎn)換,尤其是在定義泛型類型時,我們明明知道泛型類型的參數(shù)是什么類型,但就是不能直接進(jìn)行類型轉(zhuǎn)換:
if (typeof(T) == typeof(int)) { int intValue = (int)value; // 錯誤:無法將類型“T”轉(zhuǎn)換為“int”}只能通過object類型“中轉(zhuǎn)”一下才行:
if (typeof(T) == typeof(int)) { int intValue = (int)(object)value;}這里是利用了值類型的裝箱/拆箱操作規(guī)避了錯誤。但如果想更通用些呢?比如,我知道char類型是可以隱式轉(zhuǎn)換為int類型的,那我能不能也這么寫呢:
if (typeof(T) == typeof(int) || typeof(T) == typeof(char)) { int intValue = (int)(object)value;}可惜,如果value是char類型,那么在運(yùn)行時會報(bào)異常: System.InvalidCastException: 指定的轉(zhuǎn)換無效。必須把不同類型分開寫的。這是因?yàn)榇蟛糠诸愋娃D(zhuǎn)換的 IL 代碼都是在編譯期就完全確定了的,在運(yùn)行時只能進(jìn)行兼容的引用類型轉(zhuǎn)換(CastClass)和裝箱/拆箱(Box/Unbox)轉(zhuǎn)換。
為了增強(qiáng)和簡化運(yùn)行時的類型轉(zhuǎn)換,我仔細(xì)研究了一下《CSharp Language Specification》和 IL,利用System.Reflection.Emit實(shí)現(xiàn)了一套在運(yùn)行時動態(tài)生成 IL 進(jìn)行類型轉(zhuǎn)換的框架,能夠在運(yùn)行時實(shí)現(xiàn)與編譯器基本相同的類型轉(zhuǎn)換支持,并對泛型類型提供了完整的支持,例如下面的將任意數(shù)字類型轉(zhuǎn)換為ulong:
// 假設(shè)這里的 TValue 保證是數(shù)字類型。public ulong ToUInt64<TValue>(TValue value) { return Convert.ChangeType<TValue, ulong>(value);}類型轉(zhuǎn)換的主要接口是Convert 類,可以完整兼容各種數(shù)值類型轉(zhuǎn)換、隱式/顯式引用類型轉(zhuǎn)換和用戶自定義類型轉(zhuǎn)換,主要包含的功能有:
GetConverter<TInput, TOutput>()和GetConverter(Type inputType, Type outputType),得到的Converter<TInput, TOutput>委托可以直接用于類型轉(zhuǎn)換。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)。所有的類型轉(zhuǎn)換,都是利用System.Reflection.Emit動態(tài)生成 IL 實(shí)現(xiàn)的,保證了類型轉(zhuǎn)換的效率。因此,也得以同時提供了ILGenerator 類的擴(kuò)展方法EmitConversion,可以在生成 IL 代碼時也能夠進(jìn)行類型轉(zhuǎn)換。
以上的所有代碼,都可以在Cyjb.Conversions和Cyjb.Reflection命名空間中找到。
接下來,我會簡要介紹一下是如何使用 IL 實(shí)現(xiàn)類型轉(zhuǎn)換的。
根據(jù)《CSharp Language Specification》,預(yù)定義的類型轉(zhuǎn)換主要包括:標(biāo)識轉(zhuǎn)換、隱式數(shù)值轉(zhuǎn)換、隱式枚舉轉(zhuǎn)換、可空類型(Nullable<T>)的隱式轉(zhuǎn)換、隱式引用轉(zhuǎn)換、裝箱轉(zhuǎn)換、顯式數(shù)值轉(zhuǎn)換、顯式枚舉轉(zhuǎn)換、可空類型的顯式轉(zhuǎn)換、顯式引用轉(zhuǎn)換和拆箱轉(zhuǎn)換這 11 類。由implicit和explicit關(guān)鍵字聲明的用戶自定義類型轉(zhuǎn)換會在下一節(jié)介紹。
規(guī)范中都給出了這些類型轉(zhuǎn)換的處理流程,但如果簡單的按順序判斷這些類型轉(zhuǎn)換,其效率是非常低的。因此我使用下圖所示的算法來進(jìn)行判斷:

圖 1 預(yù)定義類型轉(zhuǎn)換判斷算法
預(yù)定義類型轉(zhuǎn)換用到的 IL 指令一般比較簡單,基本就是castclass、box和unbox指令,復(fù)雜一些的就是隱式/顯式數(shù)值轉(zhuǎn)換和可空類型的轉(zhuǎn)換。
隱式/顯式數(shù)值轉(zhuǎn)換我總結(jié)了下面的表格,其實(shí)現(xiàn)基本就是查表格的過程。表格的上方是不進(jìn)行溢出檢查的 IL 指令,下方是進(jìn)行溢出檢查的 IL 指令,空格表示無需插入 IL 指令即可進(jìn)行類型轉(zhuǎn)換;綠色背景表示隱式數(shù)值轉(zhuǎn)換,黃色背景表示顯式數(shù)值轉(zhuǎn)換:

圖 2 隱式/顯式數(shù)值轉(zhuǎn)換
注意數(shù)值轉(zhuǎn)換有溢出檢查的區(qū)分(checked/unchecked),而且表格中并未列出Decimal類型,因?yàn)镈ecimal類型與其它數(shù)值類型間的轉(zhuǎn)換依靠的是使用 implicit/explicit 定義的類型轉(zhuǎn)換方法,不適合使用查表的方法。
可空類型的轉(zhuǎn)換,可以分為三種情況(設(shè)S、T都是非可空的值類型):
S?到T?的顯式類型轉(zhuǎn)換,其過程為:null,那么結(jié)果為T?類型的null。S?解包為S,然后執(zhí)行從S到T的類型轉(zhuǎn)換,最后從T包裝為T?。S?到T的隱式/顯式類型轉(zhuǎn)換,其過程為:null,那么引發(fā)異常。S?解包為S,然后執(zhí)行從S到T的類型轉(zhuǎn)換。S到T?的隱式/顯式類型轉(zhuǎn)換,先執(zhí)行從S到T的類型轉(zhuǎn)換,然后從T包裝為T?。可空類型的轉(zhuǎn)換,可參見BetweenNullableConversion.cs、FromNullableConversion.cs和ToNullableConversion.cs。
這里指的就是由implicit和explicit關(guān)鍵字聲明的用戶自定義類型轉(zhuǎn)換方法。下面介紹的算法來自《CSharp Language Specification》6.4.5 User-defined explicit conversions,我并不會區(qū)分是隱式類型轉(zhuǎn)換還是顯式類型轉(zhuǎn)換,因?yàn)樵谶\(yùn)行時這樣的區(qū)分并不重要。
首先需要明確一些概念。
提升轉(zhuǎn)換運(yùn)算符:如果存在從不可空值類型S到不可空值類型T的用戶自定義類型轉(zhuǎn)換運(yùn)算符,那么存在從S?轉(zhuǎn)換為T?的提升轉(zhuǎn)換運(yùn)算符。這個提升轉(zhuǎn)換運(yùn)算符執(zhí)行從S?到S的解包,接著是從S到T的用戶自定義類型轉(zhuǎn)換,然后是從T到T?的包裝;若是S?的值為null,那么直接轉(zhuǎn)換為值為null的T?。
包含/被包含:若A類型可以隱式類型轉(zhuǎn)換(指預(yù)定義的類型轉(zhuǎn)換)為B類型,而且A和B都不是接口,那么就稱A被B包含,而B包含A。
包含程度最大:在給定類型集合中,包含程度最大的類型可以包含集合中的所有其它類型。如果沒有某個類型可以包含集合中的所有其它類型,那么就不存在包含程度最大的類型。更直觀的說,包含程度最大的類型就是集合中最“廣泛”的類型——其它類型都可以隱式轉(zhuǎn)換為它。
被包含程度最大:在給定類型集合中,被包含程度最大的類型可以被集合中的所有其它類型包含。如果沒有某個類型可以被集合中的所有其它類型包含,那么就不存在被包含程度最大的類型。更直觀的說,被包含程度最大的類型就是集合中最“精確”的類型——它可以隱式轉(zhuǎn)換為其它類型。
從S類型到T類型的用戶自定義顯式類型轉(zhuǎn)換按下面這樣處理:
S0和T0。如果S或T是可空類型,則S0和T0就是它們的基礎(chǔ)類型;否則S0和T0分別等于S和T。得到S0和T0是為了在其中查找用戶自定義的隱式/顯式類型轉(zhuǎn)換運(yùn)算符。D,將從該集合中查找用戶自定義類型轉(zhuǎn)換運(yùn)算符。此集合由S0(如果S0是類或結(jié)構(gòu)體)、S0的所有基類(如果S0是類)、T0(如果T0是類或結(jié)構(gòu)體)和T0的所有基類(如果T0是類)組成。這里包含S0和T0的基類,是因?yàn)?code>S和T也可以使用基類中聲明的類型轉(zhuǎn)換運(yùn)算符。U。此集合由在D中的類或結(jié)構(gòu)內(nèi)聲明的隱式/顯式用戶自定義類型轉(zhuǎn)換運(yùn)算符和提升轉(zhuǎn)換運(yùn)算符組成,用于從包含S或被S包含的類型(即S、S的基類、S實(shí)現(xiàn)的接口或S的子類)轉(zhuǎn)換為包含T或被T包含的類型。如果U為空,則產(chǎn)生未定義轉(zhuǎn)換的錯誤。U中查找運(yùn)算符的最精確的源類型SX:U中存在某一運(yùn)算符從S轉(zhuǎn)換,則SX為S。U中存在某一運(yùn)算符從包含S的類型轉(zhuǎn)換,那么SX是這類運(yùn)算符的源類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產(chǎn)生不明確的轉(zhuǎn)換的錯誤。這里找到的是距離S最近的包含S的類型。U中的運(yùn)算符都是從被S包含的類型轉(zhuǎn)換的,那么SX是U中運(yùn)算符的源類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產(chǎn)生不明確的轉(zhuǎn)換的錯誤。這里找到的是距離S最近的被S包含的類型。U中查找運(yùn)算符的最精確的目標(biāo)類型TX:U中存在某一運(yùn)算符轉(zhuǎn)換為T,則TX為T。U中存在某一運(yùn)算符轉(zhuǎn)換到被T包含的類型,那么TX是這類運(yùn)算符的目標(biāo)類型中包含程度最大的類型。如果無法恰好找到一個包含程度最大的類型,則產(chǎn)生不明確的轉(zhuǎn)換的錯誤。這里找到的是上距離T最近的被T包含的類型。U中的運(yùn)算符都是轉(zhuǎn)換到包含T的類型,那么TX是U中運(yùn)算符的目標(biāo)類型中被包含程度最大的類型。如果無法恰好找到一個被包含程度最大的類型,則產(chǎn)生不明確的轉(zhuǎn)換的錯誤。這里找到的是距離T最近的包含T的類型。U中只包含一個從SX轉(zhuǎn)換到TX的用戶自定義類型轉(zhuǎn)換運(yùn)算符,那么這就是最精確的轉(zhuǎn)換運(yùn)算符。U只包含一個從SX轉(zhuǎn)換到TX的提升轉(zhuǎn)換運(yùn)算符,則這就是最精確的轉(zhuǎn)換運(yùn)算符。S不是SX,則執(zhí)行從S到SX的標(biāo)準(zhǔn)顯式轉(zhuǎn)換。SX轉(zhuǎn)換到TX。TX不是T,則執(zhí)行從TX到T的標(biāo)準(zhǔn)顯式轉(zhuǎn)換。該算法可參見UserConversionCache.cs。
上面所述的兩類方法,都是在編譯時已經(jīng)完全確定的類型轉(zhuǎn)換方法。Convert 類額外提供了兩個接口,可以提供任意的類型轉(zhuǎn)換方法。
AddConverter<TInput, TOutput>(Converter<TInput, TOutput> converter)方法可以將任意類型轉(zhuǎn)換方法注冊進(jìn)來,而AddConverterProvider(IConverterProvider provider)方法可以注冊類型轉(zhuǎn)換方法的提供者,可以批量提供與某一類型相關(guān)的類型轉(zhuǎn)換方法(示例可以參見StringConverterProvider.cs,提供了與字符串相關(guān)的類型轉(zhuǎn)換方法)。
注意:優(yōu)先級最高的是上面的預(yù)定義類型轉(zhuǎn)換方法和用戶自定義類型轉(zhuǎn)換方法,其次是由AddConverter方法注冊的類型轉(zhuǎn)換方法,然后是IConverterProvider的GetConverterTo提供的類型轉(zhuǎn)換方法,最后是
新聞熱點(diǎn)
疑難解答
圖片精選