在之前的文章中,我大致介绍过一些类型间的隐式和显式类型转换规则。但当时并未很仔细的研究过《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 指令一般比较简单,基本就是 castclassbox 和 unbox 指令,复杂一些的就是隐式/显式数值转换和可空类型的转换。

隐式/显式数值转换我总结了下面的表格,其实现基本就是查表格的过程。表格的上方是不进行溢出检查的 IL 指令,下方是进行溢出检查的 IL 指令,空格表示无需插入 IL 指令即可进行类型转换;绿色背景表示隐式数值转换,黄色背景表示显式数值转换:

图 2 隐式/显式数值转换

注意数值转换有溢出检查的区分(checked/unchecked),而且表格中并未列出 Decimal 类型,因为 Decimal 类型与其它数值类型间的转换依靠的是使用 implicit/explicit 定义的类型转换方法,不适合使用查表的方法。

可空类型的转换,可以分为三种情况(设 ST 都是非可空的值类型):

  1. 从 S? 到 T? 的显式类型转换,其过程为:

    • 如果输入值是 null,那么结果为 T? 类型的 null
    • 否则将 S? 解包为 S,然后执行从 S 到 T 的类型转换,最后从 T 包装为 T?
  2. 从 S? 到 T 的隐式/显式类型转换,其过程为:
    • 若输入值是 null,那么引发异常。
    • 否则将 S? 解包为 S,然后执行从 S 到 T 的类型转换。
  3. 从 S 到 T? 的隐式/显式类型转换,先执行从 S 到 T 的类型转换,然后从 T 包装为T?

