为什么C#编译器在最后一个条件调用时会删除一系列方法调用?

Kyr*_*rio 69 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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

var a = new A();
var b = a.GetB();
b.Hello();
Run Code Online (Sandbox Code Playgroud)

在发布版本中(即没有DEBUG标志),我们只会GetB在控制台上看到打印,因为Hello()编译器会忽略调用.在调试版本中,两个打印都会出现.

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

a.GetB().Hello();
Run Code Online (Sandbox Code Playgroud)

调试版本中的行为保持不变; 但是,如果未设置标志,我们会得到不同的结果:两个调用都被省略,并且控制台上没有打印.快速浏览一下IL可以看出整行都没有编译过.

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

如果在呼叫点定义了一个或多个相关的条件编译符号,则包括对条件方法的调用,否则省略该调用.(§22.5.3)

这似乎并不表示整个链应该被忽略,因此我的问题.话虽如此,微软C#6.0草案规范提供了更多细节:

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

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

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

Mar*_*ell 62

它归结为这句话:

(包括接收机的评估和呼叫的参数)被省略.

在表达式中:

a.GetB().Hello();
Run Code Online (Sandbox Code Playgroud)

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

var b = a.GetB();
b.Hello();
Run Code Online (Sandbox Code Playgroud)

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

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

LogStatus("added: " + engine.DoImportantStuff());
Run Code Online (Sandbox Code Playgroud)

和:

var count = engine.DoImportantStuff();
LogStatus("added: " + count);
Run Code Online (Sandbox Code Playgroud)

如果被标记可能会有很大不同- 结果是你的实际"重要的东西"没有完成.LogStatus[Conditional]

  • @Kyrio我认为它总是*表现出那种方式,但是 - 并且*意图*表现得那样.所以关键点在于:规范现在使其更加明显 (7认同)
  • 我不确定我们怎么能确定这是有意的,但如果我们认为是,那仍然没有真正回答我的问题. (4认同)

Eri*_*ert 19

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

是.

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

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

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


Sør*_*æus 13

我做了一些挖掘,发现C#5.0语言规范实际上已经包含了第424.2"条件"属性第424页中的第二个引用.

Marc Gravell的回答已经表明这种行为是有意的,在实践中意味着什么.您还询问了这背后的基本原理,但似乎对Marc提到的消除开销不满意.

也许你想知道为什么它被认为是可以删除的开销?

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();
Run Code Online (Sandbox Code Playgroud)

Hello()在使用方法链时,只省略了调用,省略了整个链.

您还可以查看完全不同的地方以获得更好的视角:C#6.0中引入的空条件运算符elvis运算符 ?.虽然它只是具有空检查的更复杂表达式的语法糖,但它允许您构建类似于方法链的东西,并且可以选择基于空检查进行短路.

GetFoos()?.MakeBars()?.AnnounceBars();如果前面的方法没有返回null,Eg 只会到达它的末尾,否则后续的调用将被省略.

这可能是违反直觉的,但请尝试将您的场景视为与此相反:编译器会Hello()在您的a.GetB().Hello();链之前省略您的调用,因为您还没有到达链的末尾.


放弃

这一切都是扶手椅的推理,所以请把这个与elvis操作员的类比与一粒盐.

  • 虽然这似乎回答了这个问题,但它揭示的逻辑却令人恐惧.链接或不链接方法的想法可能有语义差异......我想我需要一份新工作. (2认同)
  • 这可能非常狡猾.更明确一点,如果不是GetB,你有`myObject.DoImportantStuff().DoDispensableStuff()`.我不希望丢弃方法DoImportantStuff,因为DoDispensableStuff是有条件的.另外,`myObject.DoImportantStuff().DoDispensableStuff()`和`myObject.DoImportantStuff()?. DoDispensableStuff()`之间的行为根据构建目标而不同,这可能非常复杂. (2认同)