合并运算符自定义隐式转换行为

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

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

需要提醒的是,空合并运算符的思想是表达式

x ?? y

首先评估x ,然后:

  • 如果x值为空,则评估y ,这是表达式的最终结果
  • 如果x的值非空,则不计算y ,并且在必要时将y转换为编译时类型之后, x的值是表达式的最终结果
  • 现在通常不需要转换,或者它只是从可空类型转换为不可空的类型 - 通常类型是相同的,或者仅仅来自(比如说) 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;
        }
    }
    

    所以我们有三种自定义值类型ABC ,转换从A到B,A到C和B到C.

    我可以理解第二种情况和第三种情况......但为什么在第一种情况下会出现额外的A到B转换? 特别是,我真的期望第一种情况和第二种情况是相同的 - 毕竟只是将表达式提取到局部变量中。

    任何参与者正在发生什么? 当谈到C#编译器时,我对于“错误”非常抱歉,但我很难理解发生了什么......

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

    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()在这里被调用两次这一事实对我来说是非常令人惊讶的 - 我无法看到表达式被评估两次的任何理由。


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

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

    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的类型的特定表达式不可能为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”。 我们希望他们能够优化它

    new int?(op_Implicit(temp2.Value)) 
    

    我的猜测是,我们在某处缓存(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/


    这绝对是一个错误。

    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)
    

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


    如果您查看左分组情况的生成代码,它实际上会做类似这样的事情( 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中的非空值之前重新评估a作为隐式转换为B一部分。

    从C#4.0规范,第6.1.4节:

  • 如果可空转换来自S? T?
  • 如果源值为nullHasValue属性为false ),那么结果是类型T?nullT?
  • 否则,转换被评估为从S?展开S?S ,然后是从ST的底层转换,然后是从TT?的换行(第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;
    

    我想知道这是不是由于类型推理系统的额外魔法?

    链接地址: http://www.djcxy.com/p/9937.html

    上一篇: coalescing operator custom implicit conversion behaviour

    下一篇: Coalesce function for PHP?