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

C# 奇怪的空合并运算符自定义隐式转换行为,c#,null-coalescing-operator,C#,Null Coalescing Operator,注意:这似乎已在 这个问题是在我写我的答案时提出的,其中谈到了 提醒一下,空合并运算符的思想是 x ?? y 首先计算x,然后: 如果x的值为空,则计算y,这是表达式的最终结果 如果x的值不为空,y不计算,x的值是表达式的最终结果,在转换为编译时类型y后(如有必要) 现在通常不需要转换,或者只是从可为null的类型转换为不可为null的类型-通常类型是相同的,或者只是从(比如)int?转换为int。但是,您可以创建自己的隐式转换运算符,并在必要时使用这些运算符 对于x??是的,我没有看到任

注意:这似乎已在

这个问题是在我写我的答案时提出的,其中谈到了

提醒一下,空合并运算符的思想是

x ?? y
首先计算
x
,然后:

  • 如果
    x
    的值为空,则计算
    y
    ,这是表达式的最终结果
  • 如果
    x
    的值不为空,
    y
    不计算,
    x
    的值是表达式的最终结果,在转换为编译时类型
    y
    后(如有必要)
现在通常不需要转换,或者只是从可为null的类型转换为不可为null的类型-通常类型是相同的,或者只是从(比如)
int?
转换为
int
。但是,您可以创建自己的隐式转换运算符,并在必要时使用这些运算符

对于
x??是的,我没有看到任何奇怪的行为。但是,使用
(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”,但我很困惑到底发生了什么

编辑:好的,这是一个更糟糕的例子,多亏了configurator的回答,这给了我更多的理由认为这是一个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()
在这里被调用了两次,这一事实令我非常惊讶-我看不出有任何理由对表达式进行两次求值。

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

另一个查找,如果您首先使用
a
b
都为null并返回
c
,那么它将生成一个快捷方式。但是,如果
a
b
为非空,则作为隐式转换到
b
的一部分,它会重新计算
a
b
中的哪一个为非空

根据C#4.0规范第6.1.4节:

  • 如果可空转换是从
    S?
    T?
    • 如果源值为
      null
      HasValue
      属性为
      false
      ),则结果为
      null
      类型
      T?
      的值
    • 否则,转换评估为从
      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;

我想知道这是否是由于类型推断系统的额外魔力造成的?

事实上,我现在用更清晰的例子将其称为bug。这仍然成立,但双重评价肯定不好

似乎
A??B
实现为A.HasValue?A:B
。在这种情况下,也有很多强制转换(在三元
?:
运算符的常规强制转换之后)。但如果您忽略了所有这些,那么根据它的实现方式,这是有意义的:

  • A??B
    扩展为A.HasValue?A:B
  • A
    是我们的
    x??y
    。扩展到
    x.HasValue:x?y
  • 替换所有出现的A->
    (x.HasValue:x?y)。HasValue?(x.HasValue:x?y):B
  • 在这里,您可以看到检查了两次
    x.HasValue
    ,如果
    x??y
    需要施法,
    x
    将施法两次

    我把它简单地说成是
    是如何实现的,而不是一个编译器错误。外卖:不要创建带有副作用的隐式铸造操作符


    这似乎是一个围绕
    如何实现的编译器错误。外卖:不要嵌套有副作用的聚合表达式。

    这肯定是一个bug

    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)
    
    这让我想到,每个
    合并表达式的第一部分都要计算两次。 这段代码证明了这一点:

    B? test= (X() ?? Y());
    
    产出:

    X()
    X()
    A to B (0)
    
    只有当表达式需要在两个可为null的类型之间进行转换时,才会出现这种情况;我尝试过各种排列,其中一个边是字符串,但没有一个导致这种行为。

    从我的问题历史中可以看出,我根本不是C#专家,但是,我尝试过,我认为这是一个错误。。。。但作为一个新手,我不得不说,我不理解这里发生的一切,所以如果我偏离了答案,我会删除我的答案

    我得出了这个
    bug
    的结论,我为您的程序制作了一个不同的版本,它处理相同的场景,但要简单得多

    我使用三个空整数属性作为备份存储。我将每个设置为4,然后运行
    int?某物2=(A??B)??C

    ()

    这只是读A,其他什么都没有

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

    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;
    
    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
    
    new int?(op_Implicit(temp2.Value))