如果使用Visual Studio 2010编译以下代码:
public struct A
{
public static implicit operator B(A a)
{
Console.WriteLine("11111111111");
return new B();
}
}
public struct B
{ }
public static B F(A? a)
{
return (B)a;
}
Run Code Online (Sandbox Code Playgroud)
使用ILSpy,return (B)a;实际编译为return A.op_Implicit(a.value).
根据我对C#4.0第6.4.5节"用户定义的显式转换"的理解,它应该产生编译器错误.
但是,阅读ECMA 334第13.4.4节"用户定义的显式转换",它有一个不同的规则,上面的代码似乎符合这一规则.
C#4.0:
查找适用的用户定义和提升转换运算符集合U.此集合包含由 D中的类或结构声明的用户定义和提升的隐式或显式转换运算符,这些运算符从包含或包含在S中的类型转换为T包含或包含T.如果U为空,则转换未定义,并发生编译时错误.
ECMA 334:
找到适用的转换运算符集合U.这个集合包含用户定义的,如果S和T都可以为空,则提升隐式或显式转换运算符(第13.7.3节),由转换为D的类或结构声明从包含或包含在S中的类型到包含或包含在T中的类型.如果U为空,则不存在转换,并且发生编译时错误.
我是否认为VS2010不符合C#4.0规范中的"用户定义的转换评估"部分,但是是否符合ECMA规范?
让我们首先看看当我们遵循各种规则时会发生什么.
遵循C#4.0规范中的规则:
这应该是有道理的.我们在这里不知道转换是否应该使用提升转换,从A转换?到B?然后从B?到B,或者我们是否应该使用未提升的转换,从A转换?到A然后是A到B.
在旁边:
经过深入反思,目前尚不清楚这是否会产生任何差异.
假设我们使用提升转换.如果一个?是非null然后我们将从A转换?到A,然后A到B,然后是B到B ?,那么B?回到B,这将成功.如果一个?是null然后我们将从A转换?直接到null B ?,然后在解包到B时崩溃
假设我们使用未提升的转换和A?是非null.然后我们从A转换?到A,A到B,完成了.如果一个?是null我们在打开A时会崩溃吗?到A.
因此,在这种情况下,转换的两个版本具有完全相同的动作,因此我们选择的并不重要,因此称这种歧义是不幸的.但是,这并没有改变这样一个事实,即编译器显然没有遵循C#4规范的字母.
ECMA规范怎么样?
现在我们只有一个可供选择,因此重载分辨率很容易.
但是,这并不意味着编译器遵循ECMA规范的规则. 事实上,它遵循两个规范的规则.它更接近 ECMA规范,因为它不会将两个运算符都添加到候选集中,因此,在这个简单的情况下,选择候选集的唯一成员.但事实上,即使源和目标都是可空的值类型,它也永远不会将提升的运算符添加到候选集中.此外,它还以许多其他方式违反了ECMA规范,这些方式将通过更复杂的示例显示:
提升转换语义(即,在调用方法之前插入空检查并在操作数为空时跳过它)允许在用户定义的转换中从不可为空的结构类型转换为可为空的结构类型,指针类型或引用类型!也就是说,如果您从A转换为字符串,那么您将从A获得提升转换?如果操作数为null,则生成空字符串的字符串.这两条规则中都没有找到这条规则.
根据规范,必须包含或被彼此包含的类型是被转换的表达式的类型(在规范中称为S)和用户定义的转换的形式参数类型.如果C#编译器是可以为空的值类型,它实际上会检查要转换的表达式的基础类型的包含.(规范中的S0)这意味着应该拒绝某些应该被拒绝的转换.
根据规范,最佳目标类型应通过查看各种转换的输出类型集来确定,提升或未提升.用户定义的从A到B的转换应被视为具有B的输出类型,以便找到最佳输出类型.但是如果你有从A到B的演员?那么编译器实际上会考虑B?作为未提升转换的输出类型,用于确定最具体的输出类型!
我可以在用户定义的转换处理中继续(以及继续......)数小时以及其他许多错误.我们在这里几乎没有触及表面; 我们甚至没有了解泛型参与时会发生什么.但我会饶了你.这里要说的是:你不能狭隘地解析任何版本的C#规范,并从中确定在复杂的用户定义转换场景中会发生什么.编译器通常会执行用户期望的操作,并且通常是出于错误的原因.
这既是规范中最复杂的部分之一,也是规范中编译器符合最小部分的部分,这是一个糟糕的组合.这是非常不幸的.
我勇敢地试图让罗斯林遵守规范,但我失败了; 这样做引入了太多,真实世界的重大变革.相反,我让Roslyn复制了原始编译器的行为,只是使用了更清晰,更易于理解的实现.