Я читаю трохи тексту Java і отримав такий код:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
У тексті автор не дав чітких пояснень, і ефект останнього рядка такий: a[1] = 0;
Я не настільки впевнений, що розумію: як сталося оцінювання?
Я читаю трохи тексту Java і отримав такий код:
int[] a = {4,4};
int b = 1;
a[b] = b = 0;
У тексті автор не дав чітких пояснень, і ефект останнього рядка такий: a[1] = 0;
Я не настільки впевнений, що розумію: як сталося оцінювання?
Відповіді:
Дозвольте сказати це дуже чітко, бо люди весь час це неправильно розуміють:
Порядок оцінки підвиразів не залежить як від асоціативності, так і від переваги . Асоціативність та пріоритет визначають, в якому порядку виконуються оператори , але не визначають, в якому порядку оцінюються підвирази . Ваше питання стосується порядку, в якому оцінюються підвирази .
Поміркуйте 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.
Майстерна відповідь Еріка Ліпперта тим не менш не є корисною, оскільки мова йде про іншу мову. Це Java, де специфікація мови Java є остаточним описом семантики. Зокрема, п. 15.26.1 є важливим, оскільки він описує порядок оцінки для =
оператора (всі ми знаємо, що він є право-асоціативним, так?). Скоротивши це трохи до бітів, про які ми дбаємо у цьому питанні:
Якщо вираз лівого операнда є виразом доступу до масиву ( §15.13 ), тоді потрібно багато кроків:
- Спочатку обчислюється підвираз посилання на масив виразу доступу до масиву лівого операнда. Якщо це обчислення завершується різко, тоді вираз присвоєння завершується раптово з тієї ж причини; підвираз індексу (виразу доступу до масиву лівого операнда) та правого операнда не обчислюються, і призначення не відбувається.
- В іншому випадку обчислюється підвираз індексу виразу доступу до масиву лівого операнда. Якщо це обчислення завершується різко, тоді вираз присвоєння завершується раптово з тієї ж причини, а правий операнд не обчислюється і присвоєння не відбувається.
- В іншому випадку обчислюється правий операнд. Якщо це обчислення завершується різко, тоді вираз присвоєння завершується раптово з тієї ж причини, і призначення не відбувається.
[… Далі він описує фактичний зміст самого завдання, яке ми можемо тут ігнорувати для стислості…]
Коротше кажучи, Java має дуже чітко визначений порядок оцінки, який є майже точно зліва направо в межах аргументів до будь-якого виклику оператора або методу. Призначення масивів є одним із найскладніших випадків, але навіть там це все ще L2R. (JLS рекомендує не писати код, який потребує такого роду складних семантичних обмежень , і я також: я можу зіткнутися з більш ніж достатньою проблемою лише за допомогою одного призначення на оператор!)
C і C ++, безумовно, відрізняються від Java у цій галузі: їх визначення мови залишають порядок оцінки невизначеним навмисно, щоб забезпечити більше оптимізації. C #, схоже, на Java, але я недостатньо добре знаю її літературу, щоб мати можливість вказати на офіційне визначення. (Це дійсно залежить від мови , хоча, Ruby строго L2R, як Tcl - хоча це не має оператор присвоювання по собі є з причин , які не відповідають тут - і Python є L2R але R2L щодо поступки , які я знаходжу дивним , але там ви йдете .)
a[-1]=c
, c
обчислюється, перш ніж -1
буде визнано недійсним.
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)
оцінюється останнім
Ваш код еквівалентний:
int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;
що пояснює результат.
Розглянемо інший більш поглиблений приклад нижче.
При вирішенні цих питань найкраще мати доступну для читання таблицю Правил порядку переваги та асоціативності, наприклад, 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/
10 - 4 - 3
.
+
- одинарний оператор (який має право на ліву асоціативність), але адитивні + та - мають як мультиплікативний * /% лівий праворуч асоціативність.