似乎表达式的某些部分可能在编译时求值,而其他部分可能在运行时求值

本文关键字:其他部 运行时 表达式 编译 些部 | 更新日期: 2023-09-27 17:50:18

可能是一个愚蠢的问题,因为我可能已经回答了我的问题,但我只是想确保我没有错过什么

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

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

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

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

我的假设正确吗?

谢谢

似乎表达式的某些部分可能在编译时求值,而其他部分可能在运行时求值

c#是否将两个操作数都是常量的子表达式归类为常量表达式,即使表达式中的其他操作数是非常量?

你的问题的答案是"是",但重要的是要清楚地了解什么构成了"子表达式"。

我假设在"int"中。MaxValue + i +(200 + 100) "在编译时只计算(200 + 100)

正确的。现在,如果你说"int。"MaxValue + i + 200 + 100"那么"200 + 100"将永远不会被求值,因为由于结合性,它不是子表达式。

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

首先,让我们区分法律上的编译时间常数和事实上的编译时间常数。让我给你举个例子。考虑这个方法体:
const int x = 123;
int y = x + x;
if (y * 0 != 0) Console.WriteLine("WOO HOO");
M(ref y);

在c# 1和c# 2中,如果你在进行优化的情况下编译它,它将被编译成好像你写了:

int y = 246;
M(ref y);

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

在c# 3中,我不小心在优化器中引入了一个bug,这个bug在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中改变这一点,并意外地在代码生成器中引入一个bug,是为了修复语义分析器中的一个bug。在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进行了突破性的修改。

那么,好吧,让我们假设您理解了法律上的常数和事实上的常数之间的区别,它们必须求值,因为规范是这样说的,而常数是求值的,因为优化器是聪明的。你的问题是,在什么情况下,表达式在编译时可以在法律上和事实上的部分"折叠"?(这里的"折叠"是指将包含常量的表达式解析为更简单的表达式。)

首先要考虑的是操作符的结合性。只有在完成结合性和优先级分析之后,我们才知道什么是子表达式,什么不是子表达式。

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

与c#中的大多数操作符一样,加减法是左结合的,因此与

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

现在很明显(2 - 1)是一个法律上的常量表达式,所以它被折叠成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; 

如果jitter希望并且能够证明这样做是安全的,它可能会选择进行优化。

,

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";

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

对于未检查的算术也可以这样做。