为啥在使用这种复合形式时用 XOR 交换值会失败?

Posted

技术标签:

【中文标题】为啥在使用这种复合形式时用 XOR 交换值会失败?【英文标题】:Why does swapping values with XOR fail when using this compound form?为什么在使用这种复合形式时用 XOR 交换值会失败? 【发布时间】:2011-07-31 10:23:42 【问题描述】:

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

现在,我想知道,为什么我的代码输出不正确?

【问题讨论】:

这与给出的答案相同:***.com/questions/3741440 或“相关”列中的许多其他人。即使他们说 C++ 而这是 C#,也适用相同的规则。 @Daemin:不,同样的规则适用。这是 C++ 中未定义的行为,但我不相信它在 C# 中是未定义的。 @Daemin - 这是两天前 Eric Lippert 的相关帖子:***.com/questions/5538193/…,具体来说:“其他答案指出,在 C 和 C++ 编程语言中,语言规范确实如此如果副作用及其观察在同一个“序列点”内,则不指定副作用出现的顺序,就像它们在这里一样。[...] C# 不允许这种纬度。在 C# 中,一个边在右边的代码执行时,观察到左边的效果已经发生。” 有一天我想想一个有趣的问题,让 Jon Skeet 在推特上发... 对于那些结束问题的人 - 另一个问题涉及 C、C++,由于缺少序列点,结果未定义。而这个问题涉及 C#,其中的答案定义明确,但与预期的不同。所以我不会把它当作一个重复的问题,因为答案明显不同。 【参考方案1】:

编辑:好的,明白了。

首先要说明的是,显然您无论如何都不应该使用此代码。但是,当你展开它时,它就相当于:

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

...变成了

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 表明这正是发生的情况。 @Jon 这难道不是另一种证明你不应该使用具有副作用的变量的表达式,除非你只使用一次变量吗? @Lasse:当然。这样的代码太可怕了。 为什么最伟大的 C# 专家之一会在 SO 答案中声明“您不应该使用此代码”? ;-) @Fredrik:在这种情况下,我没有想出开始的代码。当有人问你如何实现某事而我起源可怕的代码时,情况会有所不同:)【参考方案2】:

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

【讨论】:

我检查了输出,它给出了不同的结果:)。问题是为什么会这样…… 因此它首先将评估整个表达式所需的所有值加载到堆栈中,然后将值异或保存回变量中(因此它将使用 i 的初始值和 j 在整个评估表达式) 添加了第二个 IL 的解释 很遗憾交换代码不只是ldloc.0; ldloc.1; stloc.0; stloc.1。无论如何,在 C# 中;这是完全有效的 IL。现在我想起来了……我想知道 C# 是否会从交换中优化临时变量。【参考方案3】:

C#将jiji加载到栈中,并在不更新栈的情况下存储每个XOR结果,所以最左边的XOR使用j的初始值.

【讨论】:

【参考方案4】:

重写:

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

【讨论】:

以上是关于为啥在使用这种复合形式时用 XOR 交换值会失败?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在 Cassandra 表中使用复合聚簇键?

为啥“复合层”需要这么多时间?

SQL中 为啥要避免在where后使用'1=1'这种表达式作为部分条件

为啥 MySQL 不使用复合 WHERE IN 的索引?

为啥复合主键还在?

为啥我的 JSF 复合方面的验证在未呈现方面时完成