Was ist die Erklärung für das Ergebnis der folgenden Operation?
k += c += k += c;
Ich habe versucht, das Ausgabeergebnis aus dem folgenden Code zu verstehen:
int k = 10;
int c = 30;
k += c += k += c;
//k=80 instead of 110
//c=70
und derzeit habe ich Probleme zu verstehen, warum das Ergebnis für "k" 80 ist. Warum funktioniert die Zuweisung von k = 40 nicht (tatsächlich sagt mir Visual Studio, dass dieser Wert nicht anderweitig verwendet wird)?
Warum ist k 80 und nicht 110?
Wenn ich die Operation aufteile:
k+=c;
c+=k;
k+=c;
das Ergebnis ist k = 110.
Ich habe versucht, durch die CIL zu schauen , aber ich bin nicht so tief in der Interpretation der generierten CIL und kann nicht ein paar Details erfahren:
// [11 13 - 11 24]
IL_0001: ldc.i4.s 10
IL_0003: stloc.0 // k
// [12 13 - 12 24]
IL_0004: ldc.i4.s 30
IL_0006: stloc.1 // c
// [13 13 - 13 30]
IL_0007: ldloc.0 // k expect to be 10
IL_0008: ldloc.1 // c
IL_0009: ldloc.0 // k why do we need the second load?
IL_000a: ldloc.1 // c
IL_000b: add // I expect it to be 40
IL_000c: dup // What for?
IL_000d: stloc.0 // k - expected to be 40
IL_000e: add
IL_000f: dup // I presume the "magic" happens here
IL_0010: stloc.1 // c = 70
IL_0011: add
IL_0012: stloc.0 // k = 80??????
c#
cil
compound-assignment
Andrii Kotliarov
quelle
quelle
Antworten:
Eine Operation wie
a op= b;
ist äquivalent zua = a op b;
. Eine Zuweisung kann als Anweisung oder als Ausdruck verwendet werden, während sie als Ausdruck den zugewiesenen Wert ergibt. Deine Meinung ...... kann, da der Zuweisungsoperator rechtsassoziativ ist, auch als geschrieben werden
oder (erweitert)
Wobei während der gesamten Auswertung die alten Werte der beteiligten Variablen verwendet werden. Dies gilt insbesondere für den Wert von
k
(siehe meine Bewertung der IL unten und den Link Wai Ha Lee). Daher erhalten Sie nicht 70 + 40 (neuer Wert vonk
) = 110, sondern 70 + 10 (alter Wert vonk
) = 80.Der Punkt ist , dass (nach der C # spec ) „Operanden in einem Ausdruck von links nach rechts ausgewertet“ (die Operanden die Variablen sind
c
undk
in unserem Fall). Dies ist unabhängig von der Priorität und Assoziativität des Operators, die in diesem Fall eine Ausführungsreihenfolge von rechts nach links vorschreiben. (Siehe Kommentare zu Eric Lipperts Antwort auf dieser Seite).Nun schauen wir uns die IL an. IL geht von einer stapelbasierten virtuellen Maschine aus, dh es werden keine Register verwendet.
Der Stapel sieht jetzt so aus (von links nach rechts; die Oberseite des Stapels ist rechts)
Beachten Sie, dass
IL_000c: dup
,IL_000d: stloc.0
dh die erste Zuordnung zuk
, weg optimiert werden könnte. Wahrscheinlich wird dies für Variablen vom Jitter durchgeführt, wenn IL in Maschinencode konvertiert wird.Beachten Sie auch, dass alle für die Berechnung erforderlichen Werte entweder vor einer Zuweisung auf den Stapel verschoben oder aus diesen Werten berechnet werden. Zugewiesene Werte (von
stloc
) werden bei dieser Auswertung nie wieder verwendet.stloc
Pops die Oberseite des Stapels.Die Ausgabe des folgenden Konsolentests ist (
Release
Modus mit aktivierten Optimierungen)quelle
k = 10 + (30 + (10 + 30)) = 80
und dieserc
endgültige Wert wird in der ersten Klammer angegebenc = 30 + (10 + 30) = 70
.k
es sich um einen lokalen Speicher handelt, wird der tote Speicher mit ziemlicher Sicherheit entfernt, wenn Optimierungen aktiviert sind, und beibehalten, wenn dies nicht der Fall ist. Eine interessante Frage ist, ob der Jitter den toten Speicher verlassen darf , wennk
es sich um ein Feld, eine Eigenschaft, einen Array-Slot usw. handelt. in der Praxis glaube ich nicht.k
er zweimal zugewiesen wird, wenn es sich um eine Eigenschaft handelt.Zunächst einmal sind die Antworten von Henk und Olivier richtig; Ich möchte es etwas anders erklären. Insbesondere möchte ich auf diesen Punkt eingehen, den Sie angesprochen haben. Sie haben folgende Aussagen:
Und Sie schließen dann fälschlicherweise, dass dies das gleiche Ergebnis wie diese Aussagen liefern sollte:
Es ist informativ zu sehen, wie Sie das falsch verstanden haben und wie Sie es richtig machen. Der richtige Weg, es zu brechen, ist so.
Schreiben Sie zuerst das äußerste + = um
Zweitens schreiben Sie das äußerste + neu. Ich hoffe, Sie stimmen zu, dass x = y + z immer dasselbe sein muss wie "y als temporär bewerten, z als temporär bewerten, temporäre Summe summieren, x die Summe zuweisen" . Lassen Sie uns das sehr deutlich machen:
Stellen Sie sicher, dass dies klar ist, da dies der Schritt ist, den Sie falsch verstanden haben . Wenn Sie komplexe Vorgänge in einfachere Vorgänge aufteilen, müssen Sie sicherstellen, dass Sie dies langsam und vorsichtig tun und keine Schritte überspringen . Beim Überspringen von Schritten machen wir Fehler.
OK, jetzt brechen Sie die Zuordnung zu t2 wieder langsam und vorsichtig auf.
Die Zuweisung weist t2 den gleichen Wert zu, der c zugewiesen ist. Nehmen wir also Folgendes an:
Toll. Brechen Sie nun die zweite Zeile auf:
Großartig, wir machen Fortschritte. Teilen Sie die Zuordnung zu t4 auf:
Brechen Sie nun die dritte Zeile auf:
Und jetzt können wir uns das Ganze ansehen:
Wenn wir fertig sind, ist k 80 und c ist 70.
Schauen wir uns nun an, wie dies in der IL implementiert ist:
Das ist ein bisschen schwierig:
Wir hätten das oben Gesagte als umsetzen können
Aber wir verwenden den "dup" -Trick, weil er den Code kürzer macht und den Jitter erleichtert, und wir erhalten das gleiche Ergebnis. Im Allgemeinen versucht der C # -Codegenerator, temporäre Elemente so kurz wie möglich auf dem Stapel zu halten. Wenn Sie es einfacher finden , die IL mit weniger Ephemerals zu folgen, drehen Optimierungen aus , und der Code - Generator wird weniger aggressiv.
Wir müssen jetzt den gleichen Trick machen, um c zu erhalten:
und schlussendlich:
Da wir die Summe für nichts anderes brauchen, täuschen wir sie nicht. Der Stapel ist jetzt leer und wir sind am Ende der Anweisung.
Die Moral der Geschichte lautet: Wenn Sie versuchen, ein kompliziertes Programm zu verstehen, brechen Sie die Operationen immer einzeln auf . Nehmen Sie keine Abkürzungen; Sie werden dich in die Irre führen.
quelle
F(i) + G(i++) * H(i)
, Verfahren F genannt wird , um den alten Wert von i verwendet wird , dann geht das Verfahren G wird mit dem alten Wert von i aufgerufen, und schließlich wird Methode H mit dem neuen Wert von i aufgerufen . Dies ist getrennt von der Operatorpriorität und steht in keinem Zusammenhang damit. " (Hervorhebung hinzugefügt.) Ich glaube, ich habe mich geirrt, als ich sagte, dass nirgendwo "der alte Wert verwendet wird" vorkommt! Es kommt in einem Beispiel vor. Aber das normative Bit ist "von links nach rechts".+
, und dann erhalten Sie+=
kostenlos, weilx += y
definiert alsx = x + y
ausgenommenx
wird nur einmal ausgewertet. Dies gilt unabhängig davon, ob das+
integriert oder benutzerdefiniert ist. Also: Überladen Sie+
einen Referenztyp und sehen Sie, was passiert.Es läuft darauf hinaus: Wird das allererste
+=
auf das Originalk
oder auf den Wert angewendet, der mehr rechts berechnet wurde?Die Antwort lautet: Obwohl die Zuweisungen von rechts nach links gebunden sind, werden die Vorgänge immer noch von links nach rechts ausgeführt.
Das am weitesten links stehende
+=
wird also ausgeführt10 += 70
.quelle
Ich habe das Beispiel mit gcc und pgcc ausprobiert und 110 erhalten. Ich habe die von ihnen erzeugte IR überprüft, und der Compiler hat den Ausdruck erweitert auf:
das sieht für mich vernünftig aus.
quelle
Für diese Art von Kettenzuweisungen müssen Sie die Werte von der rechten Seite aus zuweisen. Sie müssen es zuweisen und berechnen und der linken Seite zuweisen und dies bis zum Ende (Zuordnung ganz links) fortsetzen. Sicher, es wird als k = 80 berechnet.
quelle
Einfache Antwort: Ersetzen Sie vars durch Werte und Sie haben es:
quelle
k = 10; m = (k += k) + k;
das nichtm = (10 + 10) + 10
. Sprachen mit mutierenden Ausdrücken können nicht so analysiert werden, als hätten sie eine eifrige Wertersubstitution . Die Wertsubstitution erfolgt in einer bestimmten Reihenfolge in Bezug auf die Mutationen, und Sie müssen dies berücksichtigen.Sie können dies durch Zählen lösen.
Es gibt also zwei
c
s und zweik
sUnd als Folge der Operatoren der Sprache ist
k
auch gleich2c + 2k
Dies funktioniert für jede Kombination von Variablen in diesem Kettenstil:
So
Und
r
wird gleich gleich sein.Sie können die Werte der anderen Zahlen berechnen, indem Sie nur bis zu ihrer Zuordnung ganz links berechnen. Also
m
gleich2m + n
undn
gleichn + m
.Dies zeigt, dass dies
k += c += k += c;
anders istk += c; c += k; k += c;
und daher unterschiedliche Antworten erhalten.Einige Leute in den Kommentaren scheinen besorgt zu sein, dass Sie versuchen könnten, diese Verknüpfung auf alle möglichen Arten von Hinzufügungen zu verallgemeinern. Ich werde also klarstellen, dass diese Verknüpfung nur auf diese Situation anwendbar ist, dh Additionszuweisungen für die eingebauten Nummerntypen miteinander verketten. Es funktioniert (nicht unbedingt), wenn Sie andere Operatoren hinzufügen, z. B.
()
oder+
, oder wenn Sie Funktionen aufrufen oder wenn Sie überschrieben+=
haben oder wenn Sie etwas anderes als die grundlegenden Nummerntypen verwenden. Es soll nur bei der jeweiligen Situation in der Frage helfen .quelle
x = 1;
undy = (x += x) + x;
ist es Ihre Behauptung, dass "es drei x gibt und y gleich ist3 * x
"? Denny
gleich4
in diesem Fall. Wasy = x + (x += x);
ist nun Ihre Behauptung, dass das algebraische Gesetz "a + b = b + a" erfüllt ist und dies auch 4 ist? Weil dies 3 ist. Leider folgt C # nicht den Regeln der High-School-Algebra, wenn die Ausdrücke Nebenwirkungen haben . C # folgt den Regeln einer Nebenwirkung der Algebra.