Jakie są zasady kolejności ocen w Javie?

86

Czytam tekst Java i otrzymałem następujący kod:

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

W tekście autor nie podał jasnego wyjaśnienia, a efektem ostatniej linijki jest: a[1] = 0;

Nie jestem pewien, czy rozumiem: jak doszło do oceny?

ipkiss
źródło
19
Zamieszanie poniżej, dlaczego tak się dzieje, oznacza przy okazji, że nigdy nie powinieneś tego robić, ponieważ wiele osób będzie musiało pomyśleć o tym, co tak naprawdę robi, zamiast być oczywistym.
Martijn
Prawidłowa odpowiedź na takie pytanie brzmi „nie rób tego!” Cesję należy traktować jako oświadczenie; użycie przypisania jako wyrażenia zwracającego wartość powinno wywołać błąd kompilatora lub co najmniej ostrzeżenie. Nie rób tego.
Mason Wheeler,

Odpowiedzi:

173

Powiem to bardzo wyraźnie, ponieważ ludzie cały czas to źle rozumieją:

Kolejność oceny podwyrażeń jest niezależna zarówno od asocjatywności, jak i pierwszeństwa . Asocjatywność i pierwszeństwo określają, w jakiej kolejności operatory są wykonywane, ale nie określają, w jakiej kolejności są oceniane podwyrażenia . Twoje pytanie dotyczy kolejności, w jakiej są oceniane podwyrażenia .

Rozważ A() + B() + C() * D(). Mnożenie ma wyższy priorytet niż dodawanie, a dodawanie jest lewostronne, więc jest to równoważne z (A() + B()) + (C() * D()) twierdzeniem Ale wiedząc, że mówi ci tylko, że pierwsze dodawanie nastąpi przed drugim dodaniem, a mnożenie nastąpi przed drugim dodaniem. Nie mówi, w jakiej kolejności zostaną wywołane A (), B (), C () i D ()! (Nie mówi również, czy mnożenie ma miejsce przed, czy po pierwszym dodaniu). Byłoby całkowicie możliwe przestrzeganie reguł pierwszeństwa i łączności , kompilując to jako:

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

Przestrzegane są tam wszystkie zasady pierwszeństwa i łączności - pierwsze dodawanie następuje przed drugim dodawaniem, a mnożenie następuje przed drugim dodawaniem. Oczywiście możemy wykonywać wywołania A (), B (), C () i D () w dowolnej kolejności i nadal przestrzegać zasad pierwszeństwa i asocjatywności!

