Was sind die Regeln für die Auswertungsreihenfolge in Java?

86

Ich lese Java-Text und habe den folgenden Code erhalten:

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

Im Text gab der Autor keine klare Erklärung und die Wirkung der letzten Zeile ist: a[1] = 0;

Ich bin mir nicht sicher, ob ich verstehe: Wie ist die Bewertung verlaufen?

ipkiss
quelle
19
Die Verwirrung darüber, warum dies geschieht, bedeutet übrigens, dass Sie dies niemals tun sollten, da viele Menschen darüber nachdenken müssen, was es tatsächlich tut, anstatt dass es offensichtlich ist.
Martijn
Die richtige Antwort auf eine solche Frage lautet: "Tu das nicht!" Die Zuweisung sollte als Erklärung behandelt werden. Die Verwendung der Zuweisung als Ausdruck, der einen Wert zurückgibt, sollte einen Compilerfehler oder zumindest eine Warnung auslösen. Tu das nicht.
Mason Wheeler

Antworten:

173

Lassen Sie mich das ganz klar sagen, weil die Leute das die ganze Zeit falsch verstehen:

Die Reihenfolge der Auswertung von Unterausdrücken ist unabhängig von Assoziativität und Vorrang . Assoziativität und Priorität bestimmen, in welcher Reihenfolge die Operatoren ausgeführt werden, bestimmen jedoch nicht , in welcher Reihenfolge die Unterausdrücke ausgewertet werden. Ihre Frage bezieht sich auf die Reihenfolge, in der Unterausdrücke ausgewertet werden.

Überlegen Sie A() + B() + C() * D(). Die Multiplikation hat eine höhere Priorität als die Addition, und die Addition ist linksassoziativ. Dies entspricht jedoch dem (A() + B()) + (C() * D()) Wissen, dass die erste Addition vor der zweiten Addition und die Multiplikation vor der zweiten Addition erfolgt. Es sagt Ihnen nicht, in welcher Reihenfolge A (), B (), C () und D () aufgerufen werden! (Es sagt Ihnen auch nicht, ob die Multiplikation vor oder nach der ersten Addition erfolgt.) Es wäre durchaus möglich, die Vorrang- und Assoziativitätsregeln zu befolgen, indem Sie Folgendes kompilieren:

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

Dort werden alle Vorrang- und Assoziativitätsregeln befolgt - die erste Addition erfolgt vor der zweiten Addition und die Multiplikation erfolgt vor der zweiten Addition. Natürlich können wir die Aufrufe von A (), B (), C () und D () in beliebiger Reihenfolge ausführen und trotzdem die Vorrang- und Assoziativitätsregeln einhalten!

