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

本文关键字:交换 失败 复合 为什么 | 更新日期: 2023-09-27 17:48:58

我发现这段代码可以交换两个数字,而不使用第三个变量,使用XOR ^操作符。

代码:

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

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

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

编辑:好的,知道了。

第一点要说明的是,显然无论如何都不应该使用这段代码。但是,当您展开它时,它就等于:
j = j ^ (i = i ^ (j = j ^ i));

(如果我们使用更复杂的表达式,如foo.bar++ ^= i,重要的是++只计算一次,但我认为这里更简单。)

现在,操作数的求值顺序总是从左到右,所以一开始我们得到:

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

这(上面)是最重要的一步。最后执行的异或操作的LHS为36。LHS不是"评估RHS后j的值"。

^的RHS的求值涉及"一层嵌套"表达式,因此它变成:

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

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

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

…这变成了

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

RHS中对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道歉:(

检查生成的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的旧值。

c#将j, i, j, i加载到堆栈中,并存储每个XOR的结果而不更新堆栈,因此最左边的XOR使用j的初始值

改写:

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