CSharp - 自定义隐式转换运算符的行为

  显示原文与译文双语对照的内容

注:这似乎是固定在 Roslyn

这个问题在写我对的回答时出现了一个,它谈到 null-coalescing操作符的结合。

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

 
x?? y

 

首先评估 x,然后:

  • 如果 x的值为空,则计算 y,这是表达式的最终结果
  • 如果 x的值为 non-null,则 y 为 ,x的值是表达式的最终结果,在转换到compile-time类型的y 之后,如果有必要

现在通常不需要转换,或只是一个可空类型non-nullable——通常是相同的类型,或者只是从( say ) 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;
 }
}

所以我们有三个自定义值类型 ABC,从A 到,A 到C,和的转换。

我可以理解第二个案例和第三个案例。 但是为什么在第一个例子中还需要一个额外的A 转换? 尤其是,我会真的有预期的首例和第二种情况是同一件事 — —它毕竟只提取到一个本地变量,表达式。

任何有问题的人? 我非常hesistant哭"Bug"谈到 C# 编译器,但是我难倒了,不知道是怎么回事。

编辑:好的,这里是一个nastier的例子,这是由配置器的回答,它给我进一步的理由,认为它是一个 Bug 。 编辑:样例现在甚至不需要两个null-coalescing操作符。。


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() 被调用两次这是让我非常惊讶,我看不出任何表情的原因评估两次。

时间:

感谢分析这个问题的每个人。 显然是一个编译器 Bug 。 只有在合并运算符的left-hand边上有涉及两个可以为空类型的提升转换时才会出现。

我还没有确定准确的事情出错,但是在编译的"可以为空值的降低"阶段--初步分析后但在代码生成--我们减少了表达式


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;

根据我的分析,我的最佳猜测是,可以空的优化器在 Rails 上。 我们有一个可以空的优化器,该优化器查找可以为空类型的特定表达式不能为空的情况。 考虑以下幼稚分析: 我们可以先说


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)) 

我的猜测是,我们在缓存的优化的形式这一事实 (int?)Foo()new int?(op_implicit(Foo().Value)) 但实际上并不是我们想要的优化形式,我们希望foo的优化形式( ) -replaced-with-temporary-and-then-converted 。

Many Bug in C# compiler are a result of bad caching decisions. 给聪明人一个词: 每次你使用缓存一个事实后,你可能会创建一个改变矛盾应该一些相关知识。 在这种情况下,更改了post初始分析的相关内容是,对 Foo()的调用应该始终实现为临时获取。

我们对 C# 3.0中的可以空重写传递进行了大量重组。 Bug 在 C# 3.0和 4.0中重现,但不是 C# 2.0,这意味着 Bug 可能是我的错。 抱歉 !

我将得到一个 Bug 进入数据库,我们将看到我们是否能得到这个语言的未来版本。 再次感谢你的分析;它非常有用 !

这绝对是一个 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)

这似乎发生只有当表达式需要两个可空类型之间的转换;我已经尝试了各种排列的两边是一个字符串,并没有这种行为引起的。

如果你查看Left-grouped案例生成的代码,它实际上会执行如下( 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 。 但是,如果 a 或者 b 是 non-null,则在返回 a 或者 b 之前,将 re-evaluates a 作为隐式转换到 b的一部分。

从 C# 4.0规范,§6.1.4:

  • 如果可以空转换是从 S?T?:
    • 如果源值 null ( HasValue 属性是 false ), 结果是 T?null 价值类型。
    • 否则,转换被评估为从 S?S的展开,之后是从 ST的底层转换,之后是从 TT?的换行( §4.1.10 ) 。

这似乎解释了第二个unwrapping-wrapping组合。


C# 2008和 2010编译器生成非常相似的代码,但是这看起来像是 C# 2005编译器( 8.00.507 27.492 7 )的回归,它为上面的代码生成了以下代码:


A? a = x;
B? b = a.HasValue? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue? new C?(b.GetValueOrDefault()) : z;

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


Console.WriteLine("First case");
 A? a2 = a;
 B? b2 = a2.HasValue? new B?(a.Value) : b;
 if (b2.HasValue)
 {
 a2 = a;
 B? b3 = a2.HasValue? new B?(a.Value) : b;
 new C?(b3.Value);
 }
 Console.WriteLine("Second case");
 a2 = a;
 B? b4 = a2.HasValue? new B?(a.Value) : b;
 b2 = b4;
 C? arg_FB_0 = b2.HasValue? new C?(b4.Value) : c;
 Console.WriteLine("Third case");
 a2 = a;
 b2 = (a2.HasValue? new B?(a.Value) : b);
 C? c3 = new C?(b2.HasValue? b2.GetValueOrDefault() : c2);

答案在反编译代码中。 它正在计算第一个表达式两次。 我看不出有什么理由重新输入表达式。 我称之为 Bug 。

实际上,我现在称之为 Bug,更清晰的例子。 这仍然存在,但是double-evaluation当然不是好的。

似乎 A?? B 被实现为 A.HasValue? A : B 在这种情况下,有太多的( 按照规则的?: 操作符进行常规铸造) 。 但是如果你忽略了所有这些,那么根据它的实现方式,这将有意义:

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

你可以看到,x.HasValue 被检查两次,如果 x?? y 需要铸造,x 将被强制转换两次。

我将把它简单地看作是如何实现 ??的工件,而不是编译器 Bug 。 Take-Away: 不创建带有副作用的隐式强制转换运算符。

它似乎是一个编译器 Bug 围绕如何实现 ?? 。 Take-away: 不使用副作用嵌套表达式。

我不是一个 C# 专家,因为你可以从我的问题历史中看到,但是我尝试了这个,我认为它是一个错误。 但作为一个新手,我必须说我不理解这里的一切,所以如果我离开,我将删除我的答案。

我通过制作一个不同的程序来得出这个 bug的结论,它处理相同的场景,但是不太复杂。

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

( 完整代码这里是 )

这只是读了一个,而没有别的。

这个声明对我来说应该是:

  1. 在括号中开始,查看一个,返回一个,如果不是空,则返回 finish 。
  2. 如果为空,请评估,如果为空则完成 finish
  3. 如果A 和B 为空,则评估C 。

因此,作为一个非 null,它只看一个并完成。

在你的示例中,在第一个例子中放置断点表明,x,y 和z 都不是空的,因此,我期望它们被视为与我的不复杂的示例相同。 但是我害怕我太多的C# 新手了,我完全忽略了这个问题 !

...