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
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:BA
是我们的x??y
。扩展到x.HasValue:x?y
(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))