Wir brauchen eine Regel, die nicht mit den Vorrang- und Assoziativitätsregeln zusammenhängt, um die Reihenfolge zu erklären, in der die Unterausdrücke ausgewertet werden. Die relevante Regel in Java (und C #) lautet "Unterausdrücke werden von links nach rechts ausgewertet". Da A () links von C () erscheint, wird A () zuerst ausgewertet, unabhängig davon, dass C () an einer Multiplikation beteiligt ist und A () nur an einer Addition beteiligt ist.

Jetzt haben Sie genug Informationen, um Ihre Frage zu beantworten. In a[b] = b = 0den Regeln der Assoziativität heißt es, dass dies a[b] = (b = 0);aber nicht bedeutet, dass das b=0zuerst läuft! Die Vorrangregeln besagen, dass die Indizierung eine höhere Vorrangstellung als die Zuweisung hat. Dies bedeutet jedoch nicht, dass der Indexer vor der Zuweisung ganz rechts ausgeführt wird .

(UPDATE: Eine frühere Version dieser Antwort hatte einige kleine und praktisch unwichtige Auslassungen im folgenden Abschnitt, die ich korrigiert habe. Ich habe auch einen Blog-Artikel geschrieben, in dem beschrieben wird, warum diese Regeln in Java und C # sinnvoll sind: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

Rangfolge und Assoziativität uns nur sagen , dass die Zuordnung von Nullb passieren muss , bevor die Zuordnung zu a[b], weil die Zuordnung von Null berechnet der Wert, der in dem Indexierungsvorgang zugeordnet ist. Vorrang und Assoziativität allein sagen nichts darüber aus, ob das vor oder nach dem a[b]bewertet wird .b=0

Dies ist wiederum genau das Gleiche wie: A()[B()] = C()- Wir wissen nur, dass die Indizierung vor der Zuweisung erfolgen muss. Wir wissen nicht, ob A (), B () oder C () zuerst ausgeführt werden, basierend auf Vorrang und Assoziativität . Wir brauchen eine andere Regel, um uns das zu sagen.

Die Regel lautet wiederum: "Wenn Sie die Wahl haben, was zuerst zu tun ist, gehen Sie immer von links nach rechts." In diesem speziellen Szenario gibt es jedoch eine interessante Falte. Wird der Nebeneffekt einer ausgelösten Ausnahme, der durch eine Nullsammlung oder einen Index außerhalb des Bereichs verursacht wird, als Teil der Berechnung der linken Seite der Zuweisung oder als Teil der Berechnung der Zuweisung selbst betrachtet? Java wählt letzteres. (Dies ist natürlich eine Unterscheidung, die nur dann von Bedeutung ist, wenn der Code bereits falsch ist , da der richtige Code nicht null dereferenziert oder überhaupt einen schlechten Index übergibt.)

Was passiert also?

  • Das a[b]ist links von b=0, also a[b]läuft das zuerst , was dazu führt a[1]. Die Überprüfung der Gültigkeit dieses Indizierungsvorgangs wird jedoch verzögert.
  • Dann b=0passiert das.
  • Dann erfolgt die Überprüfung, adie gültig ist und a[1]sich in Reichweite befindet
  • Die Zuordnung des Wertes a[1]erfolgt zuletzt.

Obwohl in diesem speziellen Fall einige Feinheiten für die seltenen Fehlerfälle zu berücksichtigen sind, die überhaupt nicht im richtigen Code auftreten sollten, können Sie im Allgemeinen argumentieren: Dinge auf der linken Seite passieren vor Dingen auf der rechten Seite . Das ist die Regel, nach der Sie suchen. Die Rede von Vorrang und Assoziativität ist sowohl verwirrend als auch irrelevant.

Die Leute verstehen dieses Zeug die ganze Zeit falsch , sogar Leute, die es besser wissen sollten. Ich habe viel zu viele Programmierbücher bearbeitet , in denen die Regeln falsch angegeben wurden. Daher ist es nicht verwunderlich, dass viele Menschen völlig falsche Vorstellungen über die Beziehung zwischen Vorrang / Assoziativität und Bewertungsreihenfolge haben - nämlich, dass es in Wirklichkeit keine solche Beziehung gibt ;; Sie sind unabhängig.

Wenn Sie dieses Thema interessiert, lesen Sie meine Artikel zum Thema zur weiteren Lektüre:

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

Es geht um C #, aber das meiste davon gilt auch für Java.

Eric Lippert
quelle
6
Persönlich bevorzuge ich das mentale Modell, bei dem Sie im ersten Schritt einen Ausdrucksbaum mit Vorrang und Assoziativität erstellen. Und im zweiten Schritt bewerten Sie diesen Baum rekursiv, beginnend mit der Wurzel. Bei der Bewertung eines Knotens gilt Folgendes: Bewerten Sie die unmittelbaren untergeordneten Knoten von links nach rechts und dann die Notiz selbst. | Ein Vorteil dieses Modells besteht darin, dass es trivial den Fall behandelt, in dem binäre Operatoren einen Nebeneffekt haben. Aber der Hauptvorteil ist, dass es einfach besser zu meinem Gehirn passt.
CodesInChaos
Stimmt es, dass C ++ dies nicht garantiert? Was ist mit Python?
Neil G
2
@Neil: C ++ garantiert nichts über die Reihenfolge der Auswertung und hat es nie getan. (Auch nicht C.) Python garantiert dies streng nach Rangfolge; Im Gegensatz zu allem anderen ist die Zuweisung R2L.
Donal Fellows
2
@aroth für mich klingst du nur verwirrt. Und die Vorrangregeln implizieren nur, dass Kinder vor den Eltern bewertet werden müssen. Sie sagen jedoch nichts über die Reihenfolge aus, in der Kinder bewertet werden. Java und C # wählten von links nach rechts, C und C ++ wählten undefiniertes Verhalten.
CodesInChaos
6
@noober: OK, betrachte: M (A () + B (), C () * D (), E () + F ()). Sie möchten, dass die Unterausdrücke in welcher Reihenfolge ausgewertet werden? Sollten C () und D () vor A (), B (), E () und F () bewertet werden, da die Multiplikation Vorrang vor der Addition hat? Es ist leicht zu sagen, dass "offensichtlich" die Reihenfolge anders sein sollte. Es ist schwieriger, eine tatsächliche Regel zu finden, die alle Fälle abdeckt. Die Designer von C # und Java wählten eine einfache, leicht zu erklärende Regel: "Von links nach rechts gehen". Was ist Ihr vorgeschlagener Ersatz dafür und warum glauben Sie, dass Ihre Regel besser ist?
Eric Lippert
32

Eric Lipperts meisterhafte Antwort ist dennoch nicht richtig hilfreich, da es sich um eine andere Sprache handelt. Dies ist Java, wobei die Java-Sprachspezifikation die endgültige Beschreibung der Semantik ist. Insbesondere ist §15.26.1 relevant, da dies die Bewertungsreihenfolge für den =Bediener beschreibt (wir alle wissen, dass es richtig assoziativ ist, ja?). Reduzieren Sie es ein wenig auf die Teile, die uns in dieser Frage wichtig sind:

Wenn der linke Operandenausdruck ein Arrayzugriffsausdruck ist ( §15.13 ), sind viele Schritte erforderlich:

  • Zunächst wird der Array-Referenz-Unterausdruck des linken Operanden-Array-Zugriffsausdrucks ausgewertet. Wenn diese Auswertung abrupt abgeschlossen wird, wird der Zuweisungsausdruck aus demselben Grund abrupt abgeschlossen. Der Index-Unterausdruck (des Zugriffsausdrucks für das linke Operandenarray) und der rechte Operand werden nicht ausgewertet und es erfolgt keine Zuweisung.
  • Andernfalls wird der Indexunterausdruck des Zugriffsausdrucks für das linke Operandenarray ausgewertet. Wenn diese Auswertung abrupt abgeschlossen wird, wird der Zuweisungsausdruck aus demselben Grund abrupt abgeschlossen, und der rechte Operand wird nicht ausgewertet, und es erfolgt keine Zuweisung.
  • Andernfalls wird der rechte Operand ausgewertet. Wenn diese Auswertung abrupt abgeschlossen wird, wird der Zuweisungsausdruck aus demselben Grund abrupt abgeschlossen, und es erfolgt keine Zuweisung.

[… Anschließend wird die tatsächliche Bedeutung der Aufgabe selbst beschrieben, die wir hier der Kürze halber ignorieren können…]

Kurz gesagt, Java hat eine sehr genau definierte Auswertungsreihenfolge, die innerhalb der Argumente für jeden Operator oder Methodenaufruf ziemlich genau von links nach rechts ist. Array-Zuweisungen sind einer der komplexeren Fälle, aber selbst dort ist es immer noch L2R. (Das JLS empfiehlt, keinen Code zu schreiben, der diese Art komplexer semantischer Einschränkungen benötigt , und ich auch: Mit nur einer Zuweisung pro Anweisung können Sie mehr als genug Probleme bekommen!)

C und C ++ unterscheiden sich in diesem Bereich definitiv von Java: Ihre Sprachdefinitionen lassen die Bewertungsreihenfolge bewusst undefiniert, um weitere Optimierungen zu ermöglichen. C # ist anscheinend wie Java, aber ich kenne seine Literatur nicht gut genug, um auf die formale Definition verweisen zu können. (Dies variiert jedoch wirklich je nach Sprache. Ruby ist streng L2R, ebenso wie Tcl - obwohl es aus hier nicht relevanten Gründen keinen Zuweisungsoperator an sich gibt - und Python ist L2R, aber R2L in Bezug auf die Zuweisung , was ich seltsam finde, aber los geht's .)

Donal Fellows
quelle
11
Sie sagen also, dass Erics Antwort falsch ist, weil Java sie speziell als genau das definiert, was er gesagt hat?
Konfigurator
8
Die relevante Regel in Java (und C #) lautet "Unterausdrücke werden von links nach rechts ausgewertet" - Klingt für mich so, als würde er über beides sprechen.
Konfigurator
2
Ein wenig verwirrt hier - macht dies die obige Antwort von Eric Lippert weniger wahr, oder zitiert sie nur einen spezifischen Hinweis darauf, warum sie wahr ist?
GreenieMeanie
5
@ Greenie: Erics Antwort ist wahr, aber wie gesagt, Sie können keinen Einblick in eine Sprache in diesem Bereich nehmen und ihn auf eine andere anwenden, ohne vorsichtig zu sein. Also habe ich die endgültige Quelle zitiert.
Donal Fellows
1
Das Seltsame ist, dass die rechte Hand ausgewertet wird, bevor die linke Variable aufgelöst wird. in a[-1]=c, cwird ausgewertet, bevor -1als ungültig erkannt wird.
ZhongYu
5
a[b] = b = 0;

1) Der Array-Indizierungsoperator hat eine höhere Priorität als der Zuweisungsoperator (siehe diese Antwort ):

(a[b]) = b = 0;

2) Nach 15.26. Zuweisungsoperatoren von JLS

Es gibt 12 Zuweisungsoperatoren. Alle sind syntaktisch rechtsassoziativ (sie gruppieren sich von rechts nach links). Somit bedeutet a = b = c a = (b = c), was b den Wert von c zuweist und dann a den Wert von b zuweist.

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

3) Gemäß 15.7. Bewertungsreihenfolge von JLS

