赋值中出现意外的后增行为

6

请帮我理解为什么在第一个案例中变量a没有增加,而在第二个案例中却增加了。

案例1:

int a = 10;            
a = a++;
Console.WriteLine(a); //prints 10

案例2:

int a = 10;                        
int c = a++;
Console.WriteLine(a); //prints 11

我已经查看了其他类似的问题,但没有找到具体的答案。

更新1:我的程序流程如下:

情况1:

1. 'a' is assigned 10
2. 'a' is assigned 10 before increment happens
3. 'a' is incremented by 1 (Why doesn't this step affect the final value of 'a'?)
4. 'a' is printed --> 10

案例2:

1. 'a' is assigned 10
2. 'c' is assigned 10 before 'a' is incremented
3. 'a' is incremented by 1 (Why does the increment of 'a' work here?)
4. 'a' is printed --> 11

更新2: 感谢所有的回答,我认为我已经理解了,如果我有错,请纠正我。

情况1:

1. `a` is assigned 10
2. Compiler evaluates `a++`, stores old value 10 and new value 11 as well. Since it's a post increment operation, assigns the old value to `a`. What i thought was, compiler would assign the old value 10 first and evaluate the `++` operation later. This is where i was wrong, compiler evaluates the RHS beforehand and assigns the value based on the operator.
4. 'a' is printed --> 10

案例2:

1. `a` is assigned 10
2. Compiler evaluates `a++`, stores old value 10 and new value 11 as well. Since it's a post increment operation, assigns the old value to `c` but value of `a` is preserved with `11`.
4. 'a' is printed --> 11

1
这是未定义行为:a=a++; - chouaib
@chouaib 如果你的意思是它可以编译,那么它确实可以编译。或者未定义行为是指另一件事情吗? - Sang Suantak
1
"未定义行为"接近于数字电路中的"不关心状态"一词,它意味着代码可以编译通过且语法正确,但无法保证其结果。 - chouaib
谢谢提供的信息,我会继续深入了解。那么,这是 C# 的一个 bug 吗? - Sang Suantak
@chouaib,这不是未定义行为。请查看我的答案了解详情。 - Richard Schneider
3
“未定义行为”意味着超出语言规范。正如 Eric Lippert 所写的那样,在 c# 中,除了一个边缘情况或unsafe部分外,除此之外没有“未定义行为”(不在可管理、安全、可编译的代码中)。 - Jcl
7个回答

7

对于我而言,理解某些行为的最佳方式是检查生成的IL代码。在您的第一个案例中,它是

IL_0001:  ldc.i4.s    0A // stack: 10
IL_0003:  stloc.0     // a = 10, stack: empty
IL_0004:  ldloc.0     // stack: 10
IL_0005:  dup         // stack: 10, 10
IL_0006:  ldc.i4.1    // stack: 10, 10, 1
IL_0007:  add         // stack: 10, 11
IL_0008:  stloc.0     // a = 11, stack: 10
IL_0009:  stloc.0     // a = 10, stack: empty
IL_000A:  ldloc.0     // stack: 10
IL_000B:  call        System.Console.WriteLine

您可以看到,在堆栈上仍然存在原始值,因此创建的11最终被覆盖。
让我试着用简单的话来解释一下。
当您将值赋给变量(a = a ++)时,为了保证正确的值,赋值语句右侧的整个内容会首先得到评估。这就是事实。因此,并不像你在执行下一行代码时得到10,应用程序继续增加值。
现在,把后缀递增想象成一个人,他首先增加了一个值,但向您保证您将从表达式中获得原始值。现在您应该看到为什么11被覆盖了。增量首先执行,最终,您将获得承诺的原始值(由IL证明)。

Janacel,谢谢你。但是,如果不看生成的IL,你会预期生成的输出吗?我的意思是结果是出乎意料的。 - Sang Suantak
所有的回答都非常有帮助,但这个让我真正理解了,所以标记它为答案。 - Sang Suantak

6

第一个案例a = a++是后增量。这意味着将1添加到a,但返回a的先前值,然后将先前的结果存回a。这基本上是无操作。

如果它是前增量,a = ++a,那么a将是11。


我正在努力理解你的回答 :D - Sang Suantak
@jSang,你在第二种情况下输出了a,所以变量c就不再起作用了。当你执行a++时,变量a会递增,这就是你得到的结果。如果你输出c,它将是10。 - Jcl
@Jcl 变量 c 只是一个虚拟变量,用于展示它如何影响最终值 a,这是有意为之的。 - Sang Suantak
1
@jSang 好的,为了理解这段代码是如何工作的,因为它只是一个虚拟的例子,你可以将它删除。把 int c = a++; 这一行改成 a++; 然后看看是否更好地理解了它(程序仍然可以编译并输出 11)。 - Jcl

3

正如评论中某人所描述的那样,这里没有未定义的行为。

这是明确定义的行为。要理解发生了什么,您必须首先了解前缀递增和后缀递增运算符的工作原理。

情况1:

a++(后缀递增)将增加a的值并将其存储在a中,然后返回递增之前的值。

因此,在执行a++;之后,a的值将为11,但运算符将返回10

然后a = a++;,赋值部分变成了a = 10;

情况2:

同样地,a++;将使a的值增加到11并返回先前的值(10),它将被分配给cc将为10,但a将为11,因为在这种情况下您没有覆盖a的值。

您的情况1等同于:

int a = 10;
int temp = a;//10
a = a + 1;  //11
a = temp;   //10
Console.WriteLine(a);//10

Case2 等于:

int a = 10;
int temp = a;//10
a = a + 1;   //11
int c = temp;//10
Console.WriteLine(a);//11

我希望现在你能明白为什么你看到了你所看到的。

2
这不是未定义的行为,也不是错误。根据MSDN文档
The increment operator (++) increments its operand by 1. 
The increment operator can appear before or after its operand.
The first form is a prefix increment operation. The result of the operation is the value of the operand after it has been incremented.
The second form is a postfix increment operation. The result of the operation is the value of the operand before it has been incremented.

字面上来说,MSDN告诉你如果你使用这个语法(后缀):

a = a++;

那么,这个操作的结果将会先把a赋值给它自己,然后再进行增量操作。但是,由于赋值操作已经发生,你会失去增加的结果。
像这样使用(前缀):
a = ++a;

这将首先递增a,然后将递增后的值分配给a编辑 我会尝试将其分解,以便您更好地理解。
首先,要知道 ++ 始终返回一个值。如果使用前缀版本(例如 ++a ),它返回 a + 1 的值。如果使用后缀版本(例如 a ++ ),则在递增发生之前返回 a 的值。
当您执行此代码时:
int a = 10;
a = a++;

您正在告诉编译器在递增之前将值a赋给a。因此,在执行此操作后,a等于10。递增的值11在“虚无”中丢失了。请注意,您没有将11分配给a。您正在分配递增前的旧值,这就是为什么输出为10的原因。

当您执行此代码时:

int a = 10;
int b = a++;

在执行最后一行代码后,会将变量 a 的值增加到 11,并将 10 赋值给变量 'b'。由于赋值给了不同的变量,所以 a 不会像第一个例子中那样被原始值 10 覆盖。
为了更加形象化地说明,可以看下面这个例子:
a = a++;
    ^ a is increased to 11, but the postfix increment returns the old value (10)

这行代码实际上变成了:
a = 10;

int b = a++;
        ^ a is increased to 11, but b gets assigned a's old value

这一行实际上变成了:
int b = 10;
a = a + 1;

这是否能解决问题?

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Sang Suantak
@jSang - 这是因为当C#编译该代码时,它首先分配值,然后再递增。赋值首先发生,不会第二次发生。递增的值会丢失。 - Icemanind
@icemanind,你不能这样看待它。会有许多“失落”的值。仅仅因为你不能像C#那样与堆栈进行如此密切的交互,并不意味着它已经丢失了。你当然可以直接在IL中编写代码并毫无问题地访问该值。 - Ondrej Janacek
@icemanind 好的,我想我可能太过于追求完美了。我们可以肯定地认同在整个过程中丢失了值11。 - Ondrej Janacek
@jSang - 是的,现在你理解得很好! - Icemanind
显示剩余4条评论

0

为了明确,++a和a++是有区别的。

它们都会将a增加1,但过程略有不同,只有将其赋值给另一个变量才能看到。

这就是你可能在寻找的前缀后缀增量之间的区别。

案例1

 int a,b;
 a = 10;
 b = a++; //Here b is assigned by a post-incremented a, therefore b value is still 10

案例2

 int a,b;
 a = 10;
 b = ++a; //Here b is assigned by a pre-incremented a, b value is now 11

摘自http://en.wikipedia.org/wiki/Increment_and_decrement_operators

更新

回复此问题:

int a = 10;
a = a++;

流程如下:

1. 创建数据类型为整数的变量'a'

2. 将变量'a'的值赋为10

3. 检查右侧操作的可用性和数据类型的正确性。这意味着对右侧的所有操作(增加'a')都将被执行。'a'现在是11

4. 将后自增的值(即10)分配给'a','a'现在是10


0

我认为这不是未定义的行为,因为在第一种情况下:

int a = 10;            
a = a++;
Console.WriteLine(a); //prints 10

a的值是在自增操作之前的值,这意味着如果我们简化a = a++,那么:

a = a ; // `a` has `10`

a++; // `a` has `11`

只有在 a 的值为 11 时才会发生。

第二种情况:

int a = 10;                        
int c = a++;
Console.WriteLine(a); //prints 11

现在c的值为10,因为赋值操作使左侧的值获取变量(右侧)的值,在增量操作之前,然后右侧的值才会增加1


-1
在第二种情况下,您应该打印出c。 后置递增运算符的结果始终是递增前的值。因此,首先将值10返回,然后将其递增到11,然后将操作结果(10)分配给左操作数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接