Чи є пояснення для вбудованих операторів у “k + = c + = k + = c;”?


89

Яке пояснення результату наступної операції?

k += c += k += c;

Я намагався зрозуміти вихідний результат із наступного коду:

int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70

і в даний час я боюся з розумінням, чому результат для "k" дорівнює 80. Чому присвоєння k = 40 не працює (насправді Visual Studio каже мені, що це значення не використовується деінде)?

Чому дорівнює k 80, а не 110?

Якщо я розділю операцію на:

k+=c;
c+=k;
k+=c;

результат k = 110.

Я намагався переглянути CIL , але я не настільки глибоко інтерпретував згенерований CIL і не можу отримати кілька деталей:

 // [11 13 - 11 24]
IL_0001: ldc.i4.s     10
IL_0003: stloc.0      // k

// [12 13 - 12 24]
IL_0004: ldc.i4.s     30
IL_0006: stloc.1      // c

// [13 13 - 13 30]
IL_0007: ldloc.0      // k expect to be 10
IL_0008: ldloc.1      // c
IL_0009: ldloc.0      // k why do we need the second load?
IL_000a: ldloc.1      // c
IL_000b: add          // I expect it to be 40
IL_000c: dup          // What for?
IL_000d: stloc.0      // k - expected to be 40
IL_000e: add
IL_000f: dup          // I presume the "magic" happens here
IL_0010: stloc.1      // c = 70
IL_0011: add
IL_0012: stloc.0      // k = 80??????

3
Ви отримали різний результат, оскільки розділили функцію, k + = c + = k + = c = 80, оскільки значення k і c залишаються однаковими у всіх сумах, тому k + = c + = k + = c дорівнює до 10 + 30 + 10 + 30
Жоау Паулу Аморім

78
Цікава вправа, але на практиці ніколи не пишіть такий ланцюжок кодів, якщо тільки ви не хочете, щоб ваші колеги ненавиділи вас. :)
UnhandledExcepSan

3
@AndriiKotliarov, оскільки k + = c + = k + = c дорівнює 10 + 30 + 10 + 30, отже, K отримує всі значення, а C отримує лише останні 3 аргументи 30 + 10 + 30 = 70
João Paulo Amorim

6
Також варто читати - Ерік Ліпперта відповідь на В чому різниця між I ++ і ++ я?
Wai Ha Lee

34
"Докторе, лікарю, боляче, коли я це роблю!" "Тож не роби цього".
Девід Конрад

Відповіді:


104

Така операція a op= b;еквівалентна a = a op b;. Призначення може використовуватися як оператор або як вираз, тоді як як вираз воно дає присвоєне значення. Ваше твердження ...

k += c += k += c;

... може, оскільки оператор присвоєння право-асоціативний, також може бути записаний як

k += (c += (k += c));

або (розширений)

k =  k +  (c = c +  (k = k  + c));
     10301030   // operand evaluation order is from left to right
      |         |        ↓    ↓
      |         ↓   4010 + 30   // operator evaluation7030 + 40
8010 + 70

Де під час усієї оцінки використовуються старі значення задіяних змінних. Це особливо стосується значення k(див. Мій огляд ІЛ нижче та посилання Вай Ха Лі). Отже, ви отримуєте не 70 + 40 (нове значення k) = 110, а 70 + 10 (старе значення k) = 80.