Die Programmiersprache Java garantiert, dass die Operanden von Operatoren in einer bestimmten Auswertungsreihenfolge ausgewertet werden, nämlich von links nach rechts.

und

Der linke Operand eines binären Operators scheint vollständig ausgewertet zu sein, bevor ein Teil des rechten Operanden ausgewertet wird.

So:

a) (a[b])zuerst ausgewerteta[1]

b) dann (b=0)ausgewertet bis0

c) (a[1] = 0)zuletzt ausgewertet

bup
quelle
1

Ihr Code entspricht:

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

das erklärt das Ergebnis.

Jérôme Verstrynge
quelle
7
Die Frage ist, warum das so ist.
Mat
@Mat Die Antwort ist, dass dies unter der Haube unter Berücksichtigung des in der Frage angegebenen Codes geschieht. So erfolgt die Auswertung.
Jérôme Verstrynge
1
Ja, ich weiß. Die Frage wird von der IMO immer noch nicht beantwortet, weshalb die Bewertung auf diese Weise erfolgt.
Mat
1
@Mat 'Warum erfolgt die Auswertung so?' ist nicht die gestellte Frage. "Wie ist die Bewertung passiert?" ist die gestellte Frage.
Jérôme Verstrynge
1
@JVerstry: Wie sind sie nicht gleichwertig? Der Array-Referenz-Unterausdruck des linken Operanden ist der am weitesten links stehende Operand. Die Aussage "mache zuerst ganz links" ist also genau das gleiche wie "mache zuerst die Array-Referenz". Wenn die Autoren der Java-Spezifikation sich dafür entschieden haben, diese spezielle Regel unnötig wortreich und überflüssig zu erklären, ist das gut für sie. Diese Art von Dingen ist verwirrend und sollte mehr als weniger wortreich sein. Aber ich sehe nicht, wie sich meine prägnante Charakterisierung semantisch von ihrer Prolix-Charakterisierung unterscheidet.
Eric Lippert
0

