使用此复合形式时,为什么用XOR交换值失败?


76

我发现此代码使用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

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


2
答案将与以下给出的答案相同:stackoverflow.com/questions/3741440或“相关”列中的许多其他答案。即使他们说的是C ++并且这是C#,也将应用相同的规则。

8
@Daemin:不,相同的规则适用。这是C ++中未定义的行为,但是我不相信它在C#中是未定义的。
乔恩·斯基特

3
@Daemin-这是两天前Eric Lippert的相关文章:stackoverflow.com/questions/5538193/…,特别是:“其他答案指出,在C和C ++编程语言中,语言规范未指定daccess-ods.un.org daccess-ods.un.org如果副作用及其观察结果位于此处相同的“序列点”之内,则按照什么顺序出现副作用。[...] C#不允许这种纬度。在执行右边的代码时,观察到左边发生了事件。”
Kobi

2
有一天我想想到一个有趣的问题,让乔恩·斯凯特(Jon Skeet)发表……
戴维·约翰斯通

8
对于那些已经结束问题的人-另一个问题涉及C,C ++,由于缺少序列点,结果不确定。而此问题与C#有关,答案的定义很明确,但与预期的不同。因此,我不会将其视为重复问题,因为答案明显不同。
Damien_The_Unbeliever 2011年

Answers:


77

编辑:好的,知道了。

首先要说明的是,显然您不应该使用此代码。但是,当您展开它时,它等效于:

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

(如果使用的是更复杂的表达式,例如foo.bar++ ^= i,则++仅对进行一次评估就很重要,但在这里我相信它会更简单。)

现在,操作数的求值顺序始终是从左到右,因此首先我们得到:

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

这是最重要的步骤。我们最后得到了36作为最后执行的XOR操作的LHS。LHS不是“ jRHS评估后的值”。

^的RHS的评估涉及“一级嵌套”表达式,因此变为:

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

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

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

...变成

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

jRHS中的分配首先发生,但是无论如何最后都会覆盖结果,因此我们可以忽略这一点-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 2011年

1
@Jon这不是证明您不应该具有带有副作用的变量的表达式的另一种方法,除非您只使用一次变量?
Lasse V. Karlsen

8
@Lasse:绝对。这样的代码太可怕了。
乔恩·斯基特

8
为什么最出色的C#专家之一会在SO答案中指出“您不应该使用此代码”呢?;-)
FredrikMörk2011年

8
@Fredrik:在这种情况下,我没有想到要开始使用的代码。当有人问您如何实现目标并且我产生了可怕的代码时,情况有所不同:)
Jon Skeet

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 2011年

增加了对第二IL解释
SWeko

1
遗憾的是,交换的代码不只是ldloc.0; ldloc.1; stloc.0; stloc.1。无论如何在C#中;这是完全有效的IL。现在我考虑了一下...我想知道C#是否可以通过交换来优化temp变量。
cHao 2011年

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);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.