使用 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 指令一般比较简单,基本就是 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

时间: 2024-10-12 18:39:25

使用 IL 实现类型转换的相关文章

More Effective C++

条款一:指针与引用的区别 指针与引用看上去完全不同(指针用操作符'*'和'->',引用使用操作符'.'),但是它们似乎有相同的功能.指针与引用都是让你间接引用其他对象.你如何决定在什么时候使用指针,在什么时候使用引用呢? 首先,要认识到在任何情况下都不能用指向空值的引用.一个引用必须总是指向某些对象.因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量.相反,如果变量肯定指向一个对象,例如你的设计不允许变量为

Atitit..net&#160;clr&#160;il指令集&#160;以及指令分类&#160;&#160;与指令详细说明

Atitit..net clr il指令集 以及指令分类  与指令详细说明 1.1. .NET CLR 和 Java VM 都是堆叠式虚拟机器(Stack-Based VM), 1 1.2. 查看工具ILDASM1 1.3. 此程式执行时,关键的记忆体有三种,分別是:1 1.4. Il指令集2 1.4.1. Mov指令3 1.4.2.  跳转指令集合6 1.4.3.  算术 逻辑 与移位指令8 1.4.4. 类型转换9 1.4.5. Other  and oo指令10 2. 参考12 1.1. 

一个类型转换而引起的三级事件的一些思考

前段时间出了个三级事件,查下来竟然是因为一个溢出造成的死循环,在公司出事件还是挺冒险的一件事,除了大boss要扣钱,还要给 高层一个合理的解释,如果在小公司干活,可能就算网站宕了一天估计也没事,如果在大点的公司每秒都是银子的流失,也许造成的损失就算 我们白干一二年也抵不了,所以责任心和代码意识真的很重要. 先来看看问题代码,在这里我做了一点点的修改,代码的意思很简单,就是想获取参数num中二进制1的个数. 1 static void Run(long num) 2 { 3 int i = 1;

c++ primer(第五版)学习笔记及习题答案代码版(第十四章)重载运算与类型转换

笔记较为零散,都是自己不熟悉的知识点. 习题答案至于一个.h 和.cc 中,需要演示某一题直接修改 #define NUM****, 如运行14.30题为#define NUM1430: Alice Emma has long flowing red hair. Her Daddy says when the wind blows through her hair, it looks almost alive, like a fiery bird in flight. A beautiful f

C++11(13):重载运算与类型转换

除了重载的函数调用运算符operator()之外,其他重载运算符不能含有默认实参. 当以个重载的运算符是成员函数时,this绑定到左侧运算对象.成员运算符函数的(显式)参数比运算对象的数量少一个. 当运算符作用于内置类型的运算对象时,我们无法改变运算的含义. ::              .*                     .          ?:   这四个运算符不能被重载. 我们只能重载已有的运算符,不能发明新的.优先级和结合律不变. data1 + data2; operat

Javascript类型转换的规则实例解析

http://www.jb51.net/article/79916.htm 类型转换可以分为隐式转换和显式转换,所谓隐式转换即程序在运行时进行的自动转换,显式转换则是人为的对类型进行强制转换.Javascript的变量是松散类型的,它可以存储Javascript支持的任何数据类型,其变量的类型可以在运行时被动态改变.请看示 例: ? 1 2 3 var n = 10; n = "hello CSSer!"; n = {}; 上面的示例中,首先声明n变量并初始化其值为10(整数类型),接

进制、类型转换、提升

进制 java支持四种进制表示 进制 示例 前缀 备注 十进制 int x = 10;     二进制 int x = 0b10; 0b或0B 计算机储存 八进制 int x = 010; 0 兼容以前程序 十六进制 int x = 0xceaf 0x或0X 0~9,a~f 细节: 一个十六进制位可以表示四个二进制位 当数值位数较多时,可以使用下划线对数字进行隔开,例:1_230_456 字面常量 整数:字面常量为int型,如果要表示long类型的字面常量,使用L或l做后缀,建议使用L,因为字母

第二节 变量 、 基本类型 、 运算符 、 表达式 、 数据 、 类型转换 、 常量

一:变量 定义:在程序运行中随时可以发生变化的量 变量声明:变量的声明只能以数字,字母,下划线,且字母不能开头,同时不能是C#中的关键字. 变量使用:变量必须先声明才能使用,变量赋值必须是对应类型,int age=true:这样就会报错. 二:常量 定义:在程序的生存期内不发生更改的不可变值 常量声明:const关键字  public const int age=10; age在程序内不会变,值一直是10: 三:内置数据类型(数值类型和非数值类型) 数值类型:分为整型和非整型 整型如下图: 非整

03 php 数据类型:整数,进制转换,浮点,字符,布尔,数组,空类型,类型转换,算术运算,比较运算

03 数据类型:整数,进制转换,浮点,字符,布尔,数组,空类型,类型转换, 算术运算,比较运算,逻辑运算,短路现象, 三目运算符,字符型运算: 数据类型 整体划分 标量类型: int, float, string, bool 复合类型: array,     object 特殊类型: null,     resouce 整数类型int, integer 3种整数表示法 十进制写法:123: $n1 = 123; 八进制写法: 0123 $n2 = 0123; 十六进制写法: 0x123 $n3