当使用这种复合形式时,为什么使用异或交换值会失败?

77

我发现了这段代码,用异或运算符 ^ 来交换两个数而不使用第三个变量。

代码:

int i = 25;
int j = 36;
j ^= i;       
i ^= j;
j ^= i;

Console.WriteLine("i:" + i + " j:" + j);

//numbers Swapped correctly
//Output: i:36 j:25

现在我把上面的代码改成了等效的代码。

我的代码:

int i = 25;
int j = 36;

j ^= i ^= j ^= i;   // I have changed to this equivalent (???).

Console.WriteLine("i:" + i + " j:" + j);

//Not Swapped correctly            
//Output: i:36 j:0

现在,我想知道,为什么我的代码会给出错误的输出?


2
这个问题的答案与以下链接中提供的答案相同:https://dev59.com/22865IYBdhLWcg3wlvmq 或者相关栏目中的其他问题。尽管它们是关于C++的,而这个问题是关于C#的,但是相同的规则适用。 - Dominik Grabiec
8
@Daemin:不,不是使用相同的规则。在C++中这是未定义行为,但我认为在C#中不是未定义的。 - Jon Skeet
3
这是Daemin的相关帖子,Eric Lippert在两天前发布了一篇相关文章:https://dev59.com/hFXTa4cB1Zd3GeqP5t55#5539496 ,具体来说:"其他答案指出,在C和C++编程语言中,如果副作用和其观察在同一“序列点”内,语言规范未指定副作用的顺序,就像它们在这里一样。[...] C#不允许这种自由度。在C#中,左侧的副作用被认为在右侧执行代码时已经发生了。" - Kobi
2
有一天我想出一个足够有趣,能让Jon Skeet发推的问题。 - David Johnstone
8
对于那些关闭了问题的人——另一个问题涉及到C、C++,由于缺少顺序点而导致结果未定义。而这个问题涉及到C#,答案是明确定义的,但可能与预期不同。因此,我不认为它是一个重复的问题,因为答案是明显不同的。 - Damien_The_Unbeliever
显示剩余3条评论
4个回答

78

编辑:好的,我懂了。

首先要明确的一点是,显然你不应该使用这段代码。然而,当你扩展它时,它变得等价于:

j = j ^ (i = i ^ (j = j ^ i));

(如果我们使用的是像foo.bar++ ^= i这样更复杂的表达式,那么 ++只被计算一次就变得很重要了,但是在这里我认为它更简单。)

现在,操作数的计算顺序总是从左到右,所以我们首先得到:

j = 36 ^ (i = i ^ (j = j ^ i));

这一步(上面提到的)是最重要的。我们得到了36作为XOR操作的LHS,这是最后执行的。LHS并不是“在RHS被评估后j的值”。

对^的RHS进行评估涉及“一级嵌套”的表达式,因此它变成了:

j = 36 ^ (i = 25 ^ (j = j ^ i));

然后我们看最深层的嵌套,可以同时替换 ij

j = 36 ^ (i = 25 ^ (j = 25 ^ 36));

... which becomes

=>

...变成了什么

j = 36 ^ (i = 25 ^ (j = 61));

在右侧的赋值操作中,对于 j 的赋值首先发生,但该结果最终会被覆盖,因此我们可以忽略它 - 在最终赋值之前没有进一步对 j 的评估:

j = 36 ^ (i = 25 ^ 61);

现在这相当于:

i = 25 ^ 61;
j = 36 ^ (i = 25 ^ 61);

或者:

i = 36;
j = 36 ^ 36;

变成了什么:

i = 36;
j = 0;