Potrzebujemy reguły niezwiązanej z regułami pierwszeństwa i asocjatywności, aby wyjaśnić kolejność, w jakiej podwyrażenia są oceniane. Odpowiednia reguła w języku Java (i C #) brzmi: „Podwyrażenia są oceniane od lewej do prawej”. Ponieważ A () pojawia się na lewo od C (), A () jest obliczane jako pierwsze, niezależnie od tego, że C () bierze udział w mnożeniu, a A () tylko w dodawaniu.

Więc teraz masz wystarczająco dużo informacji, aby odpowiedzieć na swoje pytanie. W a[b] = b = 0regułach skojarzeń powiedz, że tak jest, a[b] = (b = 0);ale to nie znaczy, że b=0biegnie pierwszy! Reguły pierwszeństwa mówią, że indeksowanie ma wyższy priorytet niż przypisanie, ale nie oznacza to, że indeksator działa przed przypisaniem znajdującym się najbardziej po prawej stronie .

(AKTUALIZACJA: Wcześniejsza wersja tej odpowiedzi zawierała drobne i praktycznie nieistotne pominięcia w następnej sekcji, którą poprawiłem. Napisałem również artykuł na blogu opisujący, dlaczego te zasady są rozsądne w Javie i C # tutaj: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

Pierwszeństwo i asocjatywność mówią nam tylko, że przypisanie zera do bmusi nastąpić przed przypisaniem do a[b], ponieważ przypisanie zera oblicza wartość przypisaną w operacji indeksowania. Pierwszeństwo i asocjatywność sam nic nie mówią o tym, czy a[b]jest oceniane przed lub pob=0 .

Znowu jest to to samo, co: A()[B()] = C()- Wiemy tylko, że indeksowanie musi nastąpić przed przypisaniem. Nie wiemy, czy A (), B () lub C () działa jako pierwsza w oparciu o pierwszeństwo i łączność . Potrzebujemy innej reguły, aby nam to powiedzieć.

Zasada jest taka, że ​​„kiedy masz wybór, co zrobić najpierw, zawsze idź od lewej do prawej”. Jednak w tym konkretnym scenariuszu jest interesująca zmarszczka. Czy efekt uboczny rzuconego wyjątku spowodowany przez kolekcję o wartości null lub indeks spoza zakresu jest uważany za część obliczenia lewej strony przypisania, czy też część obliczenia samego przypisania? Java wybiera to drugie. (Oczywiście jest to rozróżnienie, które ma znaczenie tylko wtedy, gdy kod jest już nieprawidłowy , ponieważ poprawny kod nie wyłuskuje wartości null ani nie przekazuje złego indeksu w pierwszej kolejności).

Więc co się dzieje?

  • a[b]Jest z lewej strony b=0, a więc a[b]biegnie pierwszy , w wyniku a[1]. Jednak sprawdzenie poprawności tej operacji indeksowania jest opóźnione.
  • Wtedy się b=0dzieje.
  • Następnie następuje weryfikacja, która ajest ważna i a[1]mieści się w zakresie
  • Przypisanie wartości do a[1]następuje na końcu.

Tak więc, chociaż w tym konkretnym przypadku istnieją pewne subtelności do rozważenia w tych rzadkich przypadkach błędów, które w pierwszej kolejności nie powinny występować w poprawnym kodzie, ogólnie można rozumować: rzeczy po lewej stronie dzieją się przed rzeczami po prawej . To jest zasada, której szukasz. Mówienie o pierwszeństwie i asocjatywności jest zarówno mylące, jak i nieistotne.

Ludzie cały czas źle to rozumieją , nawet ludzie, którzy powinni wiedzieć lepiej. Wydałem zbyt wiele książek o programowaniu, które podawały zasady niepoprawnie, więc nie jest zaskoczeniem, że wiele osób ma zupełnie niepoprawne przekonania na temat związku między pierwszeństwem / asocjatywnością a porządkiem ocen - mianowicie, że w rzeczywistości nie ma takiego związku ; są niezależni.

Jeśli ten temat Cię interesuje, zobacz moje artykuły na ten temat do dalszej lektury:

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

Dotyczą języka C #, ale większość z nich odnosi się równie dobrze do Javy.

Eric Lippert
źródło
6
Osobiście wolę model mentalny, w którym w pierwszym kroku budujesz drzewo wyrażeń przy użyciu pierwszeństwa i asocjatywności. W drugim kroku dokonaj rekurencyjnej oceny tego drzewa, zaczynając od korzenia. Przy ocenie węzła: Oceń bezpośrednie węzły potomne od lewej do prawej, a następnie samą notatkę. | Zaletą tego modelu jest to, że w trywialny sposób radzi sobie z przypadkiem, w którym operatory binarne mają efekt uboczny. Ale główną zaletą jest to, że po prostu lepiej pasuje do mojego mózgu.
CodesInChaos
Czy mam rację, że C ++ tego nie gwarantuje? A co z Pythonem?
Neil G
2
@Neil: C ++ nie gwarantuje nic o kolejności oceny i nigdy tego nie robił. (Ani C.) Python gwarantuje to ściśle według kolejności pierwszeństwa; w przeciwieństwie do wszystkiego innego, przypisanie to R2L.
Donal Fellows
2
@aroth wydaje mi się być po prostu zdezorientowanym A zasady pierwszeństwa oznaczają tylko, że dzieci należy oceniać przed rodzicami. Ale nic nie mówią o kolejności, w jakiej oceniane są dzieci. Java i C # wybrały od lewej do prawej, C i C ++ wybrały niezdefiniowane zachowanie.
CodesInChaos
6
@noober: OK, rozważ: M (A () + B (), C () * D (), E () + F ()). Chcesz, aby podwyrażenia były oceniane w jakiej kolejności? Czy C () i D () należy oceniać przed A (), B (), E () i F (), ponieważ mnożenie ma wyższy priorytet niż dodawanie? Łatwo powiedzieć, że „oczywiście” kolejność powinna być inna. Znalezienie rzeczywistej zasady obejmującej wszystkie przypadki jest raczej trudniejsze. Projektanci C # i Java wybrali prostą, łatwą do wyjaśnienia regułę: „idź od lewej do prawej”. Jaki jest twój proponowany zamiennik i dlaczego uważasz, że twoja zasada jest lepsza?
Eric Lippert
32

Mistrzowska odpowiedź Erica Lipperta nie jest jednak odpowiednio pomocna, ponieważ mówi o innym języku. To jest Java, gdzie specyfikacja języka Java jest ostatecznym opisem semantyki. W szczególności paragraf 15.26.1 jest istotny, ponieważ opisuje kolejność oceny dla =operatora (wszyscy wiemy, że jest prawostronna, tak?). Zmniejszając trochę do fragmentów, na których nam zależy w tym pytaniu:

Jeśli wyrażenie po lewej stronie jest wyrażeniem dostępu do tablicy ( §15.13 ), to wymaganych jest wiele kroków:

  • Najpierw obliczane jest podwyrażenie odwołania do tablicy wyrażenia dostępu do tablicy po lewej stronie operandu. Jeśli ocena zakończy się nagle, wyrażenie przypisania kończy się nagle z tego samego powodu; podwyrażenie indeksu (wyrażenia dostępu do tablicy po lewej stronie) i operand po prawej stronie nie są obliczane i nie następuje przypisanie.
  • W przeciwnym razie obliczane jest podwyrażenie indeksu wyrażenia dostępu do tablicy po lewej stronie operandu. Jeśli ta ocena zakończy się nagle, wyrażenie przypisania kończy się nagle z tego samego powodu, a operand po prawej stronie nie jest oceniany i nie następuje przypisanie.
  • W przeciwnym razie oceniany jest prawy operand. Jeśli ocena zakończy się nagle, wyrażenie przypisania kończy się nagle z tego samego powodu i nie następuje przypisanie.

[… Następnie przechodzi do opisu rzeczywistego znaczenia samego zadania, które możemy tutaj zignorować dla zwięzłości…]

Krótko mówiąc, Java ma bardzo ściśle określoną kolejność oceny, która jest prawie dokładnie od lewej do prawej w argumentach dowolnego operatora lub wywołania metody. Przypisania tablic są jednym z bardziej złożonych przypadków, ale nawet tam nadal jest to L2R. (JLS zaleca, aby nie pisać kodu, który wymaga tego rodzaju złożonych ograniczeń semantycznych , podobnie jak ja: możesz wpaść w więcej niż wystarczające kłopoty, wykonując tylko jedno przypisanie na instrukcję!)

C i C ++ zdecydowanie różnią się od Javy w tym obszarze: ich definicje językowe celowo pozostawiają niezdefiniowaną kolejność ocen, aby umożliwić więcej optymalizacji. C # najwyraźniej przypomina Javę, ale nie znam jego literatury na tyle dobrze, by móc wskazać formalną definicję. (To naprawdę różni się w zależności od języka, Ruby jest ściśle L2R, podobnie jak Tcl - chociaż brakuje mu operatora przypisania per se z powodów nieistotnych tutaj - a Python to L2R, ale R2L pod względem przypisania , co wydaje mi się dziwne, ale proszę bardzo .)

Donal Fellows
źródło
11
Więc mówisz, że odpowiedź Erica jest błędna, ponieważ Java definiuje ją jako dokładnie to, co powiedział?
konfigurator
8
Odpowiednia reguła w Javie (i C #) brzmi: „Podwyrażenia są oceniane od lewej do prawej” - brzmi to tak, jakby mówił o obu.
konfigurator
2
Trochę zdezorientowany - czy to sprawia, że ​​powyższa odpowiedź Erica Lipperta jest mniej prawdziwa, czy tylko cytuje konkretne odniesienie do tego, dlaczego jest prawdziwa?
GreenieMeanie
6
@ Greenie: Odpowiedź Erica jest prawdziwa, ale jak powiedziałem, nie można wyciągnąć wglądu z jednego języka w tej dziedzinie i zastosować go w innym bez zachowania ostrożności. Podałem więc ostateczne źródło.
Donal Fellows
1
dziwne jest to, że prawa ręka jest oceniana przed rozstrzygnięciem zmiennej lewej ręki; in a[-1]=c, cjest oceniana, zanim -1zostanie rozpoznana jako nieprawidłowa.
ZhongYu
5
a[b] = b = 0;

1) operator indeksowania tablicy ma wyższy priorytet niż operator przypisania (zobacz odpowiedź ):

(a[b]) = b = 0;

2) Zgodnie z 15.26. Operatory przypisania JLS

Istnieje 12 operatorów przypisania; wszystkie są syntaktycznie prawostronne (grupują od prawej do lewej). Zatem a = b = c oznacza a = (b = c), co przypisuje wartość c do b, a następnie przypisuje wartość b do a.

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

3) Zgodnie z 15.7. Kolejność oceny JLS

