空合并运算符的右结合如何表现?
Posted
技术标签:
【中文标题】空合并运算符的右结合如何表现?【英文标题】:How the right associative of null coalescing operator behaves? 【发布时间】:2011-09-08 10:43:05 【问题描述】:null 合并运算符是右结合的,表示形式为
第一??第二个??第三个
被评估为
第一?? (第二个??第三个)
基于上述规则,我认为下面的翻译是不正确的。
发件人:
Address contact = user.ContactAddress;
if (contact == null)
contact = order.ShippingAddress;
if (contact == null)
contact = user.BillingAddress;
收件人:
Address contact = user.ContactAddress ??
order.ShippingAddress ??
user.BillingAddress;
相反,我认为以下是正确的(如果我错了,请纠正我)
Address contact = (user.ContactAddress ?? order.ShippingAddress) ??
user.BillingAddress;
【问题讨论】:
鉴于空合并运算符的正确关联性,我相信如果不使用一对开闭括号()
来确定表达式评估的优先级,您的代码将永远无法成功转换为使用空合并运算符。
【参考方案1】:
这个规范实际上是自相矛盾的。
C# 4 规范的第 7.13 节指出:
null 合并运算符是右关联的,这意味着操作是从右到左分组的。例如,
a ?? b ?? c
形式的表达式被计算为a ?? (b ?? c)
。
另一方面,正如已经指出的那样,7.3.1 声称:
除了赋值运算符,所有二元运算符都是左结合的
我完全同意,对于简单的情况,如何进行分组并不重要......但在某些情况下,由于隐式类型转换会在操作数的情况下做一些有趣的事情,可能有不同的类型。
我会进一步考虑,ping Mads 和 Eric,并在深度 C# 的相关部分添加勘误表(这激发了这个问题)。
编辑:好的,我现在有一个例子,它确实很重要......并且空合并运算符绝对是正确-关联的,至少在MS C# 4 编译器。代码:
using System;
public struct Foo
public static implicit operator Bar(Foo input)
Console.WriteLine("Foo to Bar");
return new Bar();
public static implicit operator Baz(Foo input)
Console.WriteLine("Foo to Baz");
return new Baz();
public struct Bar
public static implicit operator Baz(Bar input)
Console.WriteLine("Bar to Baz");
return new Baz();
public struct Baz
class Test
static void Main()
Foo? x = new Foo();
Bar? y = new Bar();
Baz? z = new Baz();
Console.WriteLine("Unbracketed:");
Baz? a = x ?? y ?? z;
Console.WriteLine("Grouped to the left:");
Baz? b = (x ?? y) ?? z;
Console.WriteLine("Grouped to the right:");
Baz? c = x ?? (y ?? z);
输出:
Unbracketed:
Foo to Baz
Grouped to the left:
Foo to Bar
Foo to Bar
Bar to Baz
Grouped to the right:
Foo to Baz
换句话说,
x ?? y ?? z
行为与
相同x ?? (y ?? z)
但不与
相同(x ?? y) ?? z
我目前不确定为什么在使用 (x ?? y) ?? z
时会有两次从 Foo 到 Bar 的转换 - 我需要更仔细地检查一下...
编辑:我现在有 another question 来涵盖双重转换...
【讨论】:
+1,这当然是一个有趣的极端案例。在涉及“相同”的陈述中,我将更新我的答案,使其不那么“权威”。 @sixlettervariables:实际上,它变得更加有趣了。自定义转换发生了一些真的奇怪的事情。我稍后会发布一个问题... 在您的场景中检查 IL 生成以获取可为空的结构。x
的奇怪重用导致多个“Foo to Bar”。
我同意。我不明白为什么它认为这很有用。
好的,有一个更简单的测试,没有用户定义的转换。假设您有一个类Animal
,其派生类Cat
和Dog
。然后假设您有三个变量animal
、cat
和dog
,每个变量都有明显的编译时类型。然后使用表达式(animal ?? cat) ?? dog
会找到一个很好的通用基类型,而animal ?? (cat ?? dog)
将无法编译,因为猫不是狗,狗也不是猫。因此,要测试??
的关联性,您只需要查看animal ?? cat ?? dog
(不带括号)是否编译即可。【参考方案2】:
乔恩的回答是正确的。
要明确一点:C# 中的??
运算符是右结合。我刚刚检查了二元运算符解析器并验证了解析器将??
视为右关联。
正如 Jon 所指出的,规范说 ??
运算符是右关联的,并且除了赋值之外的所有二元运算符都是左关联的。由于规范自相矛盾,显然只有其中一个是正确的。我会将规范修改为:
除了简单赋值、复合赋值和空合并运算符外,所有二元运算符都是左结合的
更新:如 cmets 中所述,lambda 运算符 =>
也是右关联的。
【讨论】:
刚刚阅读another question 并意识到在某种意义上,=>
运算符也是右关联的。这是因为写Func<int, Func<int, int>> f = x => y => 0;
是合法的,所以右边有“隐含”的括号。但这当然不是“真正的”右结合性,因为左边的括号永远不会有意义。所以我不确定你是否认为这是一个必须添加到规范中的额外“例外”?
(addition) 但是对于x = y = 0
,把括号放在左边是没有意义的,所以真的没有区别。如果赋值运算符=
要称为右结合,那么lambda运算符=>
也是,不是吗?
@ntohl:三元运算符不是二元运算符。
@Jeppe:“lambda 运算符=>
”确实是右结合的,但我不认为它是运算符。
@Tom:您声称 lambda 运算符不是运算符?我想你会发现 lambda 运算符确实是一个运算符,因此得名“lambda 运算符”。【参考方案3】:
我看不出这两者有什么关系:
(a ?? b) ?? c
和
a ?? (b ?? c)
结果一样!
【讨论】:
a
、b
和 c
的类型相同时会这样做。但是假设类型是 A、B 和 C,并且存在从 A 到 B、A 到 C 和 B 到 C 的隐式转换……那么会发生什么?看我的回答:)
@Jon:我确实想到了这一点,然后再次查看了这个问题,并认为这无关紧要。但我确实为您深入研究并确定实际可观察到的关联性而鼓掌。【参考方案4】:
两者都按预期工作并且有效相同,因为表达式涉及简单类型(谢谢@Jon Skeet)。它将在您的示例中从左到右链接到第一个非空值。
将此运算符与operators of different precedence 结合使用时,优先级(感谢@Ben Voigt)更有趣:
value = A ?? B ? C : D ?? E;
基本上,关联性通过运算符优先级或用户引入的子表达式(括号)固有地表达。
【讨论】:
我仍然不认为关联性很重要,只有优先级。三元运算符具有更高的优先级,因此它是:A ?? (B ? C : D) ?? E
。而且不管是(A ?? (B ? C : D) )?? E
还是A ?? ((B ? C : D) ?? E)
OMFG,请不要在任何我必须阅读的代码中写这个!!!是的,我知道你只是在说明你的观点。
@sixletter: Oh drat: 我刚刚查了一下,并设法在浏览器选项卡之间忘记了:(。然后是 (A ?? B)? C : (D ?? E)
并且没有相同优先级的相邻操作,所以关联规则甚至没有应用。
@sixletter:除了混淆代码之外,没有其他方法可以说明优先级,而且混淆代码除了说明优先级(以及添加括号的动机,即使不是绝对必要的情况下)没有其他用途。
@sixlettervariables:当类型不同并且涉及转换时,它们实际上并不相同。看我的回答。以上是关于空合并运算符的右结合如何表现?的主要内容,如果未能解决你的问题,请参考以下文章