为啥在使用这种复合形式时用 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));
然后看最深层次的嵌套,我们可以同时替换i
和j
:
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#将j
、i
、j
、i
加载到栈中,并在不更新栈的情况下存储每个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 交换值会失败?的主要内容,如果未能解决你的问题,请参考以下文章