好奇的空合并运算符自定义隐式转换行为

注意:这似乎已经在Roslyn中修复了

这个问题出现在我写这个答案的时候,谈到了空合并算子的相关性。

提醒一下,空合并运算符的概念是表单的forms

x ?? y 

首先评估x ,然后:

  • 如果x值为null,则评估y ,这是expression式的最终结果
  • 如果x的值非空, 则不计算y ,并且如果需要的话,在转换为编译时typesy之后, x的值是expression式的最终结果

现在通常不需要转换,或者只是从可空types转换为不可空的types – 通常types是相同的,或者只是从(说) int? int 但是,您可以创build自己的隐式转换运算符,并在必要时使用这些运算符。

对于简单的情况下, 我没有看到任何奇怪的行为。 但是, (x ?? y) ?? z 我看到一些混乱的行为。

这是一个简短但完整的testing程序 – 结果在评论中:

 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; } } 

所以我们有三个自定义值typesABC ,转换从A到B,A到C和B到C.

我可以理解第二种情况和第三种情况……但为什么在第一种情况下会有额外的A到B转换? 特别是,我真的期望第一种情况和第二种情况是一样的 – 毕竟只是将一个expression式提取到一个局部variables中。

任何接受者在发生什么? C#编译器对于“错误”我非常踌躇不绝,但是我很难理解发生了什么…

编辑:好的,这是一个很糟糕的例子,感谢configuration器的答案,这给了我更多的理由认为这是一个错误。 编辑:示例甚至不需要两个空合并操作符现在…

 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()在这里被调用两次的事实对我来说是非常令人惊讶的 – 我看不出有任何理由要对expression式进行两次评估

感谢所有分析这个问题的人。 这显然是一个编译器错误。 这似乎只发生在合并运算符左侧的两个可空types的提升转换时。

我还没有确定出错的地方,但是在编译的“可空的降低”阶段的某个时刻,在初始分析之后,代码生成之前,我们减less了expression式

 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; 

根据我迄今为止的分析,我最好的猜测是可优化的优化器正在脱轨。 我们有一个可以为null的优化器,它查找那些我们知道可以为null的types的特定expression式不可能为null的情况。 考虑下面的天真分析:我们可以先说

 result = Foo() ?? 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)) 

我的猜测是,我们在某处caching(int?)Foo()的优化forms是new int?(op_implicit(Foo().Value))但这实际上并不是我们想要的优化forms; 我们需要Foo()的优化forms – 用临时和随后转换replace。

C#编译器中的许多错误是caching决定不当的结果。 聪明的一句话: 每次你caching一个事实供以后使用,如果有相关的变化,你可能会造成不一致 。 在这种情况下,改变了post初始分析的相关事件是,对Foo()的调用应该总是作为临时获取来实现。

我们在C#3.0中做了很多可重写的重写过程的重组。 该错误在C#3.0和4.0中重现,但不在C#2.0中,这意味着该错误可能是我的错误。 抱歉!

我会得到一个input到数据库中的错误,我们会看看我们是否可以修复这个语言的未来版本。 再次感谢大家的分析。 这是非常有益的!

更新:我重写了可空的优化器从头开始Roslyn; 它现在做得更好,避免了这些奇怪的错误。 有关Roslyn中优化器如何工作的一些想法,请参阅我的系列文章: https : //ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/

这绝对是一个错误。

 public class Program { static A? X() { Console.WriteLine("X()"); return new A(); } static B? Y() { Console.WriteLine("Y()"); return new B(); } static C? Z() { Console.WriteLine("Z()"); return new C(); } public static void Main() { C? test = (X() ?? Y()) ?? Z(); } } 

这段代码将输出:

 X() X() A to B (0) X() X() A to B (0) B to C (0) 

这让我觉得每个人的第一部分?? coalesceexpression式被评估两次。 这段代码certificate了它:

 B? test= (X() ?? Y()); 

输出:

 X() X() A to B (0) 

这似乎只在expression式需要两个可空types之间的转换时才会发生; 我尝试了各种排列方式,其中一个方面是一个string,没有一个引起这种行为。

如果你看一下左分组情况的生成代码,它实际上是这样的( csc /optimize- ):

 C? first; A? atemp = a; B? btemp = (atemp.HasValue ? new B?(a.Value) : b); if (btemp.HasValue) { first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value); } 

另一个发现,如果你first 使用它将产生一个快捷方式,如果ab都为空,并返回c 。 然而,如果ab非空,则在返回ab中的非空值之前,将其重新评估为隐式转换为B一部分。

从C#4.0规范,第6.1.4节:

  • 如果可空转换来自S? T?
    • 如果源值为nullHasValue属性为false ),那么结果是typesT?null值 。
    • 否则,转换被评估为从S?展开S?S ,然后是从ST的底层转换,然后是从TT?的包装(§4.1.10) 。

这似乎解释了第二个解开包装组合。


C#2008和2010编译器生成非常相似的代码,但是这看起来像是从C#2005编译器(8.00.50727.4927)中得到的一个回归,它为上面的代码生成了以下代码:

 A? a = x; B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y; C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z; 

我不知道这是不是由于types推理系统的额外的魔法

实际上,我现在将这个错误称为一个错误,更清晰的例子。 这依然成立,但双重评价当然不好。

看来好像A ?? B A ?? B被实现为A.HasValue ? A : B A.HasValue ? A : B 。 在这种情况下,也有很多铸造(在三元操作符的常规铸造之后)。 但是如果你忽略了这一切,那么这是基于如何实现的:

  1. A ?? B A ?? B扩展到A.HasValue ? A : B A.HasValue ? A : B
  2. A是我们的x ?? y x ?? y 。 展开到x.HasValue : x ? y x.HasValue : x ? y
  3. replace所有出现的A – > (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

在这里你可以看到x.HasValue被选中了两次,如果x ?? y x ?? y需要施放, x会施放两次。

我会把它简单地作为一个神器如何?? 被实现,而不是一个编译器错误。 带走:不要创build带有副作用的隐式转换运算符。

这似乎是一个编译器错误周围旋转?? 被执行。 外卖:不要嵌套凝聚式expression与副作用。

从我的问题历史中我可以看到,我不是一个C#专家,但是,我尝试了这一点,我认为这是一个错误….但作为一个新手,我不得不说,我不明白一切在这里,所以我会删除我的答案,如果我走了。

我已经通过制作一个处理同一场景的不同版本的程序来得出这个bug结论,但是要简单得多。

我正在使用三个空整数属性与后备存储。 我将每个设置为4,然后运行int? something2 = (A ?? B) ?? C; int? something2 = (A ?? B) ?? C;

( 完整代码在这里 )

这只读了A,没有别的。

对我来说这个说法对我来说应该是:

  1. 从括号开始,看A,返回A,如果A不为null,则结束。
  2. 如果A为空,则评估B,如果B不为空,则结束
  3. 如果A和B为空,则评估C.

所以,因为A不是空的,它只看A并结束。

在你的例子中,在第一个案例中放置一个断点表明x,y和z全部不为空,因此,我希望它们被视为与我不那么复杂的例子一样……但是我担心我太多了一个C#新手已经完全错过了这个问题的重点!