Dlaczego x == (x = y) to nie to samo co (x = y) == x?

207

Rozważ następujący przykład:

class Quirky {
    public static void main(String[] args) {
        int x = 1;
        int y = 3;

        System.out.println(x == (x = y)); // false
        x = 1; // reset
        System.out.println((x = y) == x); // true
     }
}

Nie jestem pewien, czy w specyfikacji języka Java znajduje się element, który nakazuje ładowanie poprzedniej wartości zmiennej do porównania z prawą stroną ( x = y), która według kolejności nawiasowej powinna być obliczona jako pierwsza.

Dlaczego pierwsze wyrażenie ocenia jako false, a drugie ocenia true? Spodziewałbym (x = y)się najpierw oceny, a potem porównania xz samym sobą ( 3) i zwrotu true.


To pytanie różni się od kolejności oceny podwyrażeń w wyrażeniu Java, ponieważ xzdecydowanie nie jest to tutaj „podwyrażenie”. Należy go załadować do porównania, a nie „ocenić”. Pytanie jest specyficzne dla Javy, a wyrażenie x == (x = y), w przeciwieństwie do dalekosiężnych, niepraktycznych konstrukcji, często tworzonych na trudne pytania podczas wywiadu, pochodzi z prawdziwego projektu. Miał to być jednowierszowy zamiennik idiomu „porównaj i zamień”

int oldX = x;
x = y;
return oldX == y;

który, będąc jeszcze prostszym niż instrukcja CMPXCHG x86, zasługiwał na krótsze wyrażenie w Javie.

John McClane
źródło
62
Lewa strona jest zawsze oceniana przed prawą stroną. Nawiasy nie mają z tym różnicy.
Louis Wasserman,
11
Ocena wyrażenia x = yjest z pewnością istotna i powoduje efekt uboczny xustawiony na wartość y.
Louis Wasserman,
50
Zrób sobie i swoim kolegom z drużyny przysługę i nie mieszaj mutacji stanu w tej samej linii co badanie stanu. Takie postępowanie drastycznie zmniejsza czytelność kodu. (Są przypadki, w których jest to absolutnie konieczne ze względu na wymagania atomowości, ale funkcje dla tych już istnieją, a ich cel zostanie natychmiast rozpoznany.)
jpmc26
50
Prawdziwe pytanie brzmi: dlaczego chcesz pisać taki kod?
klutt
26
Kluczem do twojego pytania jest twoje fałszywe przekonanie, że nawiasy oznaczają kolejność oceny. Jest to powszechne przekonanie ze względu na to, jak uczymy się matematyki w szkole podstawowej oraz dlatego, że niektóre książki dla początkujących wciąż błędnie to rozumieją , ale jest to fałszywe przekonanie. To dość częste pytanie. Możesz skorzystać z przeczytania moich artykułów na ten temat; dotyczą C #, ale dotyczą Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Eric Lippert

Odpowiedzi:

97

które w kolejności wynikającej z nawiasów powinny być obliczone w pierwszej kolejności

Nie. To powszechne błędne przekonanie, że nawiasy mają jakikolwiek (ogólny) wpływ na kolejność obliczeń lub oceny. Przymuszają tylko części twojego wyrażenia do konkretnego drzewa, wiążąc odpowiednie operandy z odpowiednimi operacjami dla zadania.

(A jeśli ich nie użyjesz, ta informacja pochodzi z „pierwszeństwa” i asocjatywności operatorów, co jest wynikiem tego, jak zdefiniowane jest drzewo składniowe języka. W rzeczywistości jest to dokładnie tak, jak działa używaj nawiasów, ale upraszczamy i mówimy, że nie polegamy wówczas na żadnych regułach pierwszeństwa).

Po wykonaniu tej czynności (tj. Po przeanalizowaniu kodu do programu) operandy te nadal wymagają oceny, a sposób ich wykonania jest odrębny: wspomniane reguły (jak pokazał nam Andrew) stwierdzają, że LHS każdej operacji jest oceniany jako pierwszy w Javie.

Pamiętaj, że nie jest tak we wszystkich językach; na przykład w C ++, chyba że używasz operatora zwarciowego, takiego jak &&lub ||, kolejność operandów jest ogólnie nieokreślona i nie powinieneś na nim polegać w żaden sposób.

