C# 判断类型间能否隐式或强制类型转换,以及开放泛型类型转换 update
C# 判断类型间能否隐式或强制类型转换,以及开放泛型类型转换 update 2015.02.03
如果要判断某个实例是否与其它类型兼容,
C# 已经提供了两个运算符 is 和 as,
Type 类也提供了 IsAssignableFrom 方法来对两个类型进行判断。
但他们实际上判断的是类型是否在继承层次结构中,而不是类型间是否可以进行转换。
例如下面的代码:
1 2 3 4 |
|
它们的返回值都是 False,但实际上 int 类型可以隐式转换为 long 类型的,long 类型也可以强制转换为 int 类型。
如果希望知道一个类型能否隐式或强制转换为其它类型,这些已有的方法就力不能及了,只能自己来进行判断。
2015.02.03 更新:我利用 IL 实现了一套完整的运行时类型转换框架,因此这里前两节所述的实现方式已被废弃,新的实现效率更高,功能更完善。更多信息请参考《使用 IL 实现类型转换》,代码实现可见 Cyjb.Conversions 和 Cyjb.Reflection。
一、隐式类型转换
首先对隐式类型转换进行判断,隐式类型转换时,需要保证不会报错,数据不会有丢失。我目前总结出下面五点:
- 如果要转换到 object 类型,那么总是可以进行隐式类型转换,因为 object 类型是所有类型的基类。
- 兼容的类型总是可以进行隐式类型转换,例如子类转换为父类。这个是显然的,而且可以直接通过 Type.IsAssignableFrom 来判断。
- .NET 的一些内置类型间总是可以进行隐式类型转换,例如 int 可以隐式转换为 long,这个稍后会总结为一个表格。
- 存在隐式类型转换运算符的类型,这个是属于用户自定义的隐式类型转换方法,可以通过 Type.GetMethods 查找名为 op_Implicit 的方法来找到所有的自定义隐式类型转换。
- 除了上面的之外,.NET 里还有一个略微特殊的类型:Nullable<T>,T 类型总是可以隐式转换为 Nullable<T>,而且 Nullable<T1> 和 Nullable<T2> 类型之间的转换,需要看 T1 和 T2 能否进行隐式转换。因此这里需要特殊处理下,才能够正确的进行判断。
.Net 内置类型间的隐式转换,我总结成了下面的表格(未包含自身的转换):
要转换到的类型 | 来源类型 |
Int16 | SByte, Byte |
UInt16 | Char, Byte |
Int32 | Char, SByte, Byte, Int16, UInt16 |
UInt32 | Char, Byte, UInt16 |
Int64 | Char, SByte, Byte, Int16, UInt16, Int32, UInt32 |
UInt64 | Char, Byte, UInt16, UInt32 |
Single | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 |
Double | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single |
Decimal | Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 |
对于类型的自定义隐式类型转换,则有些需要特别注意的地方。
自定义隐式类型转换可以有两个方向:转换自和转换到,区别就是参数和返回值不同。例如,下面的方法表示 Test 类型可以隐式转换自 int 类型:
1 |
|
而下面的方法则表示 Test 类型可以隐式转换到 int 类型:
1 |
|
因此,在判断类型间是否可以隐式类型转换时,需要考虑这两种不同的情况,而且同时还可能存在兼容类型转换或内置类型转换。但是,却不可以进行两次隐式类型转换。
也就是说,假设 Test 类型可以隐式转换为 int 类型或 Test3 类型,Test3 类继承自 Test2 类,Test3 类可以隐式转换为 Test4 类,图 1 的绿色隐式类型转换是合法的,红色是非法的:
图 1 合法和非法的隐式类型转换
这里的判断算法如图 2 所示,设要判断 fromType 能否隐式转换为 type 类型,取集合 S1 为 type 的隐式转换自方法集合,S2 为 fromType 的隐式转换到方法集合,那么分别判断 S1 中是否存在与 fromType 类型兼容或者可以进行内置类型转换的类型,和 S2 中是否存在与 type 类型兼容或者可以进行内置类型转换的类型。如果找到了这样的类型,就表示可以成功进行隐式类型转换。
图 2 隐式类型转换判断算法
判断算法的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
2013.3.17 更新:后来我又研究了《CSharp Language Specification》v5.0 中与隐式类型转换相关的部分,发现上面的图 2 并不完全准确,根据 6.1.11 用户定义的隐式转换 这一节的说明,用户定义的隐式转换由以下三部分组成:先是一个标准的隐式转换(可选);然后是执行用户定义的隐式转换运算符;最后是另一个标准的隐式转换(可选)。
图 3 规范中的隐式类型转换判断算法
所以,用户定义的隐式转换比我之前考虑的要更复杂些(自己总结的难免会有疏漏,看来还是要根据规范才行),而“标准隐式转换”则包括下面这些转换:
- 标识转换(参见第 6.1.1 节,简单的说就是相同类型间可以进行隐式转换。)
- 隐式数值转换(参见第 6.1.2 节,就是我上面总结的 .Net 内置类型间的隐式转换。)
- 可以为 null 的隐式转换(参见第 6.1.4 节,就是我上面总结的第 5 点相同。)
- 隐式引用转换(参见第 6.1.6 节,基本上就是 Type.IsAssignableFrom 的功能。)
- 装箱转换(参见第 6.1.7 节,同样包含在 Type.IsAssignableFrom 的功能中,就是特别针对值类型进行了说明。)
- 隐式常量表达式转换(参见第 6.1.8 节,针对编译时可以确定值得常量表达式,运行时无需考虑。)
- 涉及类型形参的隐式转换(参见第 6.1.10 节,针对类型参数 T 的转换,同样无需考虑。)
下面给出在 6.4.4 用户定义的隐式转换 节定义的算法,这个算法中文版的翻译有问题,我根据英文版重新修改了一些地方,并加入了自己的说明:
从 S 类型到 T 类型的用户定义的隐式转换按下面这样处理:
- 确定类型 S0 和 T0。如果 S 或 T 是可以为 null 的类型,则 S0 和 T0 为它们的基础类型;否则 S0 和 T0 分别等于 S 和 T。这里这样做仅仅是为了在下一步查找用户自定义的隐式转换运算符,因为 Nullalbe<T> 类型显然并不包含 T 中定义的运算符。
- 查找类型集 D,将从该类型集考虑用户定义的转换运算符。此集由 S0(如果 S0 是类或结构)、S0 的所有基类(如果 S0 是类)和 T0(如果 T0 是类或结构)组成。这里包含 S0 的某个基类,是因为 S 也可以使用基类中定义的转换到其他类的类型转换运算符。
- 查找适用的用户定义转换运算符和提升转换运算符集 U。此集合由用户定义的隐式转换运算符和提升隐式转换运算符组成,这些运算符是在 D 中的类或结构内声明的,用于从包含 S 的类型(即 S 或 S 的基类和实现的接口)转换为被 T 包含的类型(即 T 或 T 的子类)。如果 U 为空,则转换未定义并且发生编译时错误。
- 在 U 中查找运算符的最精确的源类型 SX:
- 如果 U 中存在某一运算符从 S 转换,则 SX 为 S。
- 否则,SX 是在 U 的运算符源类型的集合中被包含程度最大的类型。如果无法恰好找到一个被包含程度最大的类型,则转换是不明确的,并且发生编译时错误。这里比较难理解,简单来说 SX 实际就是在继承链中最“接近” S 的类,S 的基类就比 S 的基类的基类要更接近 S。
- 在 U 中查找运算符的最精确的目标类型 TX:
- 如果 U 中存在某一运算符转换为 T,则 TX 为 T。
- 否则,TX 是 U 的运算符源类型的集合中包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则转换是不明确的,并且发生编译时错误。这里的 TX 是在继承链中最“接近” T 的类,这里与 S 不同的是,需要注意 Tx 总是 T 或 T 的子类。
- 查找最具体的转换运算符:
- 如果 U 中只含有一个从 SX 转换到 TX 的用户定义转换运算符,则这就是最精确的转换运算符。
- 否则,如果 U 恰好包含一个从 SX 转换到 TX 的提升转换运算符,则这就是最具体的转换运算符。
- 否则,转换是不明确的,并发生编译时错误。
- 最后,应用转换:
- 如果 S 不是 SX,则执行从 S 到 SX 的标准隐式转换。
- 调用最具体的转换运算符,以从 SX 转换到 TX。
- 如果 TX 不是 T,则执行从 TX 到 T 的标准隐式转换。
下面就是更改后的核心代码:
下面附上一些隐式类型转换判断方法(IsImplicitFrom)和 Type.IsAssignableFrom 方法的对比:
1 2 3 4 5 6 7 8 9 10 11 |
|
二、强制类型转换
在进行强制类型转换时,无需保证数据不丢失,也无需保证类型一定能够兼容。判断的过程也类似于隐式类型转换,同样总结为五点:
- 总是可以与 object 类型进行相互转换。
- 兼容的类型不但可以从子类转换为父类,也可以从父类转换为子类。
- .NET 的一些内置类型间总是可以进行强制类型转换,这是另外一个表格。
- 存在隐式或显式类型转换运算符的类型,可以通过 Type.GetMethods 查找名为 op_Explicit 的方法来找到所有的自定义显式类型转换。
- 仍然要考虑 Nullable<T> 类型,不过在强制类型转换时,T 类型总是可以与 Nullable<T> 相互转换。
.Net 内置的强制类型转换比较简单,就是 Char, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double 和 Decimal 之间的相互转换。
强制类型转换的判断算法也是与隐式类型转换类似的,只不过是要考虑一下自定义的显式类型转换方法。
2012.12.27 更新:在强制类型转换时,枚举类型也是需要特殊处理的,处理方法也很简单,就是将枚举类型转换为对应的基类型,再进行判断。
1 2 3 |
|
2012.12.31 更新:在对枚举类型能否进行强制类型转换进行判断时,上面的方法是有问题的,仅仅简单考虑了基础类型,而没有有效利用枚举类型本身。实际上,仅枚举类型及其基础类型间可以进行强制类型转换,或者是两个枚举类型之间进行强制类型转换,其他情况则不需要考虑枚举的基础类型的问题。对应的代码如下:
1 2 3 4 5 6 7 |
|
其中,IsEnumUnderlyingType 用于判断类型是否是 Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64 之中的一个。
2013.3.17 更新:对于显式转换,根据《CSharp Language Specification》v5.0 中 6.2 节的说明,我上面总结的规则显然是不全面的,显式引用转换中的一大部分都没有实现(例如允许从任何 interface-type S 到任何 interface-type T)。不过,我之前没有实现的部分并不属于常用的显式转换,而且实现起来会很麻烦,尤其是涉及到泛型类的时候(有兴趣的可以自己找找规范看看,6.2.4 节总共有 14 条,我仅实现了其中的 4 条),所以最后也没有完整实现。
我基本改写了一下之前的显式类型转换算法,用户定义的显式转换部分也并未完全按照 6.4.5 节 用户定义的显式转换 中说明的实现(有点复杂,而且效率不高),而是采用了类似用户定义的隐式转换的算法(反正实现本来就是不完整的,还是简便起见的好)。
下面给出更改后的核心代码:
三、是否可以分配到开放泛型类型
开放泛型类型指的就是类似于 List<> 这样的没有提供类型参数的泛型类型,有时也是需要判断
1 |
|
的,不过很可惜,Type.IsAssignableFrom 方法同样不适用,这里同样需要自己来处理。
这里需要考虑的情况简单些,假设 type 是要测试的开放泛型类型,fromType 是源类型,则有下面几种情况:
fromType 是接口 | fromType 是类型 | |
type 是接口 | 判断 fromType 及其实现的接口是否匹配 type | 判断 fromType 实现的接口是否匹配 type |
type 是类 | 不可能匹配 | 判断 fromType 的所有基类是否匹配 type |
而泛型类型间是否匹配的判断,只要从 fromType 从将类型参数提取出来,再通过 type.MakeGenericType 生成相应的类型,就可以用 IsAssignableFrom 判断类型是否匹配了,这样做还可以一并获取匹配的类型参数。
主要代码为:
下面是一个例子:
1 |
|
方法可以正确的判断出 Dictionary<string,int> 类型是可以分配到 IEnumerable<> 类型的,而且类型参数为 KeyValuePair<string,int>。
完整的代码源文件可见 https://github.com/CYJB/.../TypeExt.cs。
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。