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?
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?
Odpowiedzi:
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 = 0
regułach skojarzeń powiedz, że tak jest, a[b] = (b = 0);
ale to nie znaczy, że b=0
biegnie 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 b
musi 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.b=0
dzieje.a
jest ważna i a[1]
mieści się w zakresiea[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.
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 .)
a[-1]=c
, c
jest oceniana, zanim -1
zostanie rozpoznana jako nieprawidłowa.
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
Twój kod jest odpowiednikiem:
int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;
co wyjaśnia wynik.
Rozważ kolejny, bardziej szczegółowy przykład poniżej.
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/
10 - 4 - 3
.
+
jest operator jednoargumentowy (który ma łączność od prawej do lewej), ale addytywne + i - mają tak samo jak multiplikatywny * /% do właściwej kojarzenia.