让我们首先看看遵循各种规则时会发生什么。
按照C#4.0规范的规则:
- 要搜索用户定义转换的类型集合D包括A和B。
- 可应用转换的集合U包括从A到B的用户定义隐式转换以及从A?到B?的升级用户定义隐式转换。
- 现在我们必须选择U中唯一最佳的两个元素之一。
- 最具体的源类型是A?。
- 最具体的目标类型是B。
- U不包含从A?到B的转换,因此这是有歧义的。
这应该是有道理的。我们不知道这里的转换是否应该使用升级转换,从A?到B?,然后从B?到B,或者是否应该使用非升级转换,从A?到A,然后从A到B。
旁注:
经过深入思考,不清楚这是否会产生任何区别。
假设我们使用升级转换。如果A?不为空,则我们将从A?转换为A,然后从A到B,然后从B到B?,然后再将B?转换回B,这样就成功了。如果A?为空,则我们将直接将A?转换为null B?,然后在将其解压缩为B时崩溃。
假设我们使用非升级转换,而A?不为空。那么我们从A?转换为A,从A到B,完成。如果A?为空,则在将A?解压缩为A时崩溃。
因此,在这种情况下,两个版本的转换具有完全相同的操作,因此无论我们选择哪个都没有关系,因此将其称为歧义是不幸的。但是,这并不改变以下事实:编译器显然没有遵循C#4规范的字面意思。
那么ECMA规范呢?
- 集合U包括从A到B的用户定义转换,但不包括升级转换,因为S(即A?)和T(即B)不都是可空的。
现在我们只有一个要选择,所以重载分辨率很容易。
但是,这并不意味着编译器遵循ECMA规范的规则。事实上,它既不遵循ECMA规范的规则,也不遵循C#4.0规范的规则。它更接近于ECMA规范,因为它不会将两个运算符都添加到候选集中,因此在这种简单情况下,选择候选集的唯一成员。但是事实上,它永远不会将升级运算符添加到候选集中,即使源和目标都是可空值类型。此外,它违反了ECMA规范的许多其他方面,这些方面将在更复杂的示例中显示出来。
在从非空结构体类型到可空结构体类型、指针类型或引用类型的用户定义转换中,允许使用提升转换语义(即,在调用方法之前插入空检查并在操作数为 null 时跳过它)。也就是说,如果您有从 A 到 string 的转换,则会得到从 A? 到 string 的提升转换,如果操作数为 null,则生成 null 字符串。该规则在任何规范中都找不到。
根据规范,必须互相包含或被包含的类型是正在转换的表达式类型(称为规范中的 S)和用户定义转换的形式参数类型。如果被转换的表达式类型是可空值类型,则 C# 编译器实际上会检查其基础类型是否包含。这意味着某些转换应该被拒绝,而被接受了。
根据规范,最佳目标类型应通过查看各种转换的输出类型(提升或未提升)集合来确定。从 A 到 B 的用户定义转换应视为具有 B 的输出类型,以便确定最佳输出类型。但是,如果您将 A 转换为 B? ,那么编译器实际上会将 B? 视为未提升转换的输出类型,以确定最具体的输出类型!
在处理用户定义转换时,我可以继续(而且会继续)讲上几个小时,介绍这些和其他数不清的漏洞。我们仅仅触及了表面;我们甚至还没有涉及泛型参与的情况。但是我会放过你。本文的要点是:您无法狭隘地解析任何版本的 C# 规范,并从中确定在复杂的用户定义转换场景中会发生什么。编译器通常会按用户的期望执行,并且通常出于错误的原因而执行。
这既是规范中最复杂的部分,也是编译器遵循最少的部分,这是一个不好的组合。这非常令人遗憾。
我曾尝试使 Roslyn 与规范保持一致,但失败了。这样做引入了太多现实世界中的破坏性变化。相反,我让 Roslyn 模仿原始编译器的行为,只不过实现更加清洁、易于理解。