可空类型的转换,可参见 BetweenNullableConversion.csFromNullableConversion.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 类型的用户自定义显式类型转换按下面这样处理:

  1. 确定类型 S0 和 T0。如果 S 或 T 是可空类型,则 S0 和 T0 就是它们的基础类型;否则 S0 和 T0 分别等于 S 和 T。得到 S0 和 T0 是为了在其中查找用户自定义的隐式/显式类型转换运算符。
  2. 找到类型集合 D,将从该集合中查找用户自定义类型转换运算符。此集合由 S0(如果 S0 是类或结构体)、S0 的所有基类(如果 S0 是类)、T0(如果 T0 是类或结构体)和 T0 的所有基类(如果 T0 是类)组成。这里包含 S0 和 T0 的基类,是因为 S 和 T 也可以使用基类中声明的类型转换运算符。
  3. 查找适用的用户自定义类型转换运算符和提升转换运算符集合 U。此集合由在 D 中的类或结构内声明的隐式/显式用户自定义类型转换运算符和提升转换运算符组成,用于从包含 S 或被 S 包含的类型(即 SS 的基类、S 实现的接口或 S 的子类)转换为包含 T 或被 T 包含的类型。如果 U 为空,则产生未定义转换的错误。
  4. 在 U 中查找运算符的最精确的源类型 SX
    • 如果 U 中存在某一运算符从 S 转换,则 SX 为 S
    • 否则,如果 U 中存在某一运算符从包含 S 的类型转换,那么 SX 是这类运算符的源类型中被包含程度最大的类型。如果无法恰好找到一个被包含程度最大的类型,则产生不明确的转换的错误。这里找到的是距离 S 最近的包含 S 的类型。
    • 否则,U 中的运算符都是从被 S 包含的类型转换的,那么 SX 是 U 中运算符的源类型中包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则产生不明确的转换的错误。这里找到的是距离 S 最近的被 S 包含的类型。
  5. 在 U 中查找运算符的最精确的目标类型 TX
    • 如果 U 中存在某一运算符转换为 T,则 TX 为 T
    • 否则,如果 U 中存在某一运算符转换到被 T 包含的类型,那么 TX 是这类运算符的目标类型中包含程度最大的类型。如果无法恰好找到一个包含程度最大的类型,则产生不明确的转换的错误。这里找到的是上距离 T 最近的被 T 包含的类型。
    • 否则,U 中的运算符都是转换到包含 T 的类型,那么 TX 是 U 中运算符的目标类型中被包含程度最大的类型。如果无法恰好找到一个被包含程度最大的类型,则产生不明确的转换的错误。这里找到的是距离 T 最近的包含 T 的类型。
  6. 查找最精确的转换运算符:
    • 如果 U 中只包含一个从 SX 转换到 TX 的用户自定义类型转换运算符,那么这就是最精确的转换运算符。
    • 否则,如果 U 只包含一个从 SX 转换到 TX 的提升转换运算符,则这就是最精确的转换运算符。
    • 否则产生不明确的转换的错误。
  7. 最后,应用转换:
    • 如果 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 实现类型转换的更多相关文章

  1. 玩转动态编译 - 高级篇:二,IL设置静态属性,字段和类型转换

    静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public void AAA(string s) { MyCl ...

  2. - 高级篇:二,IL设置静态属性,字段和类型转换

    - 高级篇:二,IL设置静态属性,字段和类型转换 静态属性赋值 先来看 Reflector反射出的IL源码(感谢Moen的提示),这次用 Release模式编译,去掉那些无用的辅助指令 public ...

  3. CLR via C# 摘要二:IL速记

    最简单的IL程序 .assembly test {} .method void Func() { .entrypoint ldstr "hello world" call void ...

  4. 玩转动态编译 - 高级篇:一,IL访问静态属性和字段

    IL介绍 通用中间语言(Common Intermediate Language,简称CIL,发音为"sill"或"kill")是一种属于通用语言架构和.NET ...

  5. CLR via C#(03)- 对象创建和类型转换

    一. 创建对象 CLR要求用new操作符创建对象,这个操作符在编译时产生的IL指令为newobj.例如: Student XiaoJing=new Student(“XiaoJing”,”1986”) ...

  6. 尽量采用as操作符而不是旧式C风格做强制类型转换

    http://www.cnblogs.com/JiangSoney/archive/2009/08/07/1541488.html MSDN: https://msdn.microsoft.com/z ...

  7. 栈和托管堆/值类型和引用类型/强制类型转换/装箱和拆箱[C#]

    原文地址:http://www.cnblogs.com/xy8.cn/articles/1227228.html 一.栈和托管堆      通用类型系统(CTS)区分两种基本类型:值类型和引用类型.它 ...

  8. 一,IL访问静态属性和字段

    一,IL访问静态属性和字段 IL介绍 通用中间语言(Common Intermediate Language,简称CIL,发音为"sill"或"kill")是一 ...

  9. 利用自动类型转换存储string类型

    类型转换是我们最常用的功能.就像上战场用的枪一样,敌人用的冲锋枪, 自己手里就一把步枪,打起仗来始终有点不爽. 因此,基本功能的完善很重要. 通常情况下我们需要String类型转其它的基础类型.这时我 ...

随机推荐

  1. 如何用火车头采集当前页面url网址

    首先创建一个标签为本文网址,勾选后面的“从网址中采集”. 选择下面的“正则提取”,点击通配符“(?<content>?)”,这样在窗口中就显示为(?<content>[\s\S ...

  2. PHP引用(&)初探:函数的引用返回

    函数的引用返回 先看代码: <?php function &test() { static $b=0;//申明一个静态变量 $b=$b+1; echo $b; return $b; } ...

  3. HDU 1062 Text Reverse(水题,字符串处理)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1062 解题报告:注意一行的末尾可能是空格,还有记得getchar()吃回车符. #include< ...

  4. [LA3026]Period

    [LA3026]Period 试题描述 For each prefix of a given string S with N characters (each character has an ASC ...

  5. linux下软件安装的方法

    linux下软件的安装与卸载   第一章   linux下安装软件,如何知道软件安装位置 注:一般的软件的默认安装目录在 jdk-1_6_0_14-linux-i586-rpm.bin    ←修改为 ...

  6. hping3命令

    hping3命令 网络测试 hping是用于生成和解析TCPIP协议数据包的开源工具.创作者是Salvatore Sanfilippo.目前最新版是hping3,支持使用tcl脚本自动化地调用其API ...

  7. 工作中常用shell之ssh登陆不用输入"yes"

    ip="192.168.5.166"ssh $ip -o StrictHostKeyChecking=no           //ssh登陆不用输入"yes" ...

  8. 【OpenStack】OpenStack系列17之OpenStack私有云设计一

    [软件系统] 1.操作系统(Minimal最小化安装): CentOS-6.6-x86_64,CentOS 6最后一个版本,官方建议版本. 相对于6.5版本: 强化对 SCSI 设备的处理,有助应付某 ...

  9. MySQL使用索引的场景及真正利用索引的SQL类型

    1. 为什么使用索引 在无索引的情况下,MySQL会扫描整张表来查找符合sql条件的记录,其时间开销与表中数据量呈正相关.对关系型数据表中的某些字段建索引可以极大提高查询速度(当然,不同字段是否sel ...

  10. RTX登录其他系统

    前台: <html> <head> <title>签名验证</title> <meta http-equiv="Content-Lang ...