当最后一个方法调用是有条件的时,为什么 C# 编译器会删除一连串的方法调用?
Why does the C# compiler remove a chain of method calls when the last one is conditional?
考虑以下 类:
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();
调试版本中的行为没有改变;但是,如果未设置标志,我们会得到不同的结果:both 调用都被省略,并且控制台上没有打印。快速查看 IL 显示整行未编译。
根据 latest ECMA standard for C#(ECMA-334,即 C# 5.0),将 Conditional
属性放置在方法上时的预期行为如下(强调我的):
A call to a conditional method is included if one or more of its associated conditional compilation symbols is
defined at the point of call, otherwise the call is omitted. (§22.5.3)
这似乎并不表示应该忽略整个链,因此我的问题。也就是说,C# 6.0 draft spec from Microsoft 提供了更多细节:
If the symbol is defined, the call is included; otherwise, the call (including evaluation of the receiver and parameters of the call) is omitted.
调用的参数未被评估这一事实有据可查,因为这是人们使用此功能而不是函数主体中的 #if
指令的原因之一。然而,关于 "evaluation of the receiver" 的部分是新的 - 我似乎无法在其他地方找到它,它似乎确实解释了上述行为。
鉴于此,我的问题是:在这种情况下,C# 编译器不评估 a.GetB()
背后的基本原理是什么? 根据条件调用的接收者是否存储在临时变量中,它的行为真的会有所不同吗?
归结为短语:
(including evaluation of the receiver and parameters of the call) is omitted.
表达式中:
a.GetB().Hello();
"evaluation of the receiver" 是:a.GetB()
。所以:根据规范,它被省略了,这是一个有用的技巧,允许[Conditional]
避免未使用的东西的开销.当你把它放到本地时:
var b = a.GetB();
b.Hello();
那么 "evaluation of the receiver" 只是本地 b
,但是原始 var b = a.GetB();
仍然被评估(即使本地 b
最终被删除)。
这 可能会 产生意想不到的后果,因此:请谨慎使用 [Conditional]
。但原因是可以轻松添加和删除诸如日志记录和调试之类的东西。请注意,如果处理得当,参数可能 也 有问题:
LogStatus("added: " + engine.DoImportantStuff());
和:
var count = engine.DoImportantStuff();
LogStatus("added: " + count);
如果 LogStatus
被标记为 [Conditional]
, 可能 非常 不同 - 结果是您的实际 "important stuff" 没有完成。
我做了一些挖掘,发现 C# 5.0 language specification 实际上已经包含了第 424 页的 17.4.2 条件属性 部分中的第二个引号。
已经表明这种行为是有意为之的,它在实践中意味着什么。
您还询问了这背后的 理由,但似乎对 Marc 提到的消除开销不满意。
也许您想知道为什么它被认为是可以删除的开销?
a.GetB().Hello();
在您的场景中根本没有被调用而 Hello()
被省略可能看起来很奇怪。
我不知道该决定背后的理由,但我自己找到了一些似是而非的推理。也许它也能帮到你。
Method chaining 仅当每个先前的方法都有一个 return 值时才有可能。当您想对这些值执行某些操作时,这很有意义,即 a.GetFoos().MakeBars().AnnounceBars();
如果你有一个函数只 做 没有 return 值的东西,你不能在它后面链接一些东西,但可以把它放在方法链的末尾,就像您的条件方法一样,因为它必须具有 return 类型 void.
另请注意,先前方法调用的 结果 被 丢弃 ,因此在您的 a.GetB().Hello();
示例中GetB()
的结果在执行此语句后没有理由存在。基本上,你 暗示 你只需要 GetB()
的结果来使用 Hello()
.
如果 Hello()
被省略了,为什么还需要 GetB()
呢?如果您省略 Hello()
,您的行将归结为 a.GetB();
而没有任何赋值,并且许多工具会发出警告,提示您没有使用 return 值,因为这很少是您想要做的事情。
你似乎对此不满意的原因是你的方法不仅试图做return某个特定值所必需的事情,而且你还有一个side effect, namely I/O. If you did instead have a pure function 真的没有理由 GetB()
如果您省略后续调用,即如果您不打算对结果做任何事情。
如果您将 GetB()
的结果赋值给一个变量,这本身就是一条语句,无论如何都会被执行。所以这个推理解释了为什么在
var b = a.GetB();
b.Hello();
仅省略了对 Hello()
的调用,而在使用方法链接时省略了整个链。
您也可以查看完全不同的地方以获得更好的视角:C# 6.0 中引入的 null-conditional operator or elvis operator ?
。尽管它只是具有空检查的更复杂表达式的语法糖,但它允许您构建类似于方法链的东西,并带有基于空检查的 short-circuit 选项。
例如GetFoos()?.MakeBars()?.AnnounceBars();
只有在前面的方法没有returnnull
的情况下才会到达终点,否则后面的调用将被省略。
它可能是 counter-intuitive 但请尝试将您的场景想象成与此相反的情况:编译器会在 a.GetB().Hello();
链中忽略 Hello()
之前的调用,因为您没有到达无论如何链的末端。
免责声明
这都是纸上谈兵的推理,所以请对这个和猫王运算符的类比持保留态度。
Should it really behave differently based on whether the receiver of the conditional call is stored in a temporary variable or not?
是的。
What's the rationale behind the C# compiler not evaluating a.GetB()
in this situation?
Marc和Søren的回答基本正确。这个回答只是为了清楚地记录时间线。
- 该功能于 1999 年设计,其目的始终是删除整个语句。
- 2003年的设计笔记表明,设计团队当时意识到规范在这一点上不清楚。到目前为止,规范仅指出 arguments 不会被评估。我注意到该规范犯了调用参数 "parameters" 的常见错误,尽管人们当然可以假设它们的意思是 "actual parameters" 而不是 "formal parameters".
- 应该创建一个工作项来解决这一点上的 ECMA 规范;显然那从未发生过。
- 更正后的文本第一次出现在任何 C# 规范中是 C# 4.0 规范,我相信是在 2010 年。(我不记得这是我的更正之一,还是其他人发现的。)
- 如果 2017 ECMA 规范不包含此更正,则该错误应在下一个版本中修复。我想,迟到 15 年总比没有好。
考虑以下 类:
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();
调试版本中的行为没有改变;但是,如果未设置标志,我们会得到不同的结果:both 调用都被省略,并且控制台上没有打印。快速查看 IL 显示整行未编译。
根据 latest ECMA standard for C#(ECMA-334,即 C# 5.0),将 Conditional
属性放置在方法上时的预期行为如下(强调我的):
A call to a conditional method is included if one or more of its associated conditional compilation symbols is defined at the point of call, otherwise the call is omitted. (§22.5.3)
这似乎并不表示应该忽略整个链,因此我的问题。也就是说,C# 6.0 draft spec from Microsoft 提供了更多细节:
If the symbol is defined, the call is included; otherwise, the call (including evaluation of the receiver and parameters of the call) is omitted.
调用的参数未被评估这一事实有据可查,因为这是人们使用此功能而不是函数主体中的 #if
指令的原因之一。然而,关于 "evaluation of the receiver" 的部分是新的 - 我似乎无法在其他地方找到它,它似乎确实解释了上述行为。
鉴于此,我的问题是:在这种情况下,C# 编译器不评估 a.GetB()
背后的基本原理是什么? 根据条件调用的接收者是否存储在临时变量中,它的行为真的会有所不同吗?
归结为短语:
(including evaluation of the receiver and parameters of the call) is omitted.
表达式中:
a.GetB().Hello();
"evaluation of the receiver" 是:a.GetB()
。所以:根据规范,它被省略了,这是一个有用的技巧,允许[Conditional]
避免未使用的东西的开销.当你把它放到本地时:
var b = a.GetB();
b.Hello();
那么 "evaluation of the receiver" 只是本地 b
,但是原始 var b = a.GetB();
仍然被评估(即使本地 b
最终被删除)。
这 可能会 产生意想不到的后果,因此:请谨慎使用 [Conditional]
。但原因是可以轻松添加和删除诸如日志记录和调试之类的东西。请注意,如果处理得当,参数可能 也 有问题:
LogStatus("added: " + engine.DoImportantStuff());
和:
var count = engine.DoImportantStuff();
LogStatus("added: " + count);
如果 LogStatus
被标记为 [Conditional]
,可能 非常 不同 - 结果是您的实际 "important stuff" 没有完成。
我做了一些挖掘,发现 C# 5.0 language specification 实际上已经包含了第 424 页的 17.4.2 条件属性 部分中的第二个引号。
也许您想知道为什么它被认为是可以删除的开销?
a.GetB().Hello();
在您的场景中根本没有被调用而 Hello()
被省略可能看起来很奇怪。
我不知道该决定背后的理由,但我自己找到了一些似是而非的推理。也许它也能帮到你。
Method chaining 仅当每个先前的方法都有一个 return 值时才有可能。当您想对这些值执行某些操作时,这很有意义,即 a.GetFoos().MakeBars().AnnounceBars();
如果你有一个函数只 做 没有 return 值的东西,你不能在它后面链接一些东西,但可以把它放在方法链的末尾,就像您的条件方法一样,因为它必须具有 return 类型 void.
另请注意,先前方法调用的 结果 被 丢弃 ,因此在您的 a.GetB().Hello();
示例中GetB()
的结果在执行此语句后没有理由存在。基本上,你 暗示 你只需要 GetB()
的结果来使用 Hello()
.
如果 Hello()
被省略了,为什么还需要 GetB()
呢?如果您省略 Hello()
,您的行将归结为 a.GetB();
而没有任何赋值,并且许多工具会发出警告,提示您没有使用 return 值,因为这很少是您想要做的事情。
你似乎对此不满意的原因是你的方法不仅试图做return某个特定值所必需的事情,而且你还有一个side effect, namely I/O. If you did instead have a pure function 真的没有理由 GetB()
如果您省略后续调用,即如果您不打算对结果做任何事情。
如果您将 GetB()
的结果赋值给一个变量,这本身就是一条语句,无论如何都会被执行。所以这个推理解释了为什么在
var b = a.GetB();
b.Hello();
仅省略了对 Hello()
的调用,而在使用方法链接时省略了整个链。
您也可以查看完全不同的地方以获得更好的视角:C# 6.0 中引入的 null-conditional operator or elvis operator ?
。尽管它只是具有空检查的更复杂表达式的语法糖,但它允许您构建类似于方法链的东西,并带有基于空检查的 short-circuit 选项。
例如GetFoos()?.MakeBars()?.AnnounceBars();
只有在前面的方法没有returnnull
的情况下才会到达终点,否则后面的调用将被省略。
它可能是 counter-intuitive 但请尝试将您的场景想象成与此相反的情况:编译器会在 a.GetB().Hello();
链中忽略 Hello()
之前的调用,因为您没有到达无论如何链的末端。
免责声明
这都是纸上谈兵的推理,所以请对这个和猫王运算符的类比持保留态度。
Should it really behave differently based on whether the receiver of the conditional call is stored in a temporary variable or not?
是的。
What's the rationale behind the C# compiler not evaluating
a.GetB()
in this situation?
Marc和Søren的回答基本正确。这个回答只是为了清楚地记录时间线。
- 该功能于 1999 年设计,其目的始终是删除整个语句。
- 2003年的设计笔记表明,设计团队当时意识到规范在这一点上不清楚。到目前为止,规范仅指出 arguments 不会被评估。我注意到该规范犯了调用参数 "parameters" 的常见错误,尽管人们当然可以假设它们的意思是 "actual parameters" 而不是 "formal parameters".
- 应该创建一个工作项来解决这一点上的 ECMA 规范;显然那从未发生过。
- 更正后的文本第一次出现在任何 C# 规范中是 C# 4.0 规范,我相信是在 2010 年。(我不记得这是我的更正之一,还是其他人发现的。)
- 如果 2017 ECMA 规范不包含此更正,则该错误应在下一个版本中修复。我想,迟到 15 年总比没有好。