Чому x == (x = y) не те саме, що (x = y) == x?


207

Розглянемо наступний приклад:

class Quirky {
    public static void main(String[] args) {
        int x = 1;
        int y = 3;

        System.out.println(x == (x = y)); // false
        x = 1; // reset
        System.out.println((x = y) == x); // true
     }
}

Я не впевнений, чи є елемент у специфікації мови Java, який диктує завантаження попереднього значення змінної для порівняння з правою стороною ( x = y), яка за порядком, що має на увазі дужки, має бути обчислена спочатку.

Чому перший вираз оцінюється так false, а другий - true? Я б сподівався, що (x = y)спочатку його оцінюють, а потім порівнюють xіз собою ( 3) і повертають true.


Це питання відрізняється від порядку оцінки підвиразів у виразі Java тим, що xтут, безумовно, не є «субекспресією». Його потрібно завантажувати для порівняння, а не «оцінювати». Питання є специфічним для Java, і вираз x == (x = y), на відміну від надуманих непрактичних конструкцій, зазвичай створених для складних запитань щодо інтерв'ю, походить від реального проекту. Це мало бути однорядковою заміною ідіоми порівняння та заміни

int oldX = x;
x = y;
return oldX == y;

який, навіть будучи простішим за інструкцію x86 CMPXCHG, заслужив коротший вираз на Java.


62
Ліва частина завжди оцінюється перед правою. Дужки не мають жодного значення для цього.
Луї Вассерман

11
Оцінка виразу x = y, безумовно, є актуальною і викликає побічний ефект, xвстановлений на значення y.
Луї Вассерман

50
Зробіть собі і своїм товаришам по службі послугу, і не змішуйте мутації держави в тій же лінії, що і державна експертиза. Це різко знижує читабельність вашого коду. (Є деякі випадки, коли це абсолютно необхідно через вимоги до атомності, але функції для тих, хто вже існує, і їх призначення буде негайно розпізнано.)
jpmc26

50
Справжнє питання, чому ви хочете писати такий код.
клотт

26
Ключовим фактором вашого питання є помилкове переконання, що дужки передбачають порядок оцінки. Це поширена віра через те, як нас навчають математиці в початковій школі і тому, що деякі програми для початківців програмування все-таки помиляються , але це помилкова віра. Це досить часте питання. Ви можете отримати користь від прочитання моїх статей на цю тему; вони про C #, але вони стосуються Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Ерік Ліпперт

Відповіді:


97

який за наказом, що міститься в дужках, слід розраховувати спочатку

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

(І якщо ви не використовуєте їх, ця інформація походить від "пріоритетності" та асоціативності операторів, що є результатом того, як визначено синтаксичне дерево мови. Насправді, це все одно саме так працює, коли ви використовувати дужки, але ми спрощуємо і кажемо, що тоді ми не покладаємось на будь-які правила пріоритетності.)

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

Зауважте, що це не так у всіх мовах; наприклад, в C ++, якщо ви не використовуєте оператора короткого замикання типу, &&або ||, порядок оцінки операндів, як правило, не визначений, і ви не повинні покладатися на нього в будь-якому випадку.

Вчителі повинні припинити пояснювати перевагу оператора, використовуючи оманливі фрази на кшталт "це робить додавання спочатку". З огляду на вираз x * y + zправильного пояснення було б «оператор старшинство робить додавання статися між x * yі z, а не між yі z», без згадки про яке - або «замовленні».


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

1
Радий, що ви згадали, що це правило не відповідає всім мовам. Крім того, якщо будь-яка сторона має інший побічний ефект, як-от запис у файл чи читання поточного часу, він (навіть на Java) не визначений у тому порядку, що це станеться. Однак результат порівняння буде таким, як якщо б його оцінювали зліва направо (на Java). Ще одна сторона: досить багато мов просто забороняють змішувати призначення та порівнювати таким чином правила синтаксису, і проблеми не виникнуть.
Авель

5
@JohnP: Погіршується. Чи означає 5 * 4 5 + 5 + 5 + 5 або 4 + 4 + 4 + 4 + 4? Деякі викладачі наполягають на тому, що лише один із цих варіантів є правильним.
Брайан

3
@Brian Але ... але ... множення реальних чисел є комутативним!
Гонки легкості на орбіті

2
У моєму світі мислення пара круглих дужок являє собою «потрібне для». Обчислюючи ´a * (b + c) ´, дужки виражають, що результат множення потрібен для множення. Будь-які неявні переваги оператора можуть бути виражені паренами, за винятком LHS-першого або RHS-першого правила. (Це правда?) @Brian У математиці є кілька рідкісних випадків, коли множення можна замінити повторним додаванням, але це далеко не завжди правда (починаючи зі складних чисел, але не обмежуючись). Тож ваші викладачі повинні справді придивлятися до того, що говорять людям ....
syck

164

==є оператором бінарної рівності .

Лівий операнд двійкового оператора, здається, повністю оцінений до того, як буде оцінена будь-яка частина правого операнда .

Специфікація Java 11> Порядок оцінювання> Спочатку оцініть операнд ліворуч


42
Формулювання "начебто" не виглядає так, як вони впевнені, тбг.
Містер Лістер

86
"Схоже, є" означає, що специфікація не вимагає, щоб операції були виконані в такому порядку хронологічно, але це вимагає, щоб ви отримали той самий результат, який ви отримали, як і вони.
Робін

24
@MrLister "Здається," виявляється поганим вибором слова з їх боку. Під "з'являються" вони означають "проявлятися як явище для розробника". "Ефективно" може бути кращою фразою.
Кельвін

18
у спільноті C ++ це еквівалент правилу "як-ніби" ... операнд повинен поводитися "так, ніби" він був реалізований за наступними правилами, навіть якщо технічно це не так.
Майкл Еденфілд

2
@Kelvin Я погоджуюся, я б вибрав це слово, а не "Здається".
MC імператор

149

Як сказав Луї Васерман, вираз оцінюється зліва направо. І Java не байдуже, що насправді робить "оцінка", вона піклується лише про те, щоб створити (нелетуче, остаточне) значення, з яким потрібно працювати.

//the example values
x = 1;
y = 3;

Отже, щоб обчислити перший вихід System.out.println(), робиться наступне:

x == (x = y)
1 == (x = y)
1 == (x = 3) //assign 3 to x, returns 3
1 == 3
false

і обчислити друге:

(x = y) == x
(x = 3) == x //assign 3 to x, returns 3
3 == x
3 == 3
true

Зауважте, що друге значення завжди буде оцінюватись як істинне, незалежно від початкових значень xта y, оскільки ви ефективно порівнюєте присвоєння значення змінній, якій воно призначено, a = bі bбуде, оцінене в тому порядку, завжди буде однаковим за визначенням.


"Зліва направо" також відповідає математиці, до речі, саме тоді, коли ви дістаєтесь до дужок або пріоритету, ви повторюєте всередині них і вирівнюєте все, що знаходиться зліва направо, перш ніж йти далі на основний рівень. Але математика ніколи цього не зробить; відмінність має значення лише тому, що це не рівняння, а комбінована операція, виконуючи як завдання, так і рівняння одним махом . Я б ніколи цього не робив, оскільки читабельність погана, якщо б я не займався кодом гольфу або шукав спосіб оптимізувати продуктивність, і тоді, будуть коментарі.
Харпер - Відновіть Моніку

25

Я не впевнений, чи є елемент у специфікації мови Java, який диктує завантаження попереднього значення змінної ...

Є. Наступного разу, коли вам буде незрозуміло, що говорить специфікація, будь ласка, прочитайте специфікацію, а потім задайте питання, якщо це не зрозуміло.

... права сторона, (x = y)яка, в порядку, що міститься в дужках, повинна бути обчислена спочатку.

Це твердження хибне. Дужки не передбачають порядок оцінювання . У Java порядок оцінювання зліва направо, незалежно від дужок. В дужках визначають, де знаходяться межі субекспресії, а не порядок оцінки.

Чому перший вираз оцінюється як хибний, а другий - істинний?

Правило для ==оператора: оцінити ліву частину для отримання значення, оцінити праву частину для отримання значення, порівняти значення, порівняння - це значення виразу.

Іншими словами, значення expr1 == expr2завжди таке ж, як якщо б ви написали temp1 = expr1; temp2 = expr2;і потім оцінили temp1 == temp2.

Правило для =оператора з локальною змінною на лівій стороні: оцінити ліву частину для отримання змінної, оцінити праву частину для отримання значення, виконати призначення, результат - значення, яке було призначено.

Тож складіть це разом:

x == (x = y)

У нас є оператор порівняння. Оцініть ліву сторону для отримання значення - ми отримаємо поточне значення x. Оцініть праву сторону: це призначення, тому ми оцінюємо ліву сторону для отримання змінної - змінної x- ми оцінюємо праву сторону - поточне значення y- присвоюємо їй x, а результат - присвоєне значення. Потім ми порівнюємо початкове значення з xприсвоєним значенням.

Можна робити (x = y) == xяк вправу. Знову ж, пам’ятайте, всі правила оцінювання лівої сторони трапляються раніше, ніж усі правила оцінки правої сторони .

Я б очікував, що (x = y) спочатку буде оцінено, а потім він порівняє x із самим собою (3) і повернеться істинним.

Ваше очікування базується на наборі неправильних переконань щодо правил Java. Сподіваємось, тепер у вас є правильні переконання і в майбутньому очікуєте справжніх речей.

Це питання відрізняється від "порядку оцінювання підекспресій у виразі Java"

Це твердження хибне. Це питання є абсолютно германським.

x, безумовно, тут не є «субекспресією».

Це твердження також хибне. Це субекспресія двічі у кожному прикладі.

Його потрібно завантажувати для порівняння, а не «оцінювати».

Я поняття не маю, що це означає.

Мабуть, у вас ще багато помилкових переконань. Моя порада - ви прочитаєте специфікацію, поки ваші помилкові переконання не будуть замінені справжніми переконаннями.

Питання є специфічним для Java, а вираз x == (x = y), на відміну від надуманих непрактичних конструкцій, які зазвичай розробляються для складних питань інтерв'ю, походить від реального проекту.

Походження виразу не стосується питання. Правила таких виразів чітко описані в специфікації; читати!

Це мало бути однорядковою заміною ідіоми порівняння та заміни

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

Між іншим, C # має порівняти та замінити як метод бібліотеки, який може бути приведений до машинної інструкції. Я вважаю, що у Java немає такого методу, оскільки він не може бути представлений у системі типу Java.


8
Якби хто-небудь міг пройти всю JLS, тоді не було б ніяких причин публікувати книги Java, і принаймні половина цього веб-сайту теж була б марною.
Джон Макклайн

8
@JohnMcClane: Запевняю вас, немає ніяких труднощів у перегляді всієї специфікації, але також, вам цього не потрібно. Специфікація Java починається з корисної "змісту", яка допоможе вам швидко дістатися до тих частин, які вас найбільше цікавлять. Це також Інтернет та пошук за ключовими словами. Однак, ви маєте рацію: є багато хороших ресурсів, які допоможуть вам дізнатися, як працює Java; моя рада вам - ви користуєтесь ними!
Ерік Ліпперт

8
Ця відповідь є надмірно поблажливою та грубою. Пам'ятайте: будьте приємні .
walen

7
@LuisG. Жодна поблажливість не передбачається і не передбачається; всі ми тут, щоб вчитися один у одного, і я не рекомендую нічого, чого я сам не робив, коли був початківцем. Не грубо. Чітке та однозначне виявлення їх помилкових переконань є добротою до оригінального афіші . Ховатися за «ввічливістю» і дозволяти людям продовжувати мати помилкові переконання - це не корисно і зміцнює шкідливі звички мислення .
Ерік Ліпперт

5
@LuisG: Я писав блог про дизайн JavaScript, і найкорисніші коментарі, які я коли-небудь отримував, були від Брендана, вказуючи чітко і однозначно, де я помилявся. Це було чудово, і я оцінив, що він витратив час, тому що я прожив наступні 20 років свого життя, не повторюючи цієї помилки у власній роботі, або ще гірше, навчаючи її іншим. Це також дало мені можливість виправити ті самі помилкові переконання в інших, використовуючи себе як приклад того, як люди приходять до віри фальшивим речам.
Ерік Ліпперт

16

Це пов'язано з пріоритетом оператора та способом оцінки операторів.

Дужки "()" мають вищий пріоритет і мають асоціативність зліва направо. Рівність '==' стоїть далі у цьому питанні та має асоціативність зліва направо. Призначення '=' закінчується останнім і має асоціативність праворуч ліворуч.

Система використання стека для оцінки вираження. Вираз оцінюється зліва направо.

Тепер приходить до оригінального питання:

int x = 1;
int y = 3;
System.out.println(x == (x = y)); // false

Спочатку х (1) буде натиснуто на стек. тоді внутрішня (x = y) буде оцінена і підсунута до стеку зі значенням x (3). Тепер x (1) буде порівнюватися з x (3), тому результат хибний.

x = 1; // reset
System.out.println((x = y) == x); // true

Тут буде оцінено (x = y), тепер значення x стане 3, а x (3) буде висунуто в стек. Тепер x (3) зі зміненим значенням після рівності буде висунуто на стек. Тепер вираження буде оцінено, і обидва будуть однаковими, тому результат справдиться.


12

Це не те саме. Ліва частина завжди буде оцінюватися перед правою частиною, а в дужках не вказаний порядок виконання, а групування команд.

З:

      x == (x = y)

Ви в основному робите те саме, що:

      x == y

І x матиме значення y після порівняння.

Хоча з:

      (x = y) == x

Ви в основному робите те саме, що:

      x == x

Після x взяв значення y . І це завжди поверне справжню .


9

У першому тесті, який ви перевіряєте, чи 1 == 3.

У другому тесті перевірка робить 3 == 3.

(x = y) призначає значення, і це значення тестується. У першому прикладі x = 1 спочатку x присвоюється 3. Чи 1 == 3?

В останньому x присвоюється 3, і, очевидно, це ще 3. Чи 3 == 3?


8

Розглянемо цей інший, можливо, простіший приклад:

int x = 1;
System.out.println(x == ++x); // false
x = 1; // reset
System.out.println(++x == x); // true

Тут оператор перед збільшенням ++xповинен бути застосований перед порівнянням - так само, як (x = y)у вашому прикладі, слід розраховувати перед порівнянням.

Однак оцінка вираження все ще відбувається зліва → вправо , тому перше порівняння насправді, 1 == 2а друге 2 == 2.
Те ж саме відбувається і у вашому прикладі.


8

Вирази оцінюються зліва направо. В цьому випадку:

int x = 1;
int y = 3;

x == (x = y)) // false
x ==    t

- left x = 1
- let t = (x = y) => x = 3
- x == (x = y)
  x == t
  1 == 3 //false

(x = y) == x); // true
   t    == x

- left (x = y) => x = 3
           t    =      3 
-  (x = y) == x
-     t    == x
-     3    == 3 //true

5

В основному перший випадок x мав його значення 1. Отже, Java порівнює 1 == з новою змінною x, яка не буде однаковою

У другому ви сказали x = y, що означає, що значення x змінилося, і тому, коли ви його знову зателефонуєте, це буде те саме значення, отже, чому це правда і x == x


4

== - оператор рівності порівняння, і він працює зліва направо.

x == (x = y);

тут старе присвоєне значення x порівнюється з новим значенням присвоєння x, (1 == 3) // false

(x = y) == x;

Беручи до уваги, що тут нове значення присвоєння x порівнюється з новим значенням утримування x, призначеним йому безпосередньо перед порівнянням, (3 == 3) // вірно

Тепер розглянемо це

    System.out.println((8 + (5 * 6)) * 9);
    System.out.println(8 + (5 * 6) * 9);
    System.out.println((8 + 5) * 6 * 9);
    System.out.println((8 + (5) * 6) * 9);
    System.out.println(8 + 5 * 6 * 9);

Вихід:

342

278

702

342

278

Таким чином, парентези відіграють свою головну роль в арифметичних виразах не лише у порівнянні виразів.


1
Висновок неправильний. Поведінка не відрізняється між арифметичними та операторами порівняння. x + (x = y)і (x = y) + xпоказала б схожу поведінку, як оригінал із операторами порівняння.
JJJ

1
@JJJ У x + (x = y) і (x = y) + x порівняння не бере участь, це просто призначення y значення до x та додавання його до x.
Нісрін Дхундія

1
... так, у цьому справа. "Парентези відіграють свою головну роль в арифметичних виразах тільки не в порівнянні виразів", це неправильно, оскільки немає різниці між арифметичними та порівняльними виразами.
JJJ

2

Тут річ у тому, що арифмічні оператори / реляційні оператори порядком пріоритетності між двома операторами =та ==домінуючим є ==(Реляційні оператори домінують), оскільки це передує =операторам присвоєння. Незважаючи на перевагу, порядок оцінювання є LTR (НАЛЯГА ВПРАВО), пріоритет вступає в зображення після порядку оцінки. Отже, незалежно від будь-яких оцінок обмежень є LTR.


1
Відповідь неправильна. Пріоритет оператора не впливає на порядок оцінки. Прочитайте декілька найвищих відповідей для пояснень, особливо цього .
JJJ

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

-1

Легко у другому порівнянні зліва - це призначення після призначення y до x (ліворуч), то ви порівнюєте 3 == 3. У першому прикладі ви порівнюєте x = 1 з новим призначенням x = 3. Здається, що завжди приймаються поточні заяви про читання зліва направо від x.


-2

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

Оскільки розробник програмного забезпечення, написання коду, який є читабельним, зрозумілим та досяжним, обидві версії вашого коду вважаються жахливими. Щоб зрозуміти, що робить код, треба точно знати , як визначена мова Java. Хтось, хто пише як код Java, так і C ++, здригнеться, дивлячись на код. Якщо вам доведеться запитати, чому один рядок коду робить те, що він робить, тоді вам слід уникати цього коду. (Я гадаю і сподіваюся, що хлопці, які відповіли правильно на ваше запитання "чому", самі також уникнуть цього індексу коду).


"Щоб зрозуміти, що робить код, треба точно знати, як визначена мова Java." Але що робити, якщо кожен колега вважає це здоровим глуздом?
BinaryTreeee
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.