我仍然觉得这个问题有点令人困惑,但让我看看是否可以将问题改写成我可以回答的形式。首先,让我重新陈述问题的背景:
在 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 行为正确?
这是正确的,因为规范指出:
因此,给出的代码不应将条件语句的结果分类为始终可达,因此不应将局部分类y
为绝对赋值。
为什么需要一个常量表达式只包含常量?
我们希望 C# 语言能够被用户清楚地理解,并且能够被编译器编写者正确实现。要求编译器对表达式的值进行所有可能的逻辑推导与这些目标背道而驰。判断一个给定的表达式是否是一个常数应该很简单,如果是,它的值是多少。简而言之,常量评估代码应该知道如何执行算术,但不需要知道关于算术操作的事实。常量求值器知道如何乘以 2 * 1,但它不需要知道“1 是整数的乘法恒等式”这一事实。
现在,编译器编写者可能会决定在某些领域他们可以很聪明,从而生成更优化的代码。编译器编写者可以这样做,但不能改变代码是否合法。他们只允许在给定合法代码时进行优化,使编译器的输出更好。
这个错误是如何在 C# 2.0 中发生的?
发生的事情是编译器被编写为过早地运行算术优化器。优化器应该是聪明的,它应该在程序被确定为合法之后运行。它在程序被确定为合法之前运行,因此影响了结果。
这是一个潜在的重大变化:虽然它使编译器符合规范,但它也可能将工作代码变成错误代码。是什么推动了这种变化?
LINQ 特性,特别是表达式树。如果你说这样的话:
(int x)=>x * 0 == 0
并将其转换为表达式树,您是否希望生成表达式树
(int x)=>true
? 可能不是!您可能希望它生成“将 x 乘以零并将结果与零进行比较”的表达式树。 表达式树应该在主体中保留表达式的逻辑结构。
当我编写表达式树代码时,还不清楚设计委员会是否会决定是否
()=>2 + 3
将生成“加二到三”的表达式树或“五”的表达式树。我们决定采用后者——在生成表达式树之前折叠常量,但在生成表达式树之前不应该通过优化器运行算术运算。
所以,现在让我们考虑一下我们刚刚声明的依赖项:
- 算术优化必须在代码生成之前进行。
- 表达式树重写必须在算术优化之前发生
- 常量折叠必须在表达式树重写之前发生
- 必须在流量分析之前进行恒定折叠
- 流分析必须在表达式树重写之前进行(因为我们需要知道表达式树是否使用未初始化的本地)
我们必须找到一个命令来完成所有这些工作,以尊重所有这些依赖关系。C# 2.0 中的编译器按以下顺序执行它们:
表达式树重写可以去哪里?无处!显然这是有问题的,因为流分析现在考虑了算术优化器推导出的事实。我们决定重新设计编译器,以便它按以下顺序执行操作:
- 不断折叠
- 流量分析
- 表达式树重写
- 算术优化
- 代码生成器
这显然需要进行重大更改。
现在,我确实考虑通过这样做来保留现有的损坏行为:
- 不断折叠
- 算术优化
- 流量分析
- 算术反优化
- 表达式树重写
- 再次算术优化
- 代码生成器
优化的算术表达式将包含一个指向其未优化形式的指针。我们认为这太复杂了,以保留一个错误。我们决定最好改成修复错误,进行重大更改,并使编译器架构更易于理解。