为什么C#编译器在最后一个方法调用是有条件的时删除了一串方法调用?

时间:2018-03-13 10:07:52

标签: c# conditional-compilation

考虑以下课程:

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,可以看出整行都没有编译。

根据latest ECMA standard for C#(ECMA-334,即C#5.0),Conditional属性放置在方法上时的预期行为如下(强调我的):

  

如果一个或多个相关的条件编译符号是,则包含对条件方法的调用   在通话点定义,否则呼叫被省略。 (§22.5.3)

这似乎并不表示应该忽略整个链,因此我的问题。话虽如此,C# 6.0 draft spec from Microsoft提供了更多细节:

  

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

未评估调用参数的事实已被充分记录,因为这是人们使用此功能而不是函数体中的#if指令的原因之一。然而,关于“接收器评估”的部分是新的 - 我似乎无法在其他地方找到它,它似乎解释了上述行为。

鉴于此,我的问题是:在这种情况下,C#编译器没有评估 a.GetB() 背后的理由是什么?它是否真的表现不同关于条件调用的接收者是否存储在临时变量中?

3 个答案:

答案 0 :(得分:62)

归结为这句话:

  

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

在表达式中:

a.GetB().Hello();

"接收器的评估"是:a.GetB()。所以:根据规范省略了,这是一个有用的技巧,允许[Conditional]避免未使用的开销。当你把它放到本地时:

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

然后对接收器"进行评估。只是本地b,但原始var b = a.GetB();仍在评估中(即使本地b最终被删除)。

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

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

var count = engine.DoImportantStuff();
LogStatus("added: " + count);
如果LogStatus被标记为[Conditional],则

可能非常 - 结果是您的实际"重要内容"没有完成。

答案 1 :(得分:19)

  

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

  

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

Marc和Søren的答案基本上是正确的。这个答案只是为了清楚地记录时间表。

  • 该功能是在1999年设计的,该功能的目的始终是删除整个声明。
  • 2003年的设计说明表明,设计团队当时意识到规格在这一点上并不清楚。到目前为止,规范仅指出参数不会被评估。我注意到规范使得调用参数“参数”的常见错误,但当然可以假设它们意味着“实际参数”而不是“形式参数”。
  • 应该创建一个工作项来修复ECMA规范;显然从未发生过。
  • 第一次更正文本出现在任何C#规范中的是C#4.0规范,我相信是2010年。(我不记得这是否是我的更正之一,或者是否有其他人发现它。)
  • 如果2017 ECMA规范不包含此更正,那么这是一个错误,应该在下一个版本中修复。我想,迟到的时间比从未好过15年。

答案 2 :(得分:13)

我做了一些挖掘,发现C# 5.0 language specification确实已经包含了第424页 17.4.2条件属性部分中的第二个引用。

Marc Gravell’s answer已经表明此行为是有意的,在实践中意味着什么。 你还问过这个背后的理由,但似乎对Marc提到的删除开销不满意。

也许您想知道为什么它被视为可以删除的开销?

a.GetB().Hello();被忽略的情况下,

Hello()根本没有被调用在面值上看起来很奇怪。

我不知道决定背后的理由,但我发现了一些合理的推理。也许它也可以帮到你。

仅当前面的每个方法都有返回值时,才能使用

Method chaining。当你想对这些值做某事时,这是有道理的,即a.GetFoos().MakeBars().AnnounceBars();

如果你有一个只有某事而没有返回值的函数,你就不能将它链接在它后面但可以把它放在方法链的末尾,就像你的条件方法一样它必须有返回类型void。

另请注意,之前方法调用的结果会被抛弃,因此在您a.GetB().Hello();的示例中,您的结果来自GetB()执行此声明后没有理由生活。基本上,您暗示您需要GetB()的结果才能使用Hello()

如果省略Hello(),为什么需要GetB()呢?如果省略Hello(),您的行可以归结为a.GetB();而没有任何作业,许多工具会发出警告,说明您没有使用返回值,因为这很少是您想要做的事情。

你似乎对此无动于衷的原因是你的方法不仅是尝试做返回某个值所必需的,而且还有一个side effect,即I / O.如果你确实有一个pure function,如果省略后续调用,那么 就没有理由GetB(),即如果你不打算对结果做任何事情。

如果将GetB()的结果分配给变量,则这是一个自己的语句,无论如何都会执行。所以这个推理解释了为什么在

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

仅省略对Hello()的调用,而在使用方法链时,省略了整个链。

您还可以查看完全不同的地方以获得更好的视角:C#6.0中引入的null-conditional operatorelvis operator ?。虽然对于具有空值检查的更复杂表达式来说它只是语法糖,但它允许您构建类似于方法链的东西,并且可以选择基于空检查进行短路。

E.g。如果以前的方法不返回GetFoos()?.MakeBars()?.AnnounceBars();null只会到达它,否则后续调用将被省略。

这可能是违反直觉的,但请尝试将您的方案视为与此相反:编译器在Hello()链中的a.GetB().Hello();之前省略了您的调用,因为您没有到达链无论如何。

声明

这一直都是扶手椅的推理,所以请把这个和elvis操作员的比喻用一粒盐。