当最后一个条件调用有条件时,为什么C#编译器会删除方法调用链?


69

考虑以下类别:

public class A {
    public B GetB() {
        Console.WriteLine("GetB");
        return new B();
    }
}

public class B {
    [System.Diagnostics.Conditional("DEBUG")]
    public void Hello() {
        Console.WriteLine("Hello");
    }
}

现在,如果我们以这种方式调用方法:

var a = new A();
var b = a.GetB();
b.Hello();

在发布版本中(即,无DEBUG标志),我们只会看到GetB控制台上打印的内容,因为Hello()编译器会省略对的调用。在调试版本中,两个打印都会出现。

现在让我们链接方法调用:

a.GetB().Hello();

调试版本中的行为不变。但是,如果未设置标志,则会得到不同的结果:两个调用都被省略,并且控制台上没有打印内容。快速浏览一下IL会发现整个行没有被编译。

根据针对C#最新ECMA标准(ECMA-334,即C#5.0),Conditional属性置于方法上如下(强调我的意思):

如果在调用点定义了一个或多个与其相关的条件编译符号,则包括对条件方法的调用,否则将省略该调用。(第22.5.3节)

这似乎并不表示应忽略整个链条,因此是我的问题。话虽如此,MicrosoftC#6.0规范草案提供了更多细节:

如果定义了该符号,则包括该呼叫;否则,将忽略该呼叫(包括对接收方的评估和该呼叫的参数)。

没有对调用的参数进行评估的事实有据可查,因为这是人们#if在功能体内使用此功能而不是使用伪指令的原因之一。但是,有关“接收者评估”的部分是新的-我似乎无法在其他地方找到它,并且确实可以解释上述行为。

有鉴于此,我的问题是:在这种情况下,C#编译器不进行评估的原理什么a.GetB() 根据条件调用的接收者是否存储在临时变量中,它的行为是否真的有所不同?


4
这只是一个猜测,但是我认为由于您B在方法链版本中未保留对它的引用,因此编译器会忽略它的创建,因为它错误地“认为”您只想调用该Hello()方法。好问题!
Zohar Peled

@ZoharPeled谢谢!稍后我会注意到C#6.0草案中增加的细节,但是此行为早于该草案。它似乎确实表明这是预期的行为,所以我现在对此最主要的理由是感兴趣:为什么要这样做,为什么直到最近才对其进行记录。
基里奥(Kyrio)

2
如果您考虑将其this作为附加的隐藏方法参数,它会更有意义。它只是得到与所有其他参数相同的处理。
卢卡斯Trzesniewski '18年

@LucasTrzesniewski这是一个合理的解释-它可能会比目前正在投票赞成的答案更好。
基里奥(Kyrio)

1
我暂时没有打开Visual Studio来查看-但是如果在生产线上有某种视觉指示a.GetB().Hello();表明它受到这种消失的影响,那肯定会很不错。因为否则语义对于读者来说是相当不可见的。
davidbak

Answers:


13

我进行了一些挖掘,发现C#5.0语言规范实际上确实已经在第424页上的17.4.2的Conditional属性中包含您的第二个引号。

马克·格雷韦尔(Marc Gravell)的答案已经表明,这种行为是有意的,在实践中是什么意思。您还询问了基本原理背后却似乎被去除开销马克的被提及的不满。

也许你想知道为什么其视为可以消除的开销?

a.GetB().Hello(); 在您的情况下根本不会被调用 Hello()被忽略的话,其面值似乎奇怪。

我不知道该决定背后的理由,但我发现了一些合理的推理依据。也许它也可以帮助您。

方法链仅当每个先前的方法都有返回值时,才可以进行。当您想对这些值做某事时,这很有意义,即a.GetFoos().MakeBars().AnnounceBars();

如果你有一个功能,只某些操作而不返回值您不能在其后面链接某些东西,而可以将其放在方法链的末尾,就像您的条件方法一样,因为它必须具有返回类型void。

还要注意,前面的方法调用的结果丢弃了,因此在您的示例中,执行此语句后a.GetB().Hello();的结果GetB()没有理由继续存在。基本上,您暗示GetB()只需要使用的结果Hello()

如果Hello()省略了为什么要GetB()那么呢?如果您忽略Hello()了行,则将其归结为a.GetB();没有任何分配,并且许多工具会警告您您没有使用返回值,因为这很少是您想要执行的操作。

您似乎对此方法不满意的原因不仅是尝试执行返回某个值所需的操作,而且还产生副作用,即I / O。如果您确实有一个纯函数,实际上没有理由GetB()忽略后续调用,即,如果您不打算对结果做任何事情。

如果将的结果分配给GetB()变量,则它本身就是一条语句,无论如何都会执行。所以这种推理解释了为什么

var b = a.GetB();
b.Hello();

Hello()当使用方法链接时,只省略对的调用,而整个链则被忽略。