认为这一切都是正确的,并且它可以得出正确的答案...如果有一些关于求值顺序的细节方面不太准确,那么向Eric Lippert道歉 :(


1
IL 表明这正是发生的事情。 - SWeko
1
@Jon 这不就是另一种证明,除非您只使用变量一次,否则不应该具有副作用的变量表达式吗? - Lasse V. Karlsen
8
@Lasse:绝对是的。像这样的代码太糟糕了。 - Jon Skeet
8
为什么一个拥有如此高水平C#技能的专家在Stack Overflow上经常会说“你不应该使用这段代码”呢?;-) - Fredrik Mörk
8
在这种情况下,起初我并没有编写这段代码。当有人问如何实现某事并且我是源头时,情况会有所不同 :) - Jon Skeet
显示剩余2条评论

15

查看生成的IL代码,发现它会产生不同的结果;

正确的交换操作会生成一个简单明了的结果:

IL_0001:  ldc.i4.s   25
IL_0003:  stloc.0        //create a integer variable 25 at position 0
IL_0004:  ldc.i4.s   36
IL_0006:  stloc.1        //create a integer variable 36 at position 1
IL_0007:  ldloc.1        //push variable at position 1 [36]
IL_0008:  ldloc.0        //push variable at position 0 [25]
IL_0009:  xor           
IL_000a:  stloc.1        //store result in location 1 [61]
IL_000b:  ldloc.0        //push 25
IL_000c:  ldloc.1        //push 61
IL_000d:  xor 
IL_000e:  stloc.0        //store result in location 0 [36]
IL_000f:  ldloc.1        //push 61
IL_0010:  ldloc.0        //push 36
IL_0011:  xor
IL_0012:  stloc.1        //store result in location 1 [25]
错误的交换生成了这段代码:
IL_0001:  ldc.i4.s   25
IL_0003:  stloc.0        //create a integer variable 25 at position 0
IL_0004:  ldc.i4.s   36
IL_0006:  stloc.1        //create a integer variable 36 at position 1
IL_0007:  ldloc.1        //push 36 on stack (stack is 36)
IL_0008:  ldloc.0        //push 25 on stack (stack is 36-25)
IL_0009:  ldloc.1        //push 36 on stack (stack is 36-25-36)
IL_000a:  ldloc.0        //push 25 on stack (stack is 36-25-36-25)
IL_000b:  xor            //stack is 36-25-61
IL_000c:  dup            //stack is 36-25-61-61
IL_000d:  stloc.1        //store 61 into position 1, stack is 36-25-61
IL_000e:  xor            //stack is 36-36
IL_000f:  dup            //stack is 36-36-36
IL_0010:  stloc.0        //store 36 into positon 0, stack is 36-36 
IL_0011:  xor            //stack is 0, as the original 36 (instead of the new 61) is xor-ed)
IL_0012:  stloc.1        //store 0 into position 1

很明显第二种方法生成的代码是不正确的,因为在需要使用新值的计算中使用了旧值的j。


我检查了输出结果,它给出了不同的结果“:)”。问题是为什么会发生这种情况... - Kobi
因此,它首先将需要评估整个表达式的所有值加载到堆栈上,然后在进行异或并将值保存回变量时(因此在评估表达式期间将使用i和j的初始值)。 - Damien_The_Unbeliever
添加了第二个IL的解释。 - SWeko
1
很遗憾,交换代码不仅仅是 ldloc.0; ldloc.1; stloc.0; stloc.1。在C#中,这是完全有效的IL。现在我想起来了...我想知道C#是否会优化掉交换的临时变量。 - cHao

7

C#在栈上加载jiji,并将每个XOR结果存储在不更新栈的情况下,因此最左边的XOR使用j的初始值。


0

重写:

j ^= i;       
i ^= j;
j ^= i;

扩展^=

j = j ^ i;       
i = j ^ i;
j = j ^ i;

替换:

j = j ^ i;       
j = j ^ (i = j ^ i);

只有在/因为^运算符的左侧先被评估时,替换才起作用:

j = (j = j ^ i) ^ (i = i ^ j);

折叠 ^

j = (j ^= i) ^ (i ^= j);

对称地:

i = (i ^= j) ^ (j ^= i);

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