Nauczyciele muszą przestać wyjaśniać pierwszeństwo operatorów za pomocą zwrotów wprowadzających w błąd, takich jak „to sprawia, że ​​dodawanie jest pierwsze”. Biorąc pod uwagę wyrażenie, x * y + zwłaściwe wyjaśnienie brzmiałoby: „pierwszeństwo operatora powoduje, że dodawanie zachodzi pomiędzy x * yi zzamiast pomiędzy yi z”, bez wzmianki o jakimkolwiek „porządku”.

Lekkość Wyścigi na orbicie
źródło
6
Chciałbym, aby moi nauczyciele dokonali pewnego rozdziału między podstawową matematyką a składnią, którą stosowali, aby ją przedstawić, tak jakbyśmy spędzili dzień z cyframi rzymskimi lub polską notacją lub czymkolwiek innym i zobaczyli, że ten dodatek ma te same właściwości. Nauczyliśmy się asocjatywności i wszystkich tych właściwości w gimnazjum, więc było dużo czasu.
John P,
1
Cieszę się, że wspomniałeś, że ta zasada nie dotyczy wszystkich języków. Ponadto, jeśli którakolwiek ze stron ma inny efekt uboczny, taki jak zapis do pliku lub odczyt aktualnego czasu, nie jest (nawet w Javie) niezdefiniowana, w jakiej kolejności to się dzieje. Jednak wynik porównania będzie taki, jakby był oceniany od lewej do prawej (w Javie). Jeszcze jedna strona: wiele języków po prostu nie zezwala na mieszanie przypisań i porównywanie w ten sposób według reguł składniowych, a problem nie pojawiłby się.
Abel,
5
@JohnP: Gorzej. Czy 5 * 4 oznacza 5 + 5 + 5 + 5 czy 4 + 4 + 4 + 4 + 4? Niektórzy nauczyciele twierdzą, że tylko jeden z tych wyborów jest właściwy.
Brian
3
@Brian Ale ... ale ... mnożenie liczb rzeczywistych jest przemienne!
Wyścigi lekkości na orbicie
2
W moim świecie myślenia nawiasy oznaczają „jest potrzebny”. Obliczając „a * (b + c)”, w nawiasach wyrażono by, że wynik dodawania jest potrzebny do pomnożenia. Wszelkie niejawne preferencje operatora mogą być wyrażone za pomocą parens, z wyjątkiem reguł LHS-first lub RHS-first. (Czy to prawda?) @Brian W matematyce istnieje kilka rzadkich przypadków, w których mnożenie można zastąpić powtarzającym się dodawaniem, ale nie zawsze jest to prawdą (zaczynając od liczb zespolonych, ale nie tylko). Zatem twoi wychowawcy powinni naprawdę mieć oko na to, co mówią ludzie ...
syck
164

==jest binarnym operatorem równości .

Wydaje się, że lewy operand operatora binarnego jest w pełni oceniany, zanim zostanie oszacowana jakakolwiek część operandu po prawej stronie .

Specyfikacja Java 11> Kolejność oceny> Najpierw oceń operand lewostronny

Andrew Tobilko
źródło
42
Sformułowanie „wydaje się być” nie brzmi tak, jakby byli tego pewni, tbh.
Pan Lister
86
„wydaje się być” oznacza, że ​​specyfikacja nie wymaga, aby operacje były faktycznie wykonywane w tej kolejności chronologicznie, ale wymaga, aby uzyskać taki sam wynik, jaki byś uzyskał, gdyby były.
Robyn
24
@MrLister „wydaje się być” wydaje się być złym wyborem z ich strony. Przez „pojawiać się” mają na myśli „manifestować się twórcy jako zjawisko”. „jest skutecznie” może być lepszym wyrażeniem.
Kelvin
18
w społeczności C ++ jest to równoważne zasadzie „jak gdyby”… operand musi zachowywać się „tak, jakby” został zaimplementowany zgodnie z następującymi regułami, nawet jeśli technicznie tak nie jest.
Michael Edenfield
2
@Kelvin Zgadzam się, wybrałbym to słowo, a nie „wydaje się”.
MC Emperor,
149

