在之前的文章中,我大致介绍过一些类型间的隐式和显式类型转换规则。但当时并未很仔细的研究过《CSharp Language Specification》,因此实现并不完整。而且只部分解决了类型间能否进行类型转换,仍未解决到底该如何进行类型转换,尤其是在定义泛型类型时,我们明明知道泛型类型的参数是什么类型,但就是不能直接进行类型转换:
class="brush:csharp;gutter:true;">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。