空合并运算符的右结合如何表现?

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,其派生类CatDog。然后假设您有三个变量animalcatdog,每个变量都有明显的编译时类型。然后使用表达式(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)

结果一样!

【讨论】:

abc 的类型相同时会这样做。但是假设类型是 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:当类型不同并且涉及转换时,它们实际上并不相同。看我的回答。

以上是关于空合并运算符的右结合如何表现?的主要内容,如果未能解决你的问题,请参考以下文章

C#语法糖空合并运算符??和空合并赋值运算符 ??=

F#中的空合并运算符?

可能重载空合并运算符?

空合并运算符是不是有“对立面”? (……任何语言?)

VB.NET 空合并运算符? [复制]

来自'??' 的 C# 类型推断(“var”)赋值空合并运算符