Język programowania Java gwarantuje, że operandy operatorów wydają się być oceniane w określonej kolejności oceny, a mianowicie od lewej do prawej.

i

Wygląda na to, że operand po lewej stronie operatora binarnego jest w pełni obliczony przed oszacowaniem jakiejkolwiek części operandu po prawej stronie.

Więc:

a) (a[b])oceniane jako pierwszea[1]

b) następnie (b=0)oceniane do0

c) (a[1] = 0)oceniane jako ostatnie

bup
źródło
1

Twój kod jest odpowiednikiem:

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

co wyjaśnia wynik.

Jérôme Verstrynge
źródło
7
Powstaje pytanie, dlaczego tak jest.
Mat
@Mat Odpowiedź brzmi, ponieważ tak dzieje się pod maską, biorąc pod uwagę kod podany w pytaniu. Tak dzieje się ocena.
Jérôme Verstrynge
1
Tak, wiem. Wciąż nie odpowiadam na pytanie przez IMO, dlatego tak właśnie odbywa się ocena.
Mat
1
@Mat „Dlaczego tak wygląda ocena?” nie jest zadawanym pytaniem. 'jak doszło do oceny?' to zadane pytanie.
Jérôme Verstrynge
1
@JVerstry: Dlaczego nie są one równoważne? Podwyrażenie odniesienia do tablicy w operandzie po lewej stronie jest operandem znajdującym się najbardziej po lewej stronie. Powiedzenie „najpierw z lewej strony” jest dokładnie tym samym, co „najpierw wykonaj odwołanie do tablicy”. Jeśli autorzy specyfikacji Javy wybrali niepotrzebną rozwlekłość i nadmiarowość w wyjaśnianiu tej konkretnej reguły, to dobrze dla nich; tego rodzaju rzeczy są zagmatwane i powinny być bardziej niż mniej rozwlekłe. Ale nie widzę, jak moja zwięzła charakterystyka różni się semantycznie od ich charakterystyk prolix.
Eric Lippert
0

