注意:这似乎已在 Roslyn中修复
这个问题是在写我对这个问题的回答时出现的,它谈到了null- coalescing operator的关联性。
提醒一下,null-coalescing 运算符的想法是形式的表达式
x ?? y
首先评估x,然后:
x
y
x``y
现在 通常 不需要转换,或者它只是从可空类型到不可空类型 - 通常类型是相同的,或者只是从(比如说)int?到int. 但是,您 可以 创建自己的隐式转换运算符,并在必要时使用这些运算符。
int?
int
对于 的简单情况x ?? y,我没有看到任何奇怪的行为。但是,(x ?? y) ?? z我看到一些令人困惑的行为。
(x ?? y) ?? z
这是一个简短但完整的测试程序 - 结果在评论中:
using System; public struct A { public static implicit operator B(A input) { Console.WriteLine("A to B"); return new B(); } public static implicit operator C(A input) { Console.WriteLine("A to C"); return new C(); } } public struct B { public static implicit operator C(B input) { Console.WriteLine("B to C"); return new C(); } } public struct C {} class Test { static void Main() { A? x = new A(); B? y = new B(); C? z = new C(); C zNotNull = new C(); Console.WriteLine("First case"); // This prints // A to B // A to B // B to C C? first = (x ?? y) ?? z; Console.WriteLine("Second case"); // This prints // A to B // B to C var tmp = x ?? y; C? second = tmp ?? z; Console.WriteLine("Third case"); // This prints // A to B // B to C C? third = (x ?? y) ?? zNotNull; } }
所以我们有三种自定义值类型,A和B,C以及从 A 到 B、A 到 C 和 B 到 C 的转换。
A
B
C
我可以理解第二种情况和第三种情况......但是 为什么 在第一种情况下会有额外的 A 到 B 转换?特别是,我 真的 希望第一种情况和第二种情况是同一件事——毕竟它只是将一个表达式提取到一个局部变量中。
有没有人知道发生了什么?当谈到 C# 编译器时,我非常犹豫要喊“错误”,但我对发生的事情感到困惑......
编辑:好的,这是一个更糟糕的例子,这要归功于配置器的回答,这让我有进一步的理由认为这是一个错误。编辑:该示例现在甚至不需要两个空合并运算符…
using System; public struct A { public static implicit operator int(A input) { Console.WriteLine("A to int"); return 10; } } class Test { static A? Foo() { Console.WriteLine("Foo() called"); return new A(); } static void Main() { int? y = 10; int? result = Foo() ?? y; } }
这个的输出是:
Foo() called Foo() called A to int
Foo()在这里被调用两次的事实让我非常惊讶——我看不出有任何理由让表达式被 计算 两次。
Foo()
感谢所有为分析此问题做出贡献的人。这显然是一个编译器错误。它似乎只发生在合并运算符左侧涉及两个可空类型的提升转换时。
我还没有确定到底哪里出了问题,但是在编译的“可空降低”阶段的某个时刻——在初始分析之后但在代码生成之前——我们减少了表达式
result = Foo() ?? y;
从上面的例子到道德上的等价物:
A? temp = Foo(); result = temp.HasValue ? new int?(A.op_implicit(Foo().Value)) : y;
显然这是不正确的;正确的降低是
result = temp.HasValue ? new int?(A.op_implicit(temp.Value)) : y;
到目前为止,根据我的分析,我最好的猜测是可空优化器在这里偏离了轨道。我们有一个可以为空的优化器,它查找我们知道可以为空类型的特定表达式不可能为空的情况。考虑以下简单的分析:我们可以先说
是相同的
A? temp = Foo(); result = temp.HasValue ? (int?) temp : y;
然后我们可以说
conversionResult = (int?) temp
A? temp2 = temp; conversionResult = temp2.HasValue ? new int?(op_Implicit(temp2.Value)) : (int?) null
但是优化器可以介入并说“哇,等一下,我们已经检查了 temp 不为空;没有必要因为我们调用提升的转换运算符而第二次检查它是否为空”。我们让他们将其优化为
new int?(op_Implicit(temp2.Value))
我的猜测是,我们在某个地方缓存了优化形式(int?)Foo()是new int?(op_implicit(Foo().Value))但实际上不是我们想要的优化形式的事实;我们想要 Foo()-replaced-with- temporary-and-then-converted 的优化形式。
(int?)Foo()
new int?(op_implicit(Foo().Value))
C# 编译器中的许多错误都是由于错误的缓存决策造成的。对智者的一句话: 每次你缓存一个事实供以后使用时,如果相关的东西发生变化,你可能会造成不一致 。在这种情况下,初始分析后改变的相关事情是对 Foo() 的调用应始终实现为临时获取。
我们在 C# 3.0 中对可为空的重写过程进行了大量重组。该错误在 C# 3.0 和 4.0 中重现,但在 C# 2.0 中没有重现,这意味着该错误可能是我的错误。对不起!
我会在数据库中输入一个错误,我们会看看我们是否可以为该语言的未来版本修复这个错误。再次感谢大家的分析;这很有帮助!
更新:我从头开始为 Roslyn 重写了可为空的优化器;它现在做得更好,并避免了这些奇怪的错误。有关 Roslyn 中的优化器如何工作的一些想法,请参阅我从这里开始的系列文章:https ://ericlippert.com/2012/12/20/nullable- micro-optimizations-part-one/