Jak powiedział LouisWasserman, wyrażenie jest oceniane od lewej do prawej. A java nie dba o to, co faktycznie robi „ocenianie”, zależy tylko na wygenerowaniu (nieulotnej, końcowej) wartości do pracy.

//the example values
x = 1;
y = 3;

Aby obliczyć pierwsze wyjście System.out.println(), należy wykonać następujące czynności:

x == (x = y)
1 == (x = y)
1 == (x = 3) //assign 3 to x, returns 3
1 == 3
false

i obliczyć drugi:

(x = y) == x
(x = 3) == x //assign 3 to x, returns 3
3 == x
3 == 3
true

Zauważ, że druga wartość zawsze będzie miała wartość true, niezależnie od wartości początkowych xi y, ponieważ efektywnie porównujesz przypisanie wartości do zmiennej, do której jest przypisana, a = bi bbędzie oceniane w tej kolejności, zawsze będzie to samo zgodnie z definicją.

Poohl
źródło
Nawiasem mówiąc, „od lewej do prawej” jest również prawdziwe w matematyce, gdy tylko dojdziesz do nawiasów lub precedensu, iterujesz w nich i sprawdzasz wszystko od lewej do prawej, zanim przejdziesz dalej na główny poziom. Ale matematyka nigdy by tego nie zrobiła; rozróżnienie ma znaczenie tylko dlatego, że nie jest to równanie, ale operacja kombi, wykonująca zarówno przypisanie, jak i równanie za jednym zamachem . Nigdy bym tego nie zrobił, ponieważ czytelność jest słaba, chyba że grałem w golfa lub szukałem sposobu na optymalizację wydajności, a potem pojawiałyby się komentarze.
Harper - Przywróć Monikę
25

Nie jestem pewien, czy istnieje specyfikacja języka Java, która nakazuje ładowanie poprzedniej wartości zmiennej ...

Jest. Następnym razem, gdy nie będziesz wiedział, co mówi specyfikacja, przeczytaj specyfikację, a następnie zadaj pytanie, czy jest niejasne.

... prawa strona, (x = y)która według kolejności podanej w nawiasach powinna być obliczona jako pierwsza.

To stwierdzenie jest fałszywe. Nawiasy nie oznaczają kolejności oceny . W Javie kolejność oceny jest od lewej do prawej, niezależnie od nawiasów. Nawiasy określają, gdzie znajdują się granice podwyrażeń, a nie kolejność oceny.

Dlaczego pierwsze wyrażenie jest fałszywe, a drugie prawdziwe?

Reguła dla ==operatora to: oceń lewą stronę, aby wygenerować wartość, oceń prawą stronę, aby wygenerować wartość, porównaj wartości, porównanie jest wartością wyrażenia.

Innymi słowy, znaczenie expr1 == expr2jest zawsze takie samo, jak gdybyś napisał, temp1 = expr1; temp2 = expr2;a następnie ocenił temp1 == temp2.

Reguła dla =operatora z lokalną zmienną po lewej stronie: oszacuj lewą stronę, aby wygenerować zmienną, oceń prawą stronę, aby wygenerować wartość, wykonaj przypisanie, wynikiem jest wartość, która została przypisana.

Więc złóż to razem:

x == (x = y)

Mamy operatora porównania. Oszacuj lewą stronę, aby uzyskać wartość - otrzymamy bieżącą wartość x. Oceń prawą stronę: jest to przypisanie, więc oceniamy lewą stronę, aby utworzyć zmienną - zmienną x- oceniamy prawą stronę - bieżącą wartość y- przypisujemy ją x, a wynikiem jest przypisana wartość. Następnie porównujemy oryginalną wartość z xwartością, która została przypisana.

Możesz to zrobić (x = y) == xjako ćwiczenie. Ponownie pamiętajcie, że wszystkie zasady oceny lewej strony mają miejsce przed wszystkimi zasadami oceny prawej strony .

Spodziewałbym się najpierw (x = y), a następnie porównałby x z samym sobą (3) i zwrócił true.