Справа в тому, що (відповідно до специфікації C # ) "Операнди у виразі обчислюються зліва направо" (операнди є змінними, cі kв нашому випадку). Це не залежить від переваги оператора та асоціативності, які в цьому випадку диктують порядок виконання справа наліво. (Див. Коментарі до Еріка Ліпперта відповіді на цій сторінці).


А тепер давайте розглянемо ІЛ. IL передбачає віртуальну машину на основі стеку, тобто вона не використовує регістри.

IL_0007: ldloc.0      // k (is 10)
IL_0008: ldloc.1      // c (is 30)
IL_0009: ldloc.0      // k (is 10)
IL_000a: ldloc.1      // c (is 30)

Тепер стек виглядає так (зліва направо; верх стека - справа)

10 30 10 30

IL_000b: add          // pops the 2 top (right) positions, adds them and pushes the sum back

10 30 40

IL_000c: dup

10 30 40 40

IL_000d: stloc.0      // k <-- 40

10 30 40

IL_000e: add

10 70

IL_000f: dup

10 70 70

IL_0010: stloc.1      // c <-- 70

10 70

IL_0011: add

80

IL_0012: stloc.0      // k <-- 80

Зауважте, що IL_000c: dup , IL_000d: stloc.0тобто перше призначення k , може бути оптимізовано. Можливо, це робиться для змінних джиттером при перетворенні ІЛ у машинний код.

Також зауважте, що всі значення, необхідні для обчислення, або переносяться в стек перед виконанням будь-якого призначення, або обчислюються з цих значень. Присвоєні значення (by stloc) ніколи не використовуються повторно під час цього оцінювання.stlocвискакує верх стека.


Результатом наступного тесту консолі є (Release режим з увімкненими оптимізаціями)

оцінка k (10)
оцінка c (30)
оцінка k (10)
оцінка c (30)
40, призначена k
70, призначена c
80, призначена k

private static int _k = 10;
public static int k
{
    get { Console.WriteLine($"evaluating k ({_k})"); return _k; }
    set { Console.WriteLine($"{value} assigned to k"); _k = value; }
}

private static int _c = 30;
public static int c
{
    get { Console.WriteLine($"evaluating c ({_c})"); return _c; }
    set { Console.WriteLine($"{value} assigned to c"); _c = value; }
}

public static void Test()
{
    k += c += k += c;
}

Ви можете додати кінцевий результат із цифрами у формулі для ще більш повного: final є k = 10 + (30 + (10 + 30)) = 80і це cкінцеве значення встановлюється в першій дужці, яка є c = 30 + (10 + 30) = 70.
Франк

2
Дійсно, якщо kлокальний, то мертвий магазин майже напевно видаляється, якщо ввімкнено оптимізацію, і зберігається, якщо його немає. Цікавим питанням є те, чи дозволено джиттеру вимикати мертвий магазин, якщо kце поле, властивість, слот масиву тощо; на практиці я вважаю, що це не так.
Ерік Ліпперт,

Консольний тест у режимі випуску справді показує, що kпризначається двічі, якщо це властивість.
Олів'є Якот-Декомб

26

По-перше, відповіді Хенка та Олів’є правильні; Я хочу пояснити це дещо по-іншому. Зокрема, я хочу розглянути це питання, яке ви зробили. У вас є такий набір тверджень:

int k = 10;
int c = 30;
k += c += k += c;

І ви неправильно робите висновок, що це повинно давати той самий результат, що і цей набір тверджень:

int k = 10;
int c = 30;
k += c;
c += k;
k += c;

Інформативно побачити, як ви зробили таку помилку і як це правильно зробити. Правильний спосіб його розбити - такий.

Спочатку перепишіть зовнішній + =

k = k + (c += k += c);

По-друге, перепишіть крайній +. Сподіваюсь, ви погоджуєтесь з тим, що x = y + z завжди має збігатися з "оцінити y тимчасовим, оцінити z тимчасовим, підсумувати тимчасові, призначити суму х" . Тож давайте зробимо це дуже явним:

int t1 = k;
int t2 = (c += k += c);
k = t1 + t2;

Переконайтеся, що це зрозуміло, адже це крок, який ви помилилися . Розбиваючи складні операції на більш прості, ви повинні переконатися, що робите це повільно та обережно і не пропускаєте кроки . Пропускаючи кроки, ми робимо помилки.

Добре, тепер розбийте призначення на t2, ще раз, повільно і обережно.

int t1 = k;
int t2 = (c = c + (k += c));
k = t1 + t2;

Присвоєння присвоює тому самому значенню t2, що і присвоєне c, тож припустимо, що:

int t1 = k;
int t2 = c + (k += c);
c = t2;
k = t1 + t2;

Чудово. Тепер розбийте другий рядок:

int t1 = k;
int t3 = c;
int t4 = (k += c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Чудово, ми робимо прогрес. Розбийте призначення на t4:

int t1 = k;
int t3 = c;
int t4 = (k = k + c);
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

Тепер розбийте третій рядок:

int t1 = k;
int t3 = c;
int t4 = k + c;
k = t4;
int t2 = t3 + t4;
c = t2;
k = t1 + t2;

І тепер ми можемо поглянути на все це:

int k = 10;  // 10
int c = 30;  // 30
int t1 = k;  // 10
int t3 = c;  // 30
int t4 = k + c; // 40
k = t4;         // 40
int t2 = t3 + t4; // 70
c = t2;           // 70
k = t1 + t2;      // 80

Отже, коли ми закінчимо, k дорівнює 80, а c дорівнює 70.

Тепер давайте розглянемо, як це реалізовано в ІЛ:

int t1 = k;
int t3 = c;  
  is implemented as
ldloc.0      // stack slot 1 is t1
ldloc.1      // stack slot 2 is t3

Зараз це трохи хитро:

int t4 = k + c; 
k = t4;         
  is implemented as
ldloc.0      // load k
ldloc.1      // load c
add          // sum them to stack slot 3
dup          // t4 is stack slot 3, and is now equal to the sum
stloc.0      // k is now also equal to the sum

Ми могли б реалізувати вищезазначене як

ldloc.0      // load k
ldloc.1      // load c
add          // sum them
stloc.0      // k is now equal to the sum
ldloc.0      // t4 is now equal to k

але ми використовуємо трюк "dup", оскільки це робить код коротшим і полегшує тремтіння, і ми отримуємо той самий результат. Взагалі, генератор коду C # намагається тримати тимчасові "швидкоплинні" у стосі, наскільки це можливо. Якщо вам легше стежити за ІЛ із меншою кількістю ефемер, увімкніть оптимізацію від , і генератор коду буде менш агресивним.

Тепер нам потрібно зробити той самий трюк, щоб отримати c:

int t2 = t3 + t4; // 70
c = t2;           // 70
  is implemented as:
add          // t3 and t4 are the top of the stack.
dup          
stloc.1      // again, we do the dup trick to get the sum in 
             // both c and t2, which is stack slot 2.

і, нарешті:

k = t1 + t2;
  is implemented as
add          // stack slots 1 and 2 are t1 and t2.
stloc.0      // Store the sum to k.

Оскільки нам не потрібна сума ні для чого іншого, ми не дублюємо її. Зараз стек порожній, і ми в кінці заяви.

Мораль історії така: коли ви намагаєтеся зрозуміти складну програму, завжди розбивайте операції по черзі . Не слід робити короткі скорочення; вони заблукають вас.


3
@ OlivierJacot-Descombes: Відповідний рядок специфікації знаходиться в розділі "Оператори" і говорить "Операнди у виразі обчислюються зліва направо. Наприклад, у F(i) + G(i++) * H(i), метод F викликається із використанням старого значення i, а потім методу G викликається зі старим значенням i, і, нарешті, метод H викликається з новим значенням i . Це окремо і не пов'язано з пріоритетом оператора. " (Підкреслюється.) Тож, мабуть, я помилився, сказавши, що ніде не трапляється "використовується старе значення"! Це трапляється у прикладі. Але нормативний біт - "зліва направо".
Ерік Ліпперт,

1
Це було відсутнє посилання. Квінтесенція полягає в тому, що ми повинні розрізняти порядок оцінки операндів та пріоритет оператора . Оцінка операнда йде зліва направо, а у випадку з оператором OP - справа наліво.
Олів'є Якот-Декомб

4
@ OlivierJacot-Descombes: Це точно так. Прецедентність та асоціативність не мають нічого спільного з порядком, в якому оцінюються підвирази, крім того, що пріоритетність та асоціативність визначають, де знаходяться межі субекспресії . Субекспресія оцінюється зліва направо.
Ерік Ліпперт,

1
На жаль, ви не можете перевантажити оператори присвоєння: /
Джонні, 5

1
@ johnny5: Це правильно. Але ви можете перевантажити +, і тоді ви отримаєте +=безкоштовно, тому що x += yвизначається як x = x + yOsim x, що оцінюється лише один раз. Це вірно незалежно від того +, вбудований чи визначений користувачем. Отже: спробуйте перевантажити +контрольний тип і подивіться, що станеться.
Ерік Ліпперт

14

Це зводиться до: це перше +=застосування до оригіналуk чи до значення, яке було обчислено більше праворуч?

Відповідь полягає в тому, що, хоча призначення прив'язуються справа наліво, операції все одно тривають зліва направо.

Тож крайній лівий +=виконує 10 += 70.


1
Це красиво поміщає його в шкаралупу горіха.
Aganju

Це фактично операнди, які оцінюються зліва направо.
Олів'є Жако-Декомб

0

Я спробував приклад з gcc і pgcc і отримав 110. Я перевірив IR, який вони створили, і компілятор розширив expr до:

k = 10;
c = 30;
k = c+k;
c = c+k;
k = c+k;

що мені здається розумним.


-1

для такого роду ланцюгових призначень вам слід призначати значення, починаючи з самого правого боку. Вам потрібно призначити, обчислити і призначити це лівій стороні, і перейти до цього аж до остаточного (крайнього лівого завдання), Звичайно, це обчислюється як k = 80.


Будь ласка, не публікуйте відповіді, які просто повторюють те, що вже зазначено в багатьох інших відповідях.
Ерік Ліпперт,

-1

Проста відповідь: Замініть vars значеннями, і ви зрозуміли:

int k = 10;
int c = 30;
k += c += k += c;
10 += 30 += 10 += 30
= 10 + 30 + 10 + 30
= 80 !!!

Ця відповідь неправильна. Хоча ця методика працює у цьому конкретному випадку, цей алгоритм взагалі не працює. Наприклад, k = 10; m = (k += k) + k;не означає m = (10 + 10) + 10. Мови з мутуючими виразами не можна аналізувати так, ніби вони мають бажання замінити значення . Заміна значення відбувається в певному порядку щодо мутацій, і ви повинні це врахувати.
Ерік Ліпперт,

-1

Ви можете вирішити це, підрахувавши.

a = k += c += k += c

Є два cs і два ks так

a = 2c + 2k

І, як наслідок операторів мови, k також дорівнює2c + 2k

Це буде працювати для будь-якої комбінації змінних у цьому стилі ланцюжка:

a = r += r += r += m += n += m

Тому

a = 2m + n + 3r

І rзрівнятиметься з тим самим.

Ви можете обчислити значення інших чисел, обчислюючи лише до самого крайнього лівого призначення. Тож mрівне 2m + nі nрівне n + m.

Це демонструє те, що k += c += k += c;відрізняється від k += c; c += k; k += c;і, отже, чому ви отримуєте різні відповіді.

Деякі люди в коментарях, здається, переживають, що ви можете спробувати узагальнити цей ярлик на всі можливі типи додавання. Отже, я чітко поясню, що цей ярлик застосовний лише до цієї ситуації, тобто зв’язуючи між собою призначення додавання для вбудованих типів чисел. Це (необов’язково) не працює, якщо ви додаєте інші оператори, наприклад, ()або +, або якщо ви викликаєте функції, або якщо ви перевизначили +=, або якщо ви використовуєте щось інше, ніж основні типи чисел. Він призначений лише для того, щоб допомогти у конкретній ситуації у питанні .


Це не відповідає на запитання
Джонні, 5

@ johnny5 це пояснює, чому ви отримуєте результат, який отримуєте, тобто тому, що так працює математика.
Метт Еллен,

2
Математика та порядки операцій, за допомогою яких компілятор евакуює твердження, - це дві різні речі. За вашою логікою k + = c; c + = k; k + = c має оцінювати той самий результат.
johnny 5

Ні, Джонні 5, це не означає. Математично це різні речі. Три окремі операції оцінюються як 3c + 2k.
Метт Еллен,

2
На жаль, ваше "алгебраїчне" рішення є лише випадково правильним. Ваша техніка взагалі не працює . Поміркуйте x = 1;і y = (x += x) + x;чи вважаєте Ви твердженням, що "існує три х, а значить, y дорівнює 3 * x"? Тому що yдорівнює 4в цьому випадку. А як щодо y = x + (x += x);вашого твердження, що алгебраїчний закон "a + b = b + a" виконаний, і це також 4? Оскільки це 3. На жаль, C # не дотримується правил алгебри середньої школи, якщо у виразах є побічні ефекти . C # дотримується правил побічної алгебри.
Ерік Ліпперт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.