好奇的null-coalescing运算符自定义隐式转换行为

Jon*_*eet 535 c# null-coalescing-operator

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

写我的回答时,此问题出现了这一个,其中谈到了的关联性空合并运算符.

提醒一下,null-coalescing运算符的概念就是表单的表达式

x ?? y
Run Code Online (Sandbox Code Playgroud)

首先评估x,然后:

  • 如果值为xnull,y则进行求值,这是表达式的最终结果
  • 如果值x是非空,y评估,的值x是表达的最终结果,转换到编译时间类型的后y如果需要的话

现在通常不需要转换,或者它只是从可空类型到非可空类型 - 通常类型相同,或者只是从(比如说)int?int.但是,您可以创建自己的隐式转换运算符,并在必要时使用它们.

对于简单的情况x ?? y,我没有看到任何奇怪的行为.但是,(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;
    }
}
Run Code Online (Sandbox Code Playgroud)

因此,我们有三个自定义值类型,A,BC,与从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;
    }
}
Run Code Online (Sandbox Code Playgroud)

这个输出是:

Foo() called
Foo() called
A to int
Run Code Online (Sandbox Code Playgroud)

Foo()这里被调用两次这一事实对我来说非常令人惊讶 - 我看不出有任何理由要对表达式进行两次评估.

Eri*_*ert 415

感谢所有为分析此问题做出贡献的人.这显然是编译器错误.它似乎只发生在合并运算符的左侧有一个涉及两个可空类型的提升转换时.

我还没有确定哪里出错了,但在编译的"可空降低"阶段 - 在初步分析之后但在代码生成之前 - 我们减少了表达式

result = Foo() ?? y;
Run Code Online (Sandbox Code Playgroud)

从上面的例子到道德等价物:

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;
Run Code Online (Sandbox Code Playgroud)

显然这是不正确的; 正确的降低是

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;
Run Code Online (Sandbox Code Playgroud)

根据我迄今为止的分析,我最好的猜测是可空的优化器在这里发挥作用.我们有一个可以为空的优化器,它可以查找我们知道可空类型的特定表达式不可能为null的情况.考虑以下天真的分析:我们可以先说

result = Foo() ?? y;
Run Code Online (Sandbox Code Playgroud)

是相同的

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;
Run Code Online (Sandbox Code Playgroud)

然后我们可以这么说

conversionResult = (int?) temp 
Run Code Online (Sandbox Code Playgroud)

是相同的

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null
Run Code Online (Sandbox Code Playgroud)

但优化器可以介入并说"哇,等一下,我们已经检查过temp不是null;没有必要再次将它检查为null,因为我们正在调用一个提升的转换运算符".我们让他们优化它

new int?(op_Implicit(temp2.Value)) 
Run Code Online (Sandbox Code Playgroud)

我的猜测是,我们正在某处缓存的事实,优化的形式(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/

  • 现在我已经拥有了Roslyn的最终用户预览版,我可以确认它已经修复了.(它仍然存在于本机C#5编译器中.) (12认同)

con*_*tor 84

这绝对是一个错误.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码将输出:

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)
Run Code Online (Sandbox Code Playgroud)

这让我觉得每个??合并表达式的第一部分都会被评估两次.该代码证明了这一点:

B? test= (X() ?? Y());
Run Code Online (Sandbox Code Playgroud)

输出:

X()
X()
A to B (0)
Run Code Online (Sandbox Code Playgroud)

这似乎只在表达式需要两个可空类型之间的转换时才会发生; 我尝试了各种排列,其中一个边是一个字符串,但没有一个导致这种行为.

  • 哇 - 两次评估表达似乎非常错误.好眼力. (11认同)
  • 你的所有方法都应该输出"X()"吗?这使得告诉实际输出到控制台的方法有点困难. (8认同)
  • 我在我的问题中添加了一个稍微简单的"双重评估"示例. (2认同)
  • 看起来是`X() ?? Y()` 在内部扩展为 `X() != null ? X() : Y()`,因此它会被评估两次。 (2认同)

use*_*116 54

如果你看看为左分组案例生成的代码,它实际上做了类似this(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);
}
Run Code Online (Sandbox Code Playgroud)

另外发现,如果你使用 first它会生成一个快捷方式,如果都ab都为空和返回c.然而,如果a还是b不为null它重新评估a的隐式转换的一部分B返回其之前ab是非空.

从C#4.0规范,§6.1.4:

  • 如果可空转换是从S?T?:
    • 如果源值为null(HasValueproperty is false),则结果为nulltype 的值T?.
    • 否则,转换被评价为一个从展开S?S,随后从底层转换ST,随后从包裹(§4.1.10)TT?.

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


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;
Run Code Online (Sandbox Code Playgroud)

我想知道这是不是因为给类型推理系统增加了额外的魔力


Phi*_*eck 16

实际上,我现在称这是一个错误,更清楚的例子.这仍然有效,但双重评估肯定不好.

似乎A ?? B实现为A.HasValue ? A : B.在这种情况下,也有很多铸造(遵循三元?:运算符的常规铸造).但是如果你忽略了这一切,那么根据它的实现方式来说这是有意义的:

  1. A ?? B 扩展到 A.HasValue ? A : B
  2. A是我们的 x ?? y.扩展到x.HasValue : x ? y
  3. 替换所有出现的A - > (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

在这里你可以看到x.HasValue被检查两次,如果x ?? y需要施放,x将被施放两次.

我把它简单地作为一个如何??实现的工件,而不是编译器错误. Take-Away:不要创建带有副作用的隐式转换运算符.

它似乎是围绕如何??实现的编译器错误.外卖:不要将具有副作用的合并表达式嵌套.


Wil*_*Wil 10

从我的问题历史中可以看出,我根本不是C#专家,但是,我尝试了这个,我认为这是一个错误....但作为一个新手,我不得不说我不明白一切在这里,如果我离开,我将删除我的答案.

bug通过制作一个处理相同场景的程序的不同版本来得出这个结论,但更简单.

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

(完整代码在这里)

这只是读取A而不是其他内容.

对我来说,这句话对我来说应该是:

  1. 从括号开始,查看A,返回A并在A不为空时完成.
  2. 如果A为null,则计算B,如果B不为null则结束
  3. 如果A和B为空,则评估C.

所以,由于A不是null,它只查看A并完成.

在你的例子中,在First Case中放置一个断点表明x,y和z都不是null,因此,我希望它们与我不太复杂的例子一样对待....但我担心我太多了一个C#新手,完全错过了这个问题!

  • Jon的例子在某种程度上是一个模糊的角落,因为他使用的是一个可以为空的结构(一种与内置类型"类似"的值类型,如"int").他通过提供多个隐式类型转换将案例进一步推向了一个不起眼的角落.这要求编译器在检查"null"时更改数据的*类型*.正是由于这些隐式类型转换,他的例子与你的不同. (5认同)