Betrachten Sie unten ein weiteres ausführlicheres Beispiel.

Als allgemeine Faustregel:

Es ist am besten, eine Tabelle mit den Regeln und der Assoziativität der Rangfolge zur Verfügung zu haben, die Sie bei der Lösung dieser Fragen lesen können, z. B. http://introcs.cs.princeton.edu/java/11precedence/.

Hier ist ein gutes Beispiel:

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

Frage: Was ist die Ausgabe der obigen Zeile?

Antwort: Wenden Sie die Vorrang- und Assoziativitätsregeln an

Schritt 1: Gemäß den Vorrangregeln: / und * haben Vorrang vor + - Operatoren. Daher wird der Ausgangspunkt zum Ausführen dieser Gleichung auf Folgendes eingegrenzt:

100/10*2

Schritt 2: Gemäß den Regeln und Vorrang: / und * haben Vorrang.

Da / und * Operatoren Vorrang haben, müssen wir die Assoziativität zwischen diesen Operatoren untersuchen.

Gemäß den ASSOCIATIVITY-REGELN dieser beiden bestimmten Operatoren beginnen wir mit der Ausführung der Gleichung von LINKS NACH RECHTS, dh 100/10 wird zuerst ausgeführt:

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

Schritt 3: Die Gleichung befindet sich jetzt im folgenden Ausführungszustand:

=3+20-13

Nach den Regeln und Vorrang: + und - haben Vorrang.

Wir müssen uns nun die Assoziativität zwischen den Operatoren + und - Operatoren ansehen. Entsprechend der Assoziativität dieser beiden bestimmten Operatoren beginnen wir mit der Ausführung der Gleichung von LINKS nach RECHTS, dh 3 + 20 wird zuerst ausgeführt:

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

10 ist die richtige Ausgabe beim Kompilieren

Auch hier ist es wichtig, eine Tabelle mit den Regeln und der Assoziativität der Rangfolge bei sich zu haben, wenn Sie diese Fragen lösen, z. B. http://introcs.cs.princeton.edu/java/11precedence/.

Mark Burleigh
quelle
1
Sie sagen, dass "Assoziativität zwischen den Operatoren + und - Operatoren" "RECHTS NACH LINKS" ist. Versuchen Sie, diese Logik zu verwenden und zu bewerten 10 - 4 - 3.
Pshemo
1
Ich vermute, dass dieser Fehler durch die Tatsache verursacht werden kann, dass oben auf introcs.cs.princeton.edu/java/11precedence ein unärer + Operator (der die Assoziativität von rechts nach links hat) ist, aber additive + und - genau wie multiplikative * /% übrig haben zur richtigen Assoziativität.
Pshemo
Gut entdeckt und entsprechend geändert, danke Pshemo
Mark Burleigh
Diese Antwort erklärt Vorrang und Assoziativität, aber wie Eric Lippert erklärte , geht es bei der Frage um die Bewertungsreihenfolge, die sehr unterschiedlich ist. Tatsächlich beantwortet dies die Frage nicht.
Fabio sagt Reinstate Monica