Чому обмін значеннями з 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
@Deemin: Ні, ті самі правила не застосовуються. Це невизначена поведінка в C ++, але я не вірю, що це невизначено в C #.
Джон Скіт,

3
@Daemin - Ось відповідний допис Еріка Ліпперта, всього два дні тому: stackoverflow.com/questions/5538193/… , зокрема: "Інші відповіді вказують, що в мовах програмування С і С ++ мовні специфікації не визначають в якому порядку виникають побічні ефекти, якщо побічний ефект та його спостереження знаходяться в тій самій "точці послідовності", що і тут. [...] C # не дозволяє такого роду широти. У C # побічний ефект спостерігається, що ліворуч сталося до того часу, коли виконується код праворуч ".
Кобі

2
Одного разу я хочу подумати над питанням, яке є досить цікавим, щоб Джон Скіт писав у Twitter ...
Девід Джонстон

8
Для тих, хто закрив питання - інше питання стосується C, C ++, де результат невизначений через відсутність точок послідовності. Тоді як це питання стосується C #, де відповідь чітко визначена, але відрізняється від того, що можна очікувати. Тому я не ставлюся до нього як до дублікату запитання, оскільки відповіді суттєво різні.
Damien_The_Unbeliever

Відповіді:


77

EDIT: Гаразд, зрозумів.

Перше, що слід сказати, це те, що очевидно, ви все одно не повинні використовувати цей код. Однак, коли ви розширюєте її, вона стає еквівалентною:

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

(Якби ми використовували більш складний вираз, такий як foo.bar++ ^= i, було б важливо, щоб його ++оцінювали лише один раз, але тут я вважаю, що це простіше.)

Тепер порядок оцінки операндів завжди зліва направо, тому для початку отримаємо:

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

Це (вище) є найважливішим кроком. Ми отримали 36 як LHS для операції XOR, яка виконується останньою. LHS не є "значенням jпісля оцінки RHS".

Оцінка RHS значення ^ включає вираз «вкладений на один рівень», тому воно стає:

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

Потім, дивлячись на найглибший рівень вкладеності, ми можемо підставити обидва iі j:

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

... що стає

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

Присвоєння jв RHS відбувається спочатку, але результат у будь-якому випадку перезаписується в кінці, тому ми можемо проігнорувати це - немає подальших оцінок jдо остаточного призначення:

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

Зараз це еквівалентно:

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

Або:

i = 36;
j = 36 ^ 36;

Що стає:

i = 36;
j = 0;

Я думаю, що це все правильно, і це діходить до правильної відповіді ... вибачення перед Еріком Ліппертом, якщо деякі деталі щодо замовлення на оцінку трохи не відповідають


1
Іллінойс припускає, що трапляється саме це.
SWeko

1
@Jon Чи це не просто ще один спосіб довести, що у вас не повинно бути виразів зі змінними, які мають побічні ефекти, якщо ви не використовуєте змінну лише один раз?
Лассе В. Карлсен,

8
@Lasse: Абсолютно. Такий код жахливий.
Джон Скіт,

8
Чому один із найбільших експертів C # з такою частотою у відповідях на запитання SO "не повинен використовувати цей код" ? ;-)
Фредрік Мерк,

8
@ Fredrik: У цьому випадку я не придумав коду для початку. Дещо інше, коли хтось запитує, як ти можеш чогось досягти, і я походжу жахливий код :)
Джон Скіт,

15

Перевірив сформований ІЛ і він видає різні результати;

Правильний обмін генерує простий:

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 використовується при обчисленні, де потрібно нове значення.


Я перевірив результати, і це дає різні результати :). Питання в тому, чому так трапляється ...
Кобі

Отже, це завантаження всіх значень, необхідних для обчислення всього виразу, у стек, а потім xoring та збереження значень назад у змінні по ходу (тому він буде використовувати початкові значення i та j протягом обчислення виразу)
Damien_The_Unbeliever

Додано пояснення для другого ІЛ
SWeko

1
Шкода, що код для обміну не просто ldloc.0; ldloc.1; stloc.0; stloc.1. У C # все одно; це цілком дійсний ІЛ. Тепер, коли я думаю над цим ... мені цікаво, чи C # оптимізував би тимчасову змінну з обміну.
cHao

7

C # навантаження j, i, j, iв стеку і зберігає кожен 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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.