您也可以在完全不同的地方看待以获得更好的视角:C#6.0中引入的空条件运算符elvis运算符 ?。尽管对于带有null检查的更复杂的表达式来说,它只是语法糖,但它允许您构建类似于方法链之类的东西,并可以选择基于null检查进行短路。

例如,GetFoos()?.MakeBars()?.AnnounceBars();如果先前的方法不返回null,则它将到达终点,否则将省略后续的调用。

这可能是反直觉的,但试试你的情况想象成这个倒数:编译器之前忽略你的电话Hello()在你的a.GetB().Hello();链,因为你没有达到链的末端反正。


免责声明

所有这些都是扶手椅式的推理,因此,请与猫王操作员进行一番比较,然后撒一粒盐。


1
好挖。有关接收器的注释首先出现在C#4规范中。
埃里克·利珀特

2
尽管这似乎确实可以回答问题,但所揭示的逻辑令人恐惧。链接或不链接方法的想法可能存在语义差异……我认为我需要一项新工作。
NPSF3000 '18

@ NPSF3000线a.GetB().Hello();没有返回值,而忽略Hello()a.GetB();忽然一个返回值,只是被扔掉。然而,你的日常方法链会更喜欢var bars = GetFoos()?.MakeBars()在那里MakeBars()有一个返回值。如果这种短路或您手动删除?.MakeBars(),即使结果可能会更改,您仍然会得到有效的分配。因此请记住,这是一个极端的情况-尽管很奇怪。
索伦D.Ptæus

2
这可能很偷偷摸摸。更明确地说,如果您使用GetB而不是GetB myObject.DoImportantStuff().DoDispensableStuff()。我不希望方法DoImportantStuff被丢弃,因为DoDispensableStuff是有条件的。而且,和之间的行为取决于构建目标myObject.DoImportantStuff().DoDispensableStuff()myObject.DoImportantStuff()?.DoDispensableStuff()不同,这可能非常难以检测。
Tichau

62

归结为以下短语:

(包括接收方评估和呼叫参数)被省略。

在表达式中:

a.GetB().Hello();

“接收者的评价”是:a.GetB()。因此:按照规范被省略,这是一个有用的技巧[Conditional],可以避免未使用的东西的开销。当您将其放入本地时:

var b = a.GetB();
b.Hello();

那么“接收者的评估”仅是local b,但原始var b = a.GetB();评估仍将进行评估(即使将localb最终删除)。

可能会带来意想不到的后果,因此:请[Conditional]谨慎使用。但是原因是可以轻松添加和删除日志记录和调试之类的内容。请注意,如果天真地对待参数,可能会出现问题:

LogStatus("added: " + engine.DoImportantStuff());

和:

var count = engine.DoImportantStuff();
LogStatus("added: " + count);

如果被标记,则可能会非常不同-结果是您的实际“重要内容”没有完成。LogStatus[Conditional]


我确实在使用此功能来进行调试,同时避免了发行版的开销。但是,如果您仔细看一下我的问题,您会发现,由于我注意到6.0规范中增加了详细信息,因此我知道这是(现在?)预期的行为,并且我对其中的原因更感兴趣。这是C#中条件编译的重要副作用,以前没有记载(据我所知,忽略参数是已知的事实)。不过,感谢您对此提供了更多的见识-这对以后的读者来说应该会很有用!
基里奥(Kyrio)

7
@Kyrio我觉得它一直很乖呀,虽然-和已打算做是对的。因此,关键是:该规范现在变得更加明显
Marc Gravell

4
我不确定我们如何确定这是预期的,但是如果我们假设是这样,那仍然不能真正回答我的问题。
基里奥(Kyrio)

也许埃里克(Eric)可以对此有所了解。
JonH

1
马克与我同在。并非如此,但埃里克(Eric)总是找到最佳答案。当我发表评论时,我敢打赌自己会发现它而不会对其进行ping操作。果然我赢了这个赌注...最近很幸运。告诉你...这不是第一次!
JonH '18

19

根据条件调用的接收者是否存储在临时变量中,它的行为是否真的有所不同?

是。

a.GetB()在这种情况下,C#编译器不进行评估的原理是什么?

马克和索伦的答案基本上是正确的。这个答案只是为了清楚地记录时间表。

  • 该功能是在1999年设计的,该功能的目的始终是删除整个语句。
  • 2003年的设计说明表明,设计团队当时意识到这一点尚不清楚。直到现在,规范仅指出不会对参数进行求值。我注意到,该规范犯了一个普遍错误,即将参数称为“参数”,尽管当然可以假设它们的意思是“实际参数”而不是“形式参数”。
  • 在这一点上,应该创建一个工作项来修复ECMA规范。显然,这从未发生过。
  • 更正的文本首次出现在任何C#规范中的都是C#4.0规范,我认为是2010年。(我不记得这是我的更正之一,还是别人发现了它。)
  • 如果2017年ECMA规范不包含此更正,那么这是一个错误,应在下一版本中修复。我想,迟到15年总比没有好。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.