常量和编译时评估 - 为啥要改变这种行为

Posted

技术标签:

【中文标题】常量和编译时评估 - 为啥要改变这种行为【英文标题】:Constants and compile time evaluation - Why change this behaviour常量和编译时评估 - 为什么要改变这种行为 【发布时间】:2012-02-17 09:49:34 【问题描述】:

如果您将 Eric Lippert 的 video 转发到大约 13 分钟,他描述了对 C# 编译器所做的更改,该更改导致以下代码无效(显然在 .NET 2 之前并包括此代码将已编译) .

int y;
int x = 10;
if (x * 0 == 0)
    y = 123;

Console.Write(y);

现在我明白,上述代码的任何执行实际上都会评估为

int y;
int x = 10;
y = 123;
Console.Write(y);

但我不明白为什么将以下代码设为不可编译被认为是“可取的”? IE:允许这样的推论运行有什么风险?

【问题讨论】:

按照我的理解,Eric 说它主要是(如果不是唯一的)修复,以使编译器的行为符合规范。 @CodyGray,我认为问题是为什么它在规范中。 @Walkerneo - 现场。我理解为什么要做出改变(正如 Eric 解释的那样),但我不明白为什么它首先存在。虽然可以说是不可取的,但在我看来这是一系列有效的陈述。 代码无法编译的原因是y在使用范围内未初始化使用。 为什么规范中有what?这个问题非常不清楚。您是在问为什么必须明确分配局部变量?或者为什么流分析只考虑常量表达式?还是什么? 【参考方案1】:

我仍然觉得这个问题有点令人困惑,但让我看看我是否可以将问题改写成我可以回答的形式。首先,让我重新陈述问题的背景:

在 C# 2.0 中,此代码:

int x = 123;
int y;
if (x * 0 == 0) 
    y = 345;
Console.WriteLine(y);

被当作你写过的

int x = 123;
int y;
if (true) 
    y = 345;
Console.WriteLine(y);

这又被视为:

int x = 123;
int y;
y = 345;
Console.WriteLine(y);

这是一个合法的程序。

但在 C# 3.0 中,我们采取了重大更改来防止这种情况发生。尽管您和我都知道它始终为真,但编译器不再将条件视为“始终为真”。我们现在将其设为非法程序,因为编译器认为它不知道“if”的主体总是被执行,因此不知道局部变量 y 总是在之前被赋值它被使用了。

为什么 C# 3.0 的行为是正确的?

这是正确的,因为规范指出:

常量表达式必须只包含常量。 x * 0 == 0 不是一个常量表达式,因为它包含一个非常量项x

只有当条件是等于 true 的常量表达式时,if 的结果才知道始终可达。

因此,给出的代码不应将条件语句的结果归类为始终可达,因此不应将本地 y 归类为明确分配。

为什么希望常量表达式只包含常量?

我们希望 C# 语言能够被用户清楚地理解,并且能够被编译器编写者正确实现。要求编译器对表达式的值进行所有可能的逻辑推导与这些目标背道而驰。判断一个给定的表达式是否是一个常量,如果是,它的值是什么应该是简单。简而言之,常量评估代码应该知道如何执行算术,但不需要知道关于算术操作的事实。常量求值器知道如何乘以 2 * 1,但它不需要知道“1 是整数的乘法恒等式”这一事实

现在,编译器编写者可能决定在某些领域他们可以很聪明,从而生成更优化的代码。编译器编写者可以这样做,但不能改变代码是否合法。他们只允许进行优化,以在给定合法代码时使编译器的输出更好

这个错误是如何在 C# 2.0 中发生的?

发生的事情是编译器被编写为过早地运行算术优化器。优化器应该很聪明,它应该在程序被确定为合法之后运行。它在程序被确定为合法之前运行,因此影响了结果。

这是一个潜在的重大更改:尽管它使编译器符合规范,但它也可能将工作代码转换为错误代码。是什么促成了这种变化?

LINQ 功能,特别是表达式树。如果你说这样的话:

(int x)=>x * 0 == 0

并将其转换为表达式树,您是否希望生成表达式树

(int x)=>true

?可能不是!您可能希望它生成“将 x 乘以零并将结果与​​零进行比较”的表达式树。 表达式树应该在主体中保留表达式的逻辑结构。

当我写表达式树代码时,还不清楚设计委员会是否会决定是否

()=>2 + 3

将生成“二到三”的表达式树或“五”的表达式树。我们决定采用后者——常量在生成表达式树之前被折叠,但是在生成表达式树之前算术不应该通过优化器运行。

