7

如果您将 Eric Lippert 的此视频转发到大约 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:允许这样的推论运行有什么风险?

4

2 回答 2

8

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

在 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.

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

因此,给出的代码不应将条件语句的结果分类为始终可达,因此不应将局部分类y为绝对赋值。

为什么需要一个常量表达式只包含常量?

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

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

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

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

这是一个潜在的重大变化:虽然它使编译器符合规范,但它也可能将工作代码变成错误代码。是什么推动了这种变化?

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

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

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

(int x)=>true

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

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

()=>2 + 3

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

所以,现在让我们考虑一下我们刚刚声明的依赖项:

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

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

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

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

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

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

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

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

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

于 2012-01-24T16:50:32.570 回答
3

该规范指出,仅在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 的端点

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

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

于 2012-01-24T06:03:47.457 回答