Curious null-coalescing operator custom implicit conversion behaviour
注:这似乎是在罗斯林修复的。
这个问题是在写我对这个问题的答案时产生的,这个问题讨论了空合并操作符的关联性。
正如一个提醒,空合并运算符的概念是
1 | x ?? y |
首先评估
- 如果
x 的值为空,则计算y 的值,这是表达式的最终结果。 - 如果
x 的值不为空,则不计算y ,x 的值是表达式的最终结果,必要时转换为y 的编译时类型。
现在通常不需要转换,或者只是从一个可以为空的类型转换为一个不可以为空的类型——通常类型是相同的,或者只是从(比如)
对于简单的
下面是一个简短但完整的测试程序-结果在注释中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | 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编译器出现"bug"的时候,我绝对不会大喊"bug",但是我很难理解到底发生了什么……
编辑:好吧,这是一个更糟糕的例子,这要归功于配置程序的回答,这给了我进一步的理由认为这是一个bug。编辑:该示例现在甚至不需要两个空合并运算符…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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; } } |
其结果是:
1 2 3 | Foo() called Foo() called A to int |
事实上,在这里两次调用
感谢所有参与分析这个问题的人。这显然是一个编译器错误。似乎只有在合并运算符左侧有涉及两个可为空类型的提升转换时才会发生。
我还没有确定到底哪里出了问题,但是在编译的"可以为空的降低"阶段的某个时刻——在初始分析之后,在代码生成之前——我们减少了表达式
1 | result = Foo() ?? y; |
从上面的例子到道德等价物:
1 2 3 4 |
显然这是不正确的;正确的降低是
1 2 3 |
根据我目前的分析,我的最佳猜测是,可以为空的优化器在这里偏离了轨道。我们有一个可以为空的优化器,它查找我们知道可以为空类型的特定表达式不可能为空的情况。想想下面的幼稚分析:我们可以先这么说
1 | result = Foo() ?? y; |
是一样的
1 2 3 4 | A? temp = Foo(); result = temp.HasValue ? (int?) temp : y; |
然后我们可以这么说
1 | conversionResult = (int?) temp |
是一样的
1 2 3 4 | A? temp2 = temp; conversionResult = temp2.HasValue ? new int?(op_Implicit(temp2.Value)) : (int?) null |
但是优化器可以介入并说"哇,等一下,我们已经检查了temp不是空的;不需要再次检查它是否为空,因为我们调用的是提升的转换运算符"。我们让他们把它优化到
1 |
我的猜测是,我们正在缓存这样一个事实:
C编译器中的许多错误都是由错误的缓存决策造成的。明智的一句话:每次缓存一个事实供以后使用时,如果发生了相关的更改,都可能导致不一致。在这种情况下,改变初始分析后的相关事情是,对foo()的调用应该始终作为临时的获取来实现。
我们对C 3.0中的可空重写过程进行了大量的重组。这个bug在C 3.0和4.0中复制,但在C 2.0中没有复制,这意味着这个bug可能是我的错。对不起的!
我将在数据库中输入一个bug,然后我们将查看是否可以在将来的语言版本中修复这个bug。再次感谢大家的分析,非常有帮助!
更新:我为Roslyn从头重写了Nullable优化器;它现在做得更好,并且避免了这些奇怪的错误。有关Roslyn中优化器如何工作的一些想法,请参阅我的系列文章,文章从这里开始:https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
这绝对是一个错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
此代码将输出:
1 2 3 4 5 6 7 | X() X() A to B (0) X() X() A to B (0) B to C (0) |
这使我认为每个
1 | B? test= (X() ?? Y()); |
输出:
1 2 3 | X() X() A to B (0) |
只有当表达式需要在两个可为空的类型之间进行转换时,才会出现这种情况;我尝试了各种排列,其中一个边是字符串,但没有一个边导致这种行为。
如果您查看左分组案例生成的代码,它实际上是这样做的(
1 2 3 4 5 6 7 |
另一个发现是,如果使用
根据C 4.0规范第6.1.4条:
- If the nullable conversion is from
S? toT? :
- If the source value is
null (HasValue property isfalse ), the result is thenull value of typeT? .- Otherwise, the conversion is evaluated as an unwrapping from
S? toS , followed by the underlying conversion fromS toT , followed by a wrapping (§4.1.10) fromT toT? .
这似乎可以解释第二个展开包装组合。
C 2008和2010编译器生成的代码非常相似,但这看起来像是C 2005编译器(8.00.50727.4927)的回归,它为上述代码生成以下代码:
1 2 3 |
我想知道这是否是因为类型推理系统的额外魔力?
实际上,我现在把它称为bug,用更清楚的例子。这仍然有效,但双重评价肯定不好。
似乎
在这里可以看到,
我把它简单地说成是一个如何实现take away:don't create implicit casting operators with side effects.
这似乎是一个围绕如何实现
从我的问题历史可以看出,我根本不是一个C专家,但是,我尝试过,我认为这是一个错误……但是作为一个新手,我不得不说我不理解这里发生的一切,所以如果我离开的话,我会删除我的答案。
我已经得出了这个结论,通过对您的程序做一个不同的版本来处理相同的场景,但要简单得多。
我正在使用三个带后备存储的空整数属性。我将每个设置为4,然后运行
(此处为完整代码)
这只是读A,没有其他内容。
我觉得这句话应该:
因此,因为a不是空的,所以它只查看a并完成。
在您的示例中,在第一种情况下放置一个断点表明x、y和z都不是空的,因此,我希望它们被视为与我的不太复杂的示例相同的东西……但我担心我太像一个新手了,完全错过了这个问题的要点!