所以,现在让我们考虑一下我们刚才所说的依赖关系:

算术优化必须在代码生成之前进行。 表达式树重写必须在算术优化之前进行 必须在表达式树重写之前进行常量折叠 必须在流分析之前进行恒定折叠 流分析必须在表达式树重写之前进行(因为我们需要知道表达式树是否使用未初始化的本地)

我们必须找到一个订单来完成所有这些工作,以尊重所有这些依赖关系。 C# 2.0 中的编译器按以下顺序进行:

同时进行常量折叠和算术优化 流量分析 代码生成

表达式树重写在哪里?无处!显然这是有问题的,因为流分析现在考虑了算术优化器推导出的事实。我们决定重新设计编译器,使其按顺序执行:

不断折叠 流量分析 表达式树重写 算术优化 代码生成

这显然需要进行重大更改。

现在,我确实考虑通过这样做来保留现有的损坏行为:

不断折叠 算术优化 流量分析 算术去优化 表达式树重写 再次算术优化 代码生成

优化的算术表达式将包含一个指向其未优化形式的指针。为了保留一个错误,我们认为这太复杂了。我们决定改为修复错误、进行重大更改并使编译器架构更易于理解会更好。

【讨论】:

Eric,您可能想要链接您关于此主题的博客文章: 万恶之根第 1 部分:blogs.msdn.com/b/ericlippert/archive/2006/03/28/… 第 2 部分:blogs.msdn.com/b/ericlippert/archive/2006/03/29/… 我能不能说非常感谢您抽出时间来如此全面地回答我的问题,并帮助创造了如此出色的产品。衷心感谢。【参考方案2】:

规范指出,仅在 if 块内分配的内容的明确分配是未确定的。该规范没有说明删除不必要的if 块的编译器魔术。特别是,当您更改 if 条件时,它会产生非常令人困惑的错误消息,并突然收到关于未分配 y 的错误“嗯?分配 y 时我没有更改!”。

编译器可以随意执行任何明显的代码删除,但首先它需要遵循规则规范。

具体来说,第 5.3.3.5 节(MS 4.0 规范):

5.3.3.5 If 语句 对于形式的 if 语句 stmt:

if ( expr ) then-stmt else else-stmt

v 在 expr 开头和在 stmt 开头具有相同的明确赋值状态。 如果 v 在 expr 的末尾明确分配,那么它肯定在控制流转移到 then-stmt 和 else-stmt 或如果没有 else 子句的情况下分配到 stmt 的端点。 如果 v 在 expr 的末尾具有“在真表达式后确定分配”的状态,则在将控制流转移到 then-stmt 时明确分配它,而不是在将控制流转移到 else-stmt 时明确分配如果没有 else 子句,则到 stmt 的终点。 如果 v 在 expr 的末尾具有“在 false 表达式后确定分配”的状态,那么它在控制流转移到 else-stmt 时肯定分配,而不是在控制流转移到 then-stmt 时确定分配。当且仅当它肯定在 then-stmt 的端点被赋值时,它才被确定在 stmt 的端点。 否则,v 在控制流转移时被认为未明确分配给 then-stmt 或 else-stmt,或者如果没有 else-stmt 则分配给 stmt 的端点

对于一个最初未赋值的变量被认为在某个位置被确定赋值,对该变量的赋值必须发生在通向该位置的每个可能的执行路径中。

技术上,执行路径存在if条件为假的地方;如果y 也被分配在else 中,那么很好,但是......规范明确没有要求发现if 条件始终为真。

【讨论】:

实际上,规范中与这个问题更相关的部分是: 任何其他语句开头的 v 的明确分配状态是通过检查所有控件上的 v 的明确分配状态来确定的以该语句开头为目标的流传输... 可能的控制流传输集的确定方式与检查语句可达性的方式相同。 如果条件始终为真,则只能达到 y 的用法通过 "if" 的主体,在该主体之后,肯定会分配 y。 @Eric 关于规范的主题 - 它不是关键或紧迫等,但您有时间考虑optional parameters issue吗?

以上是关于常量和编译时评估 - 为啥要改变这种行为的主要内容,如果未能解决你的问题,请参考以下文章

nameof() 是在编译时评估的吗?

sizeof 是在编译时还是运行时评估?

是啥阻止了这个 constexpr 函数的编译时评估?

是否可以在编译时评估数组?

C# 逻辑顺序和编译器行为

jquery click函数声明在运行时评估变量