看起来表达式的某些部分可能在编译时进行评估,而其他部分则在运行时进行评估

时间:2011-04-14 19:22:52

标签: c#

可能是一个愚蠢的问题,因为我可能已经回答了我的问题,但我只是想确定我没有遗漏某些东西

在检查的上下文中,在编译时计算常量表达式。我认为不应该在编译时评估以下表达式,因为我假设只有当左侧的所有操作数都是常量时,C#才将特定表达式视为常量表达式:

int i= 100;
long u = (int.MaxValue  + 100 + i); //error

相反,似乎编译器会考虑任何子表达式,其中两个操作数都是常量作为常量表达式,即使表达式中的其他操作数是非常量?因此编译器可能只在编译时评估表达式的一部分,而表达式的剩余部分(包含非常量值)将在运行时进行评估 - >我假设在以下示例中,仅在编译时评估(200 +100)

int i=100;
long l = int.MaxValue  + i + ( 200 + 100 ); // works

我的假设是否正确?

感谢名单

1 个答案:

答案 0 :(得分:24)

  

doec C#将任何两个操作数都是常量的子表达式分类为常量表达式,即使表达式中的其他操作数是非常量的吗?

你的问题的答案是“是的”,但重要的是要清楚地理解什么是“子表达”。

  

我假设“int.MaxValue + i +(200 + 100)”仅在编译时评估(200 + 100)

正确。现在,如果您反而说“int.MaxValue + i + 200 + 100”那么“200 + 100”根本就不会被评估,因为这不是由于关联性而导致的子表达式。

这有点微妙。让我详细解释一下。

首先,让我们区分 de jure 编译时常量和事实上的编译时常量。让我给你举个例子。考虑这个方法体:

const int x = 123;
int y = x + x;
if (y * 0 != 0) Console.WriteLine("WOO HOO");
M(ref y);

在C#1和2中,如果你通过优化编译它,那就会编译好像你写的那样:

int y = 246;
M(ref y);

常量x消失,y被初始化为常量折叠表达式,算术优化器意识到任何局部整数乘以零永远不会等于零,因此它也会优化它。

在C#3中,我意外地在优化器中引入了一个错误,该错误在C#4中也没有修复。在C#3/4中,我们将其生成为

int y = 246;
bool b = false;
if (b) Console.WriteLine("WOO HOO");
M(ref y);

也就是说,算法被优化掉了,但是我们不会更进一步并且优化掉“if(false)”。

区别在于x + x上的常量折叠行为保证在编译时发生,但是部分变量表达式y * 0上的常量折叠行为不是。

我为这个错误感到遗憾并道歉。但是,我在C#3中更改了这个并且意外地在代码生成器中引入了一个错误的原因是修复了语义分析器中的错误。在C#2.0中,这是合法的:

int z;
int y = 246;
if (y * 0 == 0) z = 123;
Console.WriteLine(z);

那不应该是合法的。你知道并且我知道y * 0 == 0将永远为真,因此z在读取之前被赋值,但是规范说只有在“if”中的表达式是编译时才能执行此分析。时间常量,这不是编译时常量,因为它包含一个变量。我们对C#3.0进行了重大改变。

所以,好吧,让我们假设您理解必须评估的 dejure 常量之间的区别,因为规范是这样说的,而事实上的常量是评估是因为优化器很聪明。你的问题是在什么情况下表达式在 de jure 事实上在编译时部分“折叠”? (“折叠”是指将包含常量的表达式解析为更简单的表达式。)

我们必须考虑的第一件事是运营商的关联性。 只有在我们完成关联性和优先级分析之后,我们才知道什么是子表达式而不是子表达式。考虑

int y = 3;
int x = 2 - 1 + y;

加法和减法是左关联的,与C#中的大多数运算符一样,因此与

相同
int x = (2 - 1) + y;

现在显然(2 - 1)是 de jure 常量表达式,因此折叠后变为1。

如果另一方面你说

int x = y + 2 - 1;

那是

int x = (y + 2) - 1;

并且它没有折叠,因为它们是两个非常量表达式。

这可能会在检查的上下文中产生实际效果。如果y是int.MaxValue - 1则第一个版本不会溢出,但第二个版本将会溢出!

现在,允许编译器和优化器说“好吧,我碰巧知道这是一个未经检查的上下文,而我碰巧知道我可以安全地将其转换为”y + 1“,所以我会这样做。” C#编译器不会这样做,但抖动可能会。在您的特定示例中,

long l = int.MaxValue + i + 200 + 100 ; 

实际上是由C#编译器代码生成的

long l = ((int.MaxValue + i) + 200) + 100 ; 

而不是

long l = (int.MaxValue + i) + 300; 

如果愿意,抖动可以选择进行优化,并且可以证明这样做是安全的。

但是

long l = int.MaxValue + i + (200 + 100); 

当然会生成为

long l = (int.MaxValue + i) + 300; 

但是,我们在C#编译器中对字符串执行所需的优化!如果你说

string y = whatever;
string x = y + "A" + "B" + "C";

那么你可能会认为,这是一个左联结表达式,所以:

string x = ((y + "A") + "B") + "C";

因此不会有不断的折叠。但是,我们实际上检测到了这种情况,并且在编译时将折叠向右移动,因此我们生成这个就像你写的那样

string x = y + "ABC";

从而节省了运行时连接的成本。字符串concat优化器实际上相当复杂,可以识别在编译时将字符串粘合在一起的各种模式。

我们可以对未经检查的算术做同样的事情。我们还没有找到它。