Rozważ kolejny, bardziej szczegółowy przykład poniżej.

Zgodnie z ogólną zasadą:

Najlepiej jest mieć dostępną do przeczytania tabelę reguł kolejności pierwszeństwa i asocjatywności podczas rozwiązywania tych pytań, np. Http://introcs.cs.princeton.edu/java/11precedence/

Oto dobry przykład:

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

Pytanie: jakie jest wyjście powyższej linii?

Odpowiedź: Zastosuj zasady pierwszeństwa i kojarzenia

Krok 1: Zgodnie z zasadami pierwszeństwa: / i * operatory mają pierwszeństwo przed operatorami + -. Dlatego punktem wyjścia do wykonania tego równania będzie zawężenie do:

100/10*2

Krok 2: Zgodnie z zasadami i pierwszeństwem: / i * mają równe pierwszeństwo.

Ponieważ operatory / i * mają równe pierwszeństwo, musimy przyjrzeć się asocjatywności między tymi operatorami.

Zgodnie z ZASADAMI STOWARZYSZENIA tych dwóch operatorów zaczynamy wykonywać równanie od LEWEGO DO PRAWEGO, czyli 100/10 zostanie wykonane jako pierwsze:

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

Krok 3: Równanie jest teraz w następującym stanie wykonania:

=3+20-13

Zgodnie z regułami i pierwszeństwem: + i - mają równe pierwszeństwo.

Musimy teraz przyjrzeć się asocjatywności między operatorami + i -. Zgodnie z asocjatywnością tych dwóch konkretnych operatorów, zaczynamy wykonywać równanie od LEWEGO do PRAWEGO, czyli 3 + 20 zostanie wykonane jako pierwsze:

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

10 jest poprawnym wyjściem po kompilacji

Ponownie, ważne jest, aby podczas rozwiązywania tych pytań mieć przy sobie tabelę z regułami kolejności pierwszeństwa i łącznością, np. Http://introcs.cs.princeton.edu/java/11precedence/

Mark Burleigh
źródło
1
Mówisz, że „asocjatywność między operatorami + i -” to „PRAWO W LEWO”. Spróbuj użyć tej logiki i oceń 10 - 4 - 3.
Pshemo
1
Podejrzewam, że ten błąd może być spowodowany faktem, że na górze introcs.cs.princeton.edu/java/11precedence + jest operator jednoargumentowy (który ma łączność od prawej do lewej), ale addytywne + i - mają tak samo jak multiplikatywny * /% do właściwej kojarzenia.
Pshemo
Dobrze zauważony i odpowiednio zmieniony, dziękuję Pshemo
Mark Burleigh
Ta odpowiedź wyjaśnia pierwszeństwo i asocjatywność, ale, jak wyjaśnił Eric Lippert , pytanie dotyczy kolejności oceny, która jest zupełnie inna. W rzeczywistości to nie odpowiada na pytanie.
Fabio mówi Przywróć Monikę