Які правила для порядку оцінки в Java?


86

Я читаю трохи тексту Java і отримав такий код:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;

У тексті автор не дав чітких пояснень, і ефект останнього рядка такий: a[1] = 0;

Я не настільки впевнений, що розумію: як сталося оцінювання?


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

Правильна відповідь на будь-яке подібне запитання - "не роби цього!" Доручення слід розглядати як заяву; використання призначення як вираз, що повертає значення, повинно викликати помилку компілятора або як мінімум попередження. Не роби цього.
Mason Wheeler

Відповіді:


173

Дозвольте сказати це дуже чітко, бо люди весь час це неправильно розуміють:

Порядок оцінки підвиразів не залежить як від асоціативності, так і від переваги . Асоціативність та пріоритет визначають, в якому порядку виконуються оператори , але не визначають, в якому порядку оцінюються підвирази . Ваше питання стосується порядку, в якому оцінюються підвирази .

Поміркуйте A() + B() + C() * D(). Множення має вищий пріоритет, ніж додавання, а додавання є лівоасоціативним, тож це еквівалентно, (A() + B()) + (C() * D()) але знання цього лише говорить вам, що перше додавання відбудеться перед другим додаванням, і що множення відбудеться перед другим додаванням. Це не говорить вам, в якому порядку будуть називатися A (), B (), C () і D ()! (Це також не говорить вам, чи відбувається множення до або після першого додавання.) Цілком можливо було б дотримуватися правил пріоритету та асоціативності , склавши це як:

d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b      // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

Там дотримуються всіх правил пріоритету та асоціативності - перше додавання відбувається до другого додавання, а множення відбувається до другого додавання. Очевидно, що ми можемо здійснювати дзвінки до A (), B (), C () і D () у будь-якому порядку і при цьому дотримуватися правил пріоритету та асоціативності!

Нам потрібне правило, не пов’язане з правилами переваги та асоціативності, щоб пояснити порядок, в якому оцінюються підвирази. Відповідне правило в Java (та C #): "підвирази обчислюються зліва направо". Оскільки A () з'являється ліворуч від C (), A () обчислюється спочатку, незалежно від того, що C () бере участь у множенні, а A () бере участь лише в додаванні.

Отже, тепер у вас є достатньо інформації, щоб відповісти на ваше запитання. У a[b] = b = 0правилах асоціативності сказано, що це є, a[b] = (b = 0);але це не означає, що b=0пробіжки першими! Правила пріоритету говорять, що індексація має вищий пріоритет, ніж присвоєння, але це не означає, що індексатор працює перед самим правим призначенням .

(ОНОВЛЕННЯ: У попередній версії цієї відповіді було кілька невеликих і практично неважливих пропусків у наступному розділі, який я виправив. Я також написав статтю в блозі, де описується, чому ці правила є розумними в Java та C # тут: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

Прецедентність та асоціативність говорять нам лише про те, що присвоєння нуля до bповинно відбуватися до присвоєння a[b], оскільки присвоєння нулю обчислює значення, яке присвоюється в операції індексації. Старшинство і асоціативність в поодинці нічого не говорять про Чи a[b]оцінюється до того чи післяb=0 того, як .

Знову ж, це точно так само, як: A()[B()] = C()- Все, що нам відомо, це те, що індексація повинна відбутися перед призначенням. Ми не знаємо, чи працює A (), B () або C () на основі пріоритету та асоціативності . Нам потрібне інше правило, щоб сказати нам це.

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

То що трапляється?

  • Значок a[b]знаходиться ліворуч від b=0, тому a[b]пробіжки спочатку , в результаті чого a[1]. Однак перевірка дійсності цієї операції індексування затримується.
  • Тоді b=0трапляється.
  • Тоді відбувається перевірка, яка aє дійсною та a[1]знаходиться в діапазоні
  • Присвоєння значення a[1]відбуватиметься останнім.

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

Люди весь час неправильно сприймають ці речі , навіть люди, які повинні знати краще. Я відредагував занадто багато книг з програмування, в яких правила неправильно викладені, тому не дивно, що багато людей мають абсолютно неправильні переконання щодо взаємозв'язку між пріоритетом / асоціативністю та порядком оцінки - а саме, що насправді таких відносин не існує ; вони незалежні.

Якщо ця тема вас цікавить, перегляньте мої статті на цю тему для подальшого читання:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

Вони стосуються C #, але більшість цих матеріалів однаково добре стосуються і Java.


6
Особисто я віддаю перевагу ментальній моделі, де на першому кроці ви будуєте дерево виразів, використовуючи пріоритет та асоціативність. А на другому кроці рекурсивно оцініть це дерево, починаючи з кореня. З оцінкою вузла: Оцініть безпосередньо дочірні вузли зліва направо, а потім саму нотатку. | Однією з переваг цієї моделі є те, що вона дріб’язково обробляє випадок, коли двійкові оператори мають побічний ефект. Але головна перевага полягає в тому, що це просто більше підходить для мого мозку.
CodesInChaos

Я правильно, що С ++ цього не гарантує? Як щодо Python?
Neil G

2
@Neil: C ++ нічого не гарантує щодо порядку оцінки, і ніколи цього не робив. (Також не C.). Python гарантує це суворо за порядком черговості; на відміну від усього іншого, призначення - R2L.
Donal Fellows

2
@aroth для мене ти звучиш просто розгублено. А правила переваги лише передбачають, що дітей потрібно оцінювати перед батьками. Але вони нічого не говорять про порядок оцінки дітей. Java та C # вибрали зліва направо, C та C ++ обрали невизначену поведінку.
CodesInChaos

6
@noober: Добре, розглянемо: M (A () + B (), C () * D (), E () + F ()). Ваше бажання, щоб підвирази оцінювались у якому порядку? Чи слід оцінювати C () та D () перед A (), B (), E () та F (), оскільки множення має вищий пріоритет, ніж додавання? Легко сказати, що "очевидно" порядок повинен бути іншим. Придумати фактичне правило, яке охоплює всі справи, досить складніше. Дизайнери C # та Java обрали просте, легко пояснюване правило: "йти зліва направо". Якою ви пропонуєте замінити його, і чому ви вважаєте, що ваше правило краще?
Ерік Ліпперт,

32

Майстерна відповідь Еріка Ліпперта тим не менш не є корисною, оскільки мова йде про іншу мову. Це Java, де специфікація мови Java є остаточним описом семантики. Зокрема, п. 15.26.1 є важливим, оскільки він описує порядок оцінки для =оператора (всі ми знаємо, що він є право-асоціативним, так?). Скоротивши це трохи до бітів, про які ми дбаємо у цьому питанні:

Якщо вираз лівого операнда є виразом доступу до масиву ( §15.13 ), тоді потрібно багато кроків:

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

[… Далі він описує фактичний зміст самого завдання, яке ми можемо тут ігнорувати для стислості…]

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

C і C ++, безумовно, відрізняються від Java у цій галузі: їх визначення мови залишають порядок оцінки невизначеним навмисно, щоб забезпечити більше оптимізації. C #, схоже, на Java, але я недостатньо добре знаю її літературу, щоб мати можливість вказати на офіційне визначення. (Це дійсно залежить від мови , хоча, Ruby строго L2R, як Tcl - хоча це не має оператор присвоювання по собі є з причин , які не відповідають тут - і Python є L2R але R2L щодо поступки , які я знаходжу дивним , але там ви йдете .)


11
Отже, ви кажете, що відповідь Еріка неправильна, оскільки Java визначає це саме так, як він сказав?
конфігуратор

8
Відповідне правило в Java (і на C #): "підвирази обчислюються зліва направо" - мені здається, він говорить про обидва.
конфігуратор

2
Тут трохи заплутано - чи робить це вищевказану відповідь Еріка Ліпперта менш правдивою, чи це просто посилання на конкретне посилання на те, чому це правда?
GreenieMeanie

5
@Greenie: Відповідь Еріка правдива, але, як я вже зазначав, ви не можете зрозуміти одну мову в цій галузі та застосувати її до іншої, не дотримуючись обережності. Тож я навів остаточне джерело.
Donal Fellows

1
дивним є те, що права оцінюється до того, як ліва змінна буде розв’язана; в a[-1]=c, cобчислюється, перш ніж -1буде визнано недійсним.
ZhongYu

5
a[b] = b = 0;

1) оператор індексації масиву має вищий пріоритет, ніж оператор присвоєння (див. Цю відповідь ):

(a[b]) = b = 0;

2) Відповідно до 15.26. Оператори присвоєння JLS

Існує 12 операторів присвоєння; всі синтаксично право-асоціативні (вони групуються справа наліво). Таким чином, a = b = c означає a = (b = c), який присвоює значення c до b, а потім присвоює значення b до a.

(a[b]) = (b=0);

3) Відповідно до 15.7. Порядок оцінки JLS

Мова програмування Java гарантує, що операнди операторів оцінюються в певному порядку оцінки, а саме зліва направо.

і

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

Тому:

а) (a[b])оцінюється спочатку доa[1]

б) потім (b=0)оцінюється до0

в) (a[1] = 0)оцінюється останнім


1

Ваш код еквівалентний:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;

що пояснює результат.


7
Питання в тому, чому це так.
Mat

@Mat Відповідь полягає в тому, що саме це відбувається під капотом, враховуючи код, наведений у питанні. Ось так відбувається оцінка.
Jérôme Verstrynge

1
Так, я знаю. Поки що IMO не відповідає на запитання, саме тому так відбувається оцінка.
Mat

1
@Mat 'Чому саме так відбувається оцінка?' не задається питання. "як відбулося оцінювання?" - це задане питання.
Jérôme Verstrynge

1
@JVerstry: Як вони не рівнозначні? Підвираз вираження посилання на масив лівого операнда є самим лівим операндом. Отже, сказати "зробити спочатку лівий край" - це точно те саме, що сказати "зробити спочатку посилання на масив". Якщо автори специфікації Java вирішили бути надмірно багатослівними та зайвими в поясненні цього конкретного правила, це добре для них; такі речі заплутані і повинні бути більше, аніж менш багатослівними. Але я не бачу, як моя стисла характеристика семантично відрізняється від їхньої проліксичної характеристики.
Ерік Ліпперт,

0

Розглянемо інший більш поглиблений приклад нижче.

Загальне правило:

При вирішенні цих питань найкраще мати доступну для читання таблицю Правил порядку переваги та асоціативності, наприклад, http://introcs.cs.princeton.edu/java/11precedence/

Ось хороший приклад:

System.out.println(3+100/10*2-13);

Запитання: Який результат отримано у вищезазначеному рядку?

Відповідь: Застосовуйте правила переваги та асоціативності

Крок 1: Відповідно до правил пріоритету: / та * оператори мають пріоритет над + -. Тому вихідна точка для виконання цього рівняння буде звужена до:

100/10*2

Крок 2: Відповідно до правил та пріоритету: / і * мають однакові переваги.

Оскільки оператори / та * мають однакові переваги, нам потрібно розглянути асоціативність між цими операторами.

Відповідно до ПРАВИЛ АСОЦІАЦІЙНОСТІ цих двох конкретних операторів, ми починаємо виконувати рівняння ЗЛІВО ВПРАВО, тобто 100/10 виконується першим:

100/10*2
=100/10
=10*2
=20

Крок 3: Рівняння тепер знаходиться у такому стані виконання:

=3+20-13

Відповідно до правил і пріоритету: + і - рівні за пріоритетом.

Тепер нам потрібно розглянути асоціативність між операторами + та - операторами. Відповідно до асоціативності цих двох конкретних операторів, ми починаємо виконувати рівняння від ВЛІВО до ВПРАВО, тобто спочатку виконується 3 + 20:

=3+20
=23
=23-13
=10

10 - правильний результат при компіляції

Знову ж таки, важливо мати з собою таблицю Правил порядку переваги та асоціативності при вирішенні цих питань, наприклад, http://introcs.cs.princeton.edu/java/11precedence/


1
Ви говорите, що "асоціативність між операторами + та - оператори" - це "ПРАВО НАЛІВО". Спробуйте використати цю логіку та оцінити 10 - 4 - 3.
Пшемо

1
Я підозрюю, що ця помилка може бути викликана тим, що вгорі introcs.cs.princeton.edu/java/11precedence + - одинарний оператор (який має право на ліву асоціативність), але адитивні + та - мають як мультиплікативний * /% лівий праворуч асоціативність.
Пшемо

Добре помічено і внесено відповідні зміни, дякую Пшемо
Марк Берлі

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