使用 IL 实现类型转换
在之前的文章中,我大致介绍过一些类型间的隐式和显式类型转换规则。但当时并未很仔细的研究过《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。
使用 IL 实现类型转换的更多相关文章
- 玩转动态编译 - 高级篇:二,IL设置静态属性,字段和类型转换
静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public void AAA(string s) { MyCl ...
- - 高级篇:二,IL设置静态属性,字段和类型转换
- 高级篇:二,IL设置静态属性,字段和类型转换 静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public ...
- CLR via C# 摘要二:IL速记
最简单的IL程序 .assembly test {} .method void Func() { .entrypoint ldstr "hello world" call void ...
- 玩转动态编译 - 高级篇:一,IL访问静态属性和字段
IL介绍 通用中间语言(Common Intermediate Language,简称CIL,发音为"sill"或"kill")是一种属于通用语言架构和.NET ...
- CLR via C#(03)- 对象创建和类型转换
一. 创建对象 CLR要求用new操作符创建对象,这个操作符在编译时产生的IL指令为newobj.例如: Student XiaoJing=new Student(“XiaoJing”,”1986”) ...
- 尽量采用as操作符而不是旧式C风格做强制类型转换
http://www.cnblogs.com/JiangSoney/archive/2009/08/07/1541488.html MSDN: https://msdn.microsoft.com/z ...
- 栈和托管堆/值类型和引用类型/强制类型转换/装箱和拆箱[C#]
原文地址:http://www.cnblogs.com/xy8.cn/articles/1227228.html 一.栈和托管堆 通用类型系统(CTS)区分两种基本类型:值类型和引用类型.它 ...
- 一,IL访问静态属性和字段
一,IL访问静态属性和字段 IL介绍 通用中间语言(Common Intermediate Language,简称CIL,发音为"sill"或"kill")是一 ...
- 利用自动类型转换存储string类型
类型转换是我们最常用的功能.就像上战场用的枪一样,敌人用的冲锋枪, 自己手里就一把步枪,打起仗来始终有点不爽. 因此,基本功能的完善很重要. 通常情况下我们需要String类型转其它的基础类型.这时我 ...
随机推荐
- iphone/ipad图标尺寸
http://www.yixieshi.com/ucd/13759.html APP界面设计规范指导APP设计过程中的设计标准,根据统一的设计标准,使得整个APP在视觉上统一.提高用户对APP的产品认 ...
- Capistrano SSH::AuthenticationFailed, not prompting for password
文章是从我的个人博客上粘贴过来的, 大家也可以访问 www.iwangzheng.com 在本地执行cap deploy部署的时候会报错: connection failed for: 11.11.1 ...
- scp 命令
复制文件: (1)将本地文件拷贝到远程 scp 文件名 用户名@计算机IP或者计算机名称:远程路径 (2)从远程将文件拷回本地 ...
- 使用caffe自动测试模型top5的结果
方法很简单,直接在定义网络的prototxt里面最后加一层就可以了. 这一层定义如下 layer { name: "accuracy_5" type: "Accuracy ...
- top对僵尸进程的处理
ps --forest ASCII art process tree 2 怎样来清除僵尸进程: 1.改写父进程,在子进程死后要为它收尸.具体做法是接管SIGCHLD信 ...
- Intersection of Two Arrays | & ||
Intersection of Two Arrays Given two arrays, write a function to compute their intersection. Example ...
- 查看别人的css
ie工具栏的“文件”选项选“另存为”到你本地电脑,存下来有两个文件 一个是空间名称命名的文件夹和html网页,文件加里有三个扩展名为.css的文件
- spring无法扫描jar包的问题
在日常开发中往往会对公共的模块打包发布,然后调用公共包的内容.然而,最近对公司的公共模块进行整理发布后.spring却无法扫描到相应的bean.折腾了好久,最终发现是认识上的误区. 2015-11-1 ...
- MySQL数据库备份和还原的常用命令
其实很多情况下mysql备份就是采用了这些命令,例如: mysql导入和导出数据 linux自动定时备份web程序和mysql数据库 备份MySQL数据库的命令 mysqldump -hhostnam ...
- 解决虚拟机 正在决定eht0 的ip信息失败 无链接-- 虚拟机上linux redhat 上网问题
对于虚拟机上,linux redhat上网的配置方式有三种 一.用setup命令进行配置(具体技巧可查setup命令的使用) 二.直接用 ifconfig eth0 ip地址进行配置 三.进入系统文 ...