好奇的空合并运算符自定义隐式转换行为
注意:这似乎已经在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; } }
所以我们有三个自定义值typesA
, B
和C
,转换从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
使用它将产生一个快捷方式,如果a
和b
都为空,并返回c
。 然而,如果a
或b
非空,则在返回a
或b
中的非空值之前,将其重新评估为隐式转换为B
一部分。
从C#4.0规范,第6.1.4节:
- 如果可空转换来自
S?
T?
:
- 如果源值为
null
(HasValue
属性为false
),那么结果是typesT?
的null
值 。- 否则,转换被评估为从
S?
展开S?
到S
,然后是从S
到T
的底层转换,然后是从T
到T?
的包装(§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
。 在这种情况下,也有很多铸造(在三元操作符的常规铸造之后)。 但是如果你忽略了这一切,那么这是基于如何实现的:
-
A ?? B
A ?? B
扩展到A.HasValue ? A : B
A.HasValue ? A : B
-
A
是我们的x ?? y
x ?? y
。 展开到x.HasValue : x ? y
x.HasValue : x ? y
- 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,没有别的。
对我来说这个说法对我来说应该是:
- 从括号开始,看A,返回A,如果A不为null,则结束。
- 如果A为空,则评估B,如果B不为空,则结束
- 如果A和B为空,则评估C.
所以,因为A不是空的,它只看A并结束。
在你的例子中,在第一个案例中放置一个断点表明x,y和z全部不为空,因此,我希望它们被视为与我不那么复杂的例子一样……但是我担心我太多了一个C#新手已经完全错过了这个问题的重点!