在之前的文章中,我大致介绍过一些类型间的隐式和显式类型转换规则。但当时并未很仔细的研究过《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提供的类型转换方法,最后是
新闻热点
疑难解答