Каковы правила порядка оценки в Java?

86

Я читаю текст Java и получаю следующий код:

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

В тексте автор не дал четкого объяснения, и эффект последней строки таков: a[1] = 0;

Не уверен, что понимаю: как прошла оценка?

ipkiss
источник
19
Заблуждение ниже, почему это происходит, кстати, означает, что вы никогда не должны этого делать, так как многим людям придется думать о том, что это на самом деле делает, вместо того, чтобы быть очевидным.
Martijn
Правильный ответ на любой подобный вопрос - «не делай этого!» Назначение следует рассматривать как заявление; использование присваивания в качестве выражения, возвращающего значение, должно вызывать ошибку компилятора или, по крайней мере, предупреждение. Не делай этого.
Мейсон Уиллер

Ответы:

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 выбирает последнее. (Конечно, это различие имеет значение только в том случае, если код уже неверен , потому что правильный код в первую очередь не разыменовывает null и не передает плохой индекс.)

Так что же происходит?

  • Символ 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
Правильно ли я, что C ++ этого не гарантирует? А как насчет 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 в отношении присваивания , что я нахожу странным, но вы идете .)

Donal Fellows
источник
11
Итак, вы говорите, что ответ Эрика неверен, потому что Java определяет его именно так, как он сказал?
конфигуратор
8
Соответствующее правило в Java (и C #) - «подвыражения оцениваются слева направо» - мне кажется, он говорит об обоих.
конфигуратор
2
Здесь немного запутано - делает ли это вышеупомянутый ответ Эрика Липперта менее верным, или это просто ссылка на конкретную ссылку, почему это правда?
GreenieMeanie
6
@Greenie: Ответ Эрика верен, но, как я уже сказал, нельзя брать понимание одного языка в этой области и применять его к другому, не проявляя осторожности. Я процитировал исчерпывающий источник.
Donal Fellows
1
странно то, что правая часть вычисляется до того, как разрешена левая переменная; in 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
Да, я знаю. Все еще не отвечая на вопрос, хотя ИМО, вот почему так происходит оценка.
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.
Pshemo
1
Я подозреваю, что эта ошибка может быть вызвана тем фактом, что вверху introcs.cs.princeton.edu/java/11precedence + стоит унарный оператор (который имеет ассоциативность справа налево), но аддитивные + и - имеют точно такие же мультипликативные * /% left к правильной ассоциативности.
Pshemo
Хорошо подмечено и соответствующим образом изменено, спасибо Пшемо
Марк Берли
Этот ответ объясняет приоритет и ассоциативность, но, как объяснил Эрик Липперт , вопрос заключается в порядке оценки, который сильно отличается. Собственно говоря, это не отвечает на вопрос.
Фабио говорит: "Восстановите Монику"