VS2010是否不符合C# 4.0规范中的“用户定义转换的评估”?

4
如果您使用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;
    }

使用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的类型的用户定义和提升的隐式或显式转换运算符组成。如果U为空,则转换未定义,并且会发生编译时错误。

ECMA 334:

查找适用的转换运算符集U。此集合由在D中声明的类或结构体定义的从包含或被包含于S的类型到包含或被包含于T的类型的用户定义和(如果S和T都可空)提升的隐式或显式转换运算符(§13.7.3)组成。如果U为空,则没有转换,将发生编译时错误。

我是否正确认为VS2010不符合C# 4.0规范中“用户定义转换的评估”部分,但符合ECMA规范?

你的文本中没有一个句子,包括标题,带有问号。你只是在陈述事实。我们应该猜测你的问题是什么吗? - Eric Lippert
更新标题。请审核。 - Vince
1个回答

5
让我们首先看看遵循各种规则时会发生什么。
按照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 模仿原始编译器的行为,只不过实现更加清洁、易于理解。


关于数字,我认为Java未能一致地定义隐式或显式转换的含义,而.NET则跟随了Java的步伐,而不是解决问题。如果允许的隐式提升是[SByte | Byte] -> [Int16 | UInt16] -> [Int32 | UInt32] -> [Int64 | UInt64] -> Decimal-> Double-> Single,则对于任何可转换T-> U-> V的三元组类型,可以定义舍入规则,使将T转换为U,然后转换为V与直接将T转换为V相同。隐式转换的规则不会... - supercat
@supercat:我不完全同意将double转换为float,但我确实同意显式的float-to-int转换是不好的。正如你所说,更好的方法是在代码中明确转换的语义。 - Eric Lippert
@EricLippert,感谢您的答复。如果C#团队能够提供C#编译器的内部规范(而不是C# 4.0规范),那么我们就能学到更多。 - Vince
@Vince:不用了,Eric Lippert是最接近4.0 C# walking spec的人。他曾是C#编译器的首席程序员,并且是C#语言设计团队的一员,直到4.0版本。我认为内部的C#规范与公开可用的规范没有任何区别。这样做只会让人感到困惑,毫无意义。 - InBetween
规则本身可以围绕着更为“普遍”的情况设计,即隐式转换为float会产生明确、清晰和合理的结果(例如float foo = 0.1;)。应该有一种方法可以接受任意混合的floatdouble,同时保持具有额外精度的值的精度,但这并不意味着编译器不应该对float one=1f,ten=10f; double foo = one/ten;进行警告。提供让程序员“指定”允许的转换的方法比试图完善规则更有帮助。 - supercat
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接