注意:这似乎已在Roslyn中修复
写我的回答时,此问题出现了这一个 ,其中谈到了的关联性空合并运算符 。
提醒一下,null-coalescing运算符的概念就是表单的表达式
x ?? y
首先评估x ,然后:
如果x值为null,则计算y ,这是表达式的最终结果
如果x值为非null, 则不计算y ,并且x的值是表达式的最终结果,在必要时转换为编译时类型的y
现在通常不需要转换,或者它只是从可空类型到非可空类型 - 通常类型是相同的,或者只是来自(比如说) int? 到int 。 但是,您可以创建自己的隐式转换运算符,并在必要时使用它们。
对于x ?? y的简单情况 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#编译器时,我非常痴迷于“bug”,但我对于发生了什么感到难过......
编辑:好的,这是一个更糟糕的例子,感谢配置器的答案,这让我有更多的理由认为它是一个错误。 编辑:样本现在甚至不需要两个空合并运算符...
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()在这里被调用两次的事实对我来说非常令人惊讶 - 我看不出有任何理由让表达式被评估两次。
#1楼
如果你看看为左分组案例生成的代码,它实际上做了类似的事情( 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都为null并返回c 。 然而,如果a或b是非空它重新评估a作为隐式转换的部分B返回其前a或b是非空的。
从C#4.0规范,§6.1.4:
如果可空转换来自S? 到T? :
如果源值为null ( HasValue属性为false ),则结果为类型T?的null值T? 。
否则,转换被评估为从S?展开S? 到S ,然后是从S到T的底层转换,接着是从T到T?的包装(§4.1.10) T? 。
这似乎解释了第二个展开包装组合。
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;
我想知道这是不是因为给类型推理系统增加了额外的魔力 ?
#2楼
实际上,我现在称这是一个错误,更清楚的例子。 这仍然有效,但双重评估肯定不好。
好像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
替换所有出现的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将被施放两次。
我只是把它作为一个神器如何
??
实现,而不是编译器错误。 Take-Away:不要创建带有副作用的隐式转换运算符。
它似乎是一个围绕如何编译器的bug ?? 实施。 外卖:不要将具有副作用的合并表达式嵌套。
#3楼
这绝对是一个错误。
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)
这让我觉得每个人的第一部分?? coalesce表达式被评估两次。 该代码证明了这一点:
B? test= (X() ?? Y());
输出:
X()
X()
A to B (0)
这似乎只在表达式需要两个可空类型之间的转换时才会发生; 我尝试了各种排列,其中一个边是一个字符串,但没有一个导致这种行为。
#4楼
从我的问题历史中可以看出,我根本不是C#专家,但是,我尝试了这个,我认为这是一个错误....但作为一个新手,我不得不说我不明白一切在这里,如果我离开,我将删除我的答案。
我通过制作一个处理相同场景的程序的不同版本来解决这个bug ,但复杂程度要低得多。
我使用三个空整数属性与后备存储。 我将每个设置为4,然后运行int? something2 = (A ?? B) ?? C; int? something2 = (A ?? B) ?? C;
这只是读取A而不是其他内容。
对我来说,这句话对我来说应该是:
从括号开始,查看A,返回A并在A不为空时完成。
如果A为null,则计算B,如果B不为null则结束
如果A和B为空,则评估C.
所以,由于A不是null,它只查看A并完成。
在你的例子中,在First Case中放置一个断点表明x,y和z都不是null,因此,我希望它们与我不那么复杂的例子一样对待....但我担心我太多了一个C#新手,完全错过了这个问题!
#5楼
感谢所有为分析此问题做出贡献的人。 这显然是编译器错误。 它似乎只发生在合并运算符的左侧有一个涉及两个可空类型的提升转换时。
我还没有确定哪里出错了,但在编译的“可空降低”阶段 - 在初步分析之后但在代码生成之前 - 我们减少了表达式
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的情况。 考虑以下天真的分析:我们可以先说
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不是null;没有必要再次将它检查为null,因为我们正在调用一个提升的转换运算符”。 我们让他们优化它
new int?(op_Implicit(temp2.Value))
我的猜测是我们在(int?)Foo()缓存这样一个事实,即(int?)Foo()的优化形式是new int?(op_implicit(Foo().Value))但实际上并不是我们想要的优化形式; 我们想要Foo()的优化形式 - 替换为临时和然后转换。
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/