Twoje oczekiwania oparte są na zestawie niepoprawnych przekonań na temat reguł Java. Mam nadzieję, że masz teraz prawidłowe przekonania i w przyszłości będziesz oczekiwać prawdziwych rzeczy.

To pytanie różni się od „kolejności oceny podwyrażeń w wyrażeniu Java”

To stwierdzenie jest fałszywe. To pytanie jest całkowicie niemądre.

x zdecydowanie nie jest tutaj „podwyrażeniem”.

To stwierdzenie jest również fałszywe. Jest to podwyrażenie dwa razy w każdym przykładzie.

Należy go załadować do porównania, a nie „ocenić”.

Nie mam pojęcia, co to znaczy.

Najwyraźniej nadal masz wiele fałszywych przekonań. Moja rada jest taka, że ​​czytasz specyfikację, dopóki twoje fałszywe przekonania nie zostaną zastąpione prawdziwymi przekonaniami.

Pytanie jest specyficzne dla Javy, a wyrażenie x == (x = y), w odróżnieniu od dalekosiężnych niepraktycznych konstrukcji powszechnie tworzonych na trudne pytania podczas wywiadu, pochodzi z prawdziwego projektu.

Pochodzenie tego wyrażenia nie ma znaczenia dla pytania. Zasady dotyczące takich wyrażeń są jasno opisane w specyfikacji; Przeczytaj to!

Miał to być jednowierszowy zamiennik idiomu „porównaj i zamień”

Ponieważ to zastąpienie jednego wiersza spowodowało w tobie wiele zamieszania, czytelniku kodu, sugerowałbym, że był to zły wybór. Uczynienie kodu bardziej zwięzłym, ale trudniejszym do zrozumienia, nie jest wygraną. Jest mało prawdopodobne, aby kod był szybszy.

Nawiasem mówiąc, C # ma porównywać i zamieniać jako metodę biblioteczną, którą można przypisać do instrukcji maszyny. Wierzę, że Java nie ma takiej metody, ponieważ nie może być reprezentowana w systemie typów Java.

Eric Lippert
źródło
8
Gdyby ktoś mógł przejrzeć cały JLS, nie byłoby powodu publikować książek o Javie, a przynajmniej połowa tej strony byłaby bezużyteczna.
John McClane,
8
@JohnMcClane: Zapewniam cię, że nie ma żadnych trudności w przejściu przez całą specyfikację, ale także nie musisz. Specyfikacja Java zaczyna się od pomocnego „spisu treści”, który pomoże ci szybko dotrzeć do części, które najbardziej Cię interesują. Można go również wyszukiwać online i za pomocą słów kluczowych. To powiedziawszy, masz rację: istnieje wiele dobrych zasobów, które pomogą ci nauczyć się, jak działa Java; radzę wam, abyście z nich korzystali!
Eric Lippert,
8
Ta odpowiedź jest niepotrzebnie protekcjonalna i niegrzeczna. Pamiętaj: bądź miły .
walen
7
@LuisG .: Żadna protekcjonalność nie jest zamierzona ani dorozumiana; wszyscy jesteśmy tutaj, aby uczyć się od siebie i nie polecam niczego, czego sam nie zrobiłem, kiedy byłem początkujący. Nie jest to niegrzeczne. Jasne i jednoznaczne identyfikowanie ich fałszywych przekonań jest życzliwością dla oryginalnego plakatu . Ukrywanie się za „grzecznością” i pozwalanie ludziom nadal mieć fałszywe przekonania jest bezużyteczne i wzmacnia złe nawyki myślenia .
Eric Lippert,
5
@LuisG .: Kiedyś pisałem blog o projektowaniu JavaScript, a najbardziej pomocne komentarze, jakie kiedykolwiek otrzymałem, pochodziły od Brendana, wskazując jasno i jednoznacznie, gdzie popełniłem błąd. To było wspaniałe i doceniłem go za poświęcenie czasu, ponieważ przeżyłem następne 20 lat mojego życia, nie powtarzając tego błędu w mojej własnej pracy lub, co gorsza, ucząc go innych. Dało mi to również możliwość skorygowania tych samych fałszywych przekonań w innych, wykorzystując siebie jako przykład tego, jak ludzie uwierzyli w fałszywe rzeczy.
Eric Lippert,
16

Jest to związane z pierwszeństwem operatorów i sposobem ich oceny.

Nawiasy „()” mają wyższy priorytet i mają skojarzenia od lewej do prawej. Równość „==” jest następna w tym pytaniu i ma skojarzenia od lewej do prawej. Zadanie „=” jest ostatnie i ma skojarzenie od prawej do lewej.

System używa stosu do oceny wyrażenia. Wyrażenie jest oceniane od lewej do prawej.

Teraz dochodzi do pierwotnego pytania:

int x = 1;
int y = 3;
System.out.println(x == (x = y)); // false

Pierwszy x (1) zostanie przesunięty na stos. następnie wewnętrzny (x = y) zostanie oceniony i wypchnięty na stos z wartością x (3). Teraz x (1) zostanie porównane z x (3), więc wynik jest fałszywy.

x = 1; // reset
System.out.println((x = y) == x); // true

Tutaj (x = y) zostanie oszacowane, teraz wartość x staje się 3, a x (3) zostanie przesunięty na stos. Teraz x (3) ze zmienioną wartością po równości zostanie przesunięty na stos. Teraz wyrażenie zostanie ocenione i oba będą takie same, więc wynik jest prawdziwy.

Amit
źródło
12

To nie jest to samo. Lewa strona zawsze będzie oceniana przed prawą stroną, a nawiasy nie określają kolejności wykonywania, lecz grupę poleceń.

Z:

      x == (x = y)

Zasadniczo robisz to samo, co:

      x == y

I x będzie miało wartość y po porównaniu.

Podczas gdy z:

      (x = y) == x

Zasadniczo robisz to samo, co:

      x == x

Po x trwało y jest wartość. I zawsze zwróci się prawda .

Or10n
źródło
9

W pierwszym teście sprawdzasz, czy 1 == 3.

W drugim teście sprawdzanie wynosi 3 == 3.

(x = y) przypisuje wartość i ta wartość jest testowana. W poprzednim przykładzie najpierw x = 1, a następnie x. 3. Czy 1 == 3?

W tym ostatnim x ma przypisane 3 i oczywiście nadal jest 3. Czy 3 == 3?

Michael Puckett II
źródło
8

Rozważ ten inny, być może prostszy przykład:

int x = 1;
System.out.println(x == ++x); // false
x = 1; // reset
System.out.println(++x == x); // true

Tutaj operator wstępnej inkrementacji ++xmusi zostać zastosowany przed dokonaniem porównania - tak jak (x = y)w twoim przykładzie, musi zostać obliczony przed porównaniem.

Jednak ocena wyrażenia nadal odbywa się od lewej → do → prawej , więc pierwsze porównanie jest w rzeczywistości, 1 == 2podczas gdy drugie jest 2 == 2.
To samo dzieje się w twoim przykładzie.

Walen
źródło
8

Wyrażenia są oceniane od lewej do prawej. W tym przypadku:

int x = 1;
int y = 3;

x == (x = y)) // false
x ==    t

- left x = 1
- let t = (x = y) => x = 3
- x == (x = y)
  x == t
  1 == 3 //false

(x = y) == x); // true
   t    == x

- left (x = y) => x = 3
           t    =      3 
-  (x = y) == x
-     t    == x
-     3    == 3 //true
Derviş Kayımbaşıoğlu
źródło
5

Zasadniczo pierwsza instrukcja x miała wartość 1, więc Java porównuje 1 == z nową zmienną x, która nie będzie taka sama

W drugim powiedziałeś, że x = y, co oznacza, że ​​wartość x uległa zmianie, więc kiedy wywołasz ją ponownie, będzie to ta sama wartość, dlatego jest to prawda, a x == x

Ahmad Bedirxan
źródło
4

== jest operatorem porównania równości i działa od lewej do prawej.

x == (x = y);

tutaj stara przypisana wartość x jest porównywana z nową wartością przypisania x, (1 == 3) // false

(x = y) == x;

Podczas gdy tutaj nowa wartość przypisania x jest porównywana z nową wartością trzymania x przypisaną do niej tuż przed porównaniem, (3 == 3) // true

Rozważ to teraz

    System.out.println((8 + (5 * 6)) * 9);
    System.out.println(8 + (5 * 6) * 9);
    System.out.println((8 + 5) * 6 * 9);
    System.out.println((8 + (5) * 6) * 9);
    System.out.println(8 + 5 * 6 * 9);

Wynik:

342

278

702

342

278

Zatem nawiasy odgrywają główną rolę w wyrażeniach arytmetycznych, a nie w wyrażeniach porównawczych.

Nisrin Dhoondia
źródło
1
Wniosek jest błędny. Zachowanie nie różni się między operatorami arytmetycznymi i porównawczymi. x + (x = y)i (x = y) + xwykazywałoby podobne zachowanie jak oryginał z operatorami porównania.
JJJ,
1
@JJJ W x + (x = y) i (x = y) + x nie ma żadnego porównania, po prostu przypisuje wartość y do x i dodaje ją do x.
Nisrin Dhoondia
1
... tak, o to chodzi. „Nawiasy odgrywają główną rolę w wyrażeniach arytmetycznych tylko nie w wyrażeniach porównawczych” jest błędne, ponieważ nie ma różnicy między wyrażeniami arytmetycznymi a porównawczymi.
JJJ,
2

Chodzi o to, że kolejność priorytetów operatorów arytmetycznych / operatorów relacyjnych spośród dwóch operatorów =względem ==dominującej to ==(operatory relacyjne dominują), ponieważ poprzedza ona =operatory przypisania. Mimo pierwszeństwa kolejność oceny jest następująca LTR (LEWO DO PRAWO) po kolejności oceny. Niezależnie od jakichkolwiek ograniczeń ocena jest LTR.

Himanshu Ahuja
źródło
1
Odpowiedź jest zła. Pierwszeństwo operatorów nie wpływa na kolejność oceny. Przeczytaj wyjaśnienia niektórych najczęściej głosowanych odpowiedzi, zwłaszcza tej .
JJJ
1
Zgadza się, tak naprawdę uczymy się iluzji ograniczeń pierwszeństwa we wszystkich tych rzeczach, ale prawidłowo wskazane, że nie ma to wpływu, ponieważ kolejność oceny pozostaje od lewej do prawej
Himanshu Ahuja
-1

Łatwo jest w drugim porównaniu po lewej stronie przypisanie po przypisaniu y do x (po lewej), a następnie porównanie 3 == 3. W pierwszym przykładzie porównujesz x = 1 z nowym przypisaniem x = 3. Wydaje się, że zawsze pobierane są instrukcje odczytu stanu bieżącego od lewej do prawej strony x.

Michał Ziobro
źródło
-2

Pytanie, które zadałeś, jest bardzo dobrym pytaniem, jeśli chcesz napisać kompilator Java lub przetestować programy, aby sprawdzić, czy kompilator Java działa poprawnie. W Javie te dwa wyrażenia muszą dawać widoczne wyniki. Na przykład w C ++ nie muszą - więc jeśli ktoś ponownie używał części kompilatora C ++ w swoim kompilatorze Java, teoretycznie może się okazać, że kompilator nie zachowuje się tak, jak powinien.

Jako twórca oprogramowania, pisząc kod, który jest czytelny, zrozumiały i łatwy w utrzymaniu, obie wersje kodu byłyby uważane za okropne. Aby zrozumieć, co robi kod, trzeba dokładnie wiedzieć , jak definiowany jest język Java. Ktoś, kto pisze zarówno kod Java, jak i C ++, wzdrygnie się na niego. Jeśli musisz zapytać, dlaczego pojedynczy wiersz kodu robi to, co robi, powinieneś unikać tego kodu. (Przypuszczam i mam nadzieję, że faceci, którzy poprawnie odpowiedzieli na pytanie „dlaczego”, również unikną tego indeksu kodu).

gnasher729
źródło
„Aby zrozumieć, co robi kod, trzeba dokładnie wiedzieć, jak definiowany jest język Java”. Ale co, jeśli każdy współpracownik uzna to za zdrowy rozsądek?
BinaryTreeee,