Warum geben Zuweisungsanweisungen einen Wert zurück?

126

Dies ist erlaubt:

int a, b, c;
a = b = c = 16;

string s = null;
while ((s = "Hello") != null) ;

Nach meinem Verständnis sollte die Zuweisung s = ”Hello”;nur dazu führen “Hello”, dass sie zugewiesen wird s, aber die Operation sollte keinen Wert zurückgeben. Wenn das wahr ((s = "Hello") != null)wäre, würde das einen Fehler erzeugen, da nulles mit nichts verglichen würde.

Was ist der Grund dafür, dass Zuweisungsanweisungen einen Wert zurückgeben dürfen?

user437291
quelle
44
Als Referenz wird dieses Verhalten in der C # -Spezifikation definiert : "Das Ergebnis eines einfachen Zuweisungsausdrucks ist der dem linken Operanden zugewiesene Wert. Das Ergebnis hat den gleichen Typ wie der linke Operand und wird immer als Wert klassifiziert."
Michael Petrotta
1
@ Kurmedel Ich bin nicht der Downvoter, ich stimme dir zu. Aber vielleicht, weil es eine rhetorische Frage war?
Schmied
In der Pascal / Delphi-Zuweisung: = gibt nichts zurück. ich hasse es.
Andrey
20
Standard für Sprachen im C-Zweig. Zuweisungen sind Ausdrücke.
Hans Passant
"Zuweisung ... sollte nur dazu führen, dass" Hallo "s zugewiesen wird, aber die Operation sollte keinen Wert zurückgeben" - Warum? "Was ist der Grund dafür, dass Zuweisungsanweisungen einen Wert zurückgeben dürfen?" - Weil es nützlich ist, wie Ihre eigenen Beispiele zeigen. (Nun, Ihr zweites Beispiel ist albern, aber while ((s = GetWord()) != null) Process(s);nicht).
Jim Balter

Antworten:

160

Nach meinem Verständnis ist die Zuordnung s = "Hallo"; sollte nur dazu führen, dass "Hallo" s zugewiesen wird, aber die Operation sollte keinen Wert zurückgeben.

Ihr Verständnis ist 100% falsch. Können Sie erklären, warum Sie diese falsche Sache glauben?

Was ist der Grund dafür, dass Zuweisungsanweisungen einen Wert zurückgeben dürfen?

Zunächst einmal, Zuordnung Anweisungen erzeugen keinen Wert. Zuordnung Ausdrücke erzeugen einen Wert. Ein Zuweisungsausdruck ist eine rechtliche Erklärung. Es gibt nur eine Handvoll Ausdrücke, die rechtliche Aussagen in C # sind: Warten auf einen Ausdruck, Instanzkonstruktion, Inkrementieren, Dekrementieren, Aufrufen und Zuweisen von Ausdrücken können verwendet werden, wenn eine Anweisung erwartet wird.

Es gibt nur eine Art von Ausdruck in C #, die keinen Wert erzeugt, nämlich einen Aufruf von etwas, das als zurückkehrende Leere eingegeben wird. (Oder gleichwertig das Warten auf eine Aufgabe ohne zugeordneten Ergebniswert.) Jede andere Art von Ausdruck erzeugt einen Wert oder eine Variable oder eine Referenz oder einen Eigenschaftszugriff oder einen Ereigniszugriff und so weiter.

Beachten Sie, dass alle Ausdrücke, die als Aussagen zulässig sind , für ihre Nebenwirkungen nützlich sind . Das ist die wichtigste Erkenntnis hier, und ich denke, vielleicht die Ursache Ihrer Intuition, dass Aufgaben Aussagen und keine Ausdrücke sein sollten. Im Idealfall hätten wir genau eine Nebenwirkung pro Aussage und keine Nebenwirkungen in einem Ausdruck. Es ist etwas seltsam, dass nebeneffektiver Code überhaupt in einem Ausdruckskontext verwendet werden kann.

Der Grund für das Zulassen dieser Funktion liegt darin, dass (1) sie häufig praktisch ist und (2) in C-ähnlichen Sprachen idiomatisch ist.

Man könnte bemerken, dass die Frage gestellt wurde: Warum ist dies in C-ähnlichen Sprachen idiomatisch?

Dennis Ritchie kann leider nicht mehr gefragt werden, aber ich vermute, dass eine Zuordnung fast immer den Wert hinterlässt, der gerade in einem Register zugewiesen wurde. C ist eine sehr "maschennah" Sprache. Es erscheint plausibel und im Einklang mit dem Design von C, dass es ein Sprachmerkmal gibt, das im Grunde bedeutet, "weiterhin den Wert zu verwenden, den ich gerade zugewiesen habe". Es ist sehr einfach, einen Codegenerator für diese Funktion zu schreiben. Sie verwenden einfach weiterhin das Register, in dem der zugewiesene Wert gespeichert ist.

Eric Lippert
quelle
1
Wusste nicht, dass alles, was einen Wert zurückgibt (sogar
Instanzkonstruktion
9
@ user437291: Wenn die Instanzkonstruktion kein Ausdruck war, konnten Sie mit dem konstruierten Objekt nichts tun - Sie konnten es keinem zuweisen, Sie konnten es nicht an eine Methode übergeben und Sie konnten keine Methoden aufrufen darauf. Wäre ziemlich nutzlos, nicht wahr?
Timwi
1
Das Ändern einer Variablen ist eigentlich "nur" ein Nebeneffekt des Zuweisungsoperators, der beim Erstellen eines Ausdrucks einen Wert als Hauptzweck liefert.
mk12
2
"Ihr Verständnis ist 100% falsch." - Während die Verwendung der englischen Sprache des OP ist nicht die größte, sein / ihr „Verständnis“ über das, was sollte der Fall sein, ist es eine Meinung zu machen, so dass es nicht die Art von Dingen ist , als man „100% falsch“ sein . Einige Sprachdesigner stimmen dem "Soll" des OP zu und machen Aufgaben ohne Wert oder verbieten sie auf andere Weise, Unterausdrücke zu sein. Ich denke, das ist eine Überreaktion auf den =/ ==Tippfehler, der leicht behoben werden kann, indem der Wert von =nicht zugelassen wird, es sei denn, er steht in Klammern. zB if ((x = y))oder if ((x = y) == true)ist erlaubt, aber if (x = y)nicht.
Jim Balter
"Wusste nicht, dass alles, was einen Wert zurückgibt ... als Ausdruck betrachtet wird" - Hmm ... alles, was einen Wert zurückgibt, wird als Ausdruck betrachtet - die beiden sind praktisch synonym.
Jim Balter
44

Hast du nicht die Antwort gegeben? Es soll genau die Arten von Konstrukten aktivieren, die Sie erwähnt haben.

Ein häufiger Fall, in dem diese Eigenschaft des Zuweisungsoperators verwendet wird, ist das Lesen von Zeilen aus einer Datei ...

string line;
while ((line = streamReader.ReadLine()) != null)
    // ...
Timwi
quelle
1
+1 Dies muss eine der praktischsten Anwendungen dieses Konstrukts sein
Mike Burton
11
Ich mag es wirklich für wirklich einfache Eigenschaftsinitialisierer, aber Sie müssen vorsichtig sein und Ihre Linie für "einfach" niedrig für die Lesbarkeit ziehen: return _myBackingField ?? (_myBackingField = CalculateBackingField());Viel weniger Spreu als auf Null zu prüfen und zuzuweisen.
Tom Mayfield
34

Meine bevorzugte Verwendung von Zuweisungsausdrücken ist für träge initialisierte Eigenschaften.

private string _name;
public string Name
{
    get { return _name ?? (_name = ExpensiveNameGeneratorMethod()); }
}
Nathan Baulch
quelle
5
Deshalb haben so viele Leute die ??=Syntax vorgeschlagen.
Konfigurator
27

Zum einen können Sie Ihre Aufgaben wie in Ihrem Beispiel verketten:

a = b = c = 16;

Zum anderen können Sie ein Ergebnis in einem einzigen Ausdruck zuweisen und überprüfen:

while ((s = foo.getSomeString()) != null) { /* ... */ }

Beides sind möglicherweise zweifelhafte Gründe, aber es gibt definitiv Leute, die diese Konstrukte mögen.

Michael Burr
quelle
5
+1 Verkettung ist keine kritische Funktion, aber es ist schön zu sehen, dass sie "einfach funktioniert", wenn so viele Dinge nicht funktionieren.
Mike Burton
+1 auch für die Verkettung; Ich hatte das immer als einen Sonderfall angesehen, der eher von der Sprache als von der natürlichen Nebenwirkung von "Ausdrücken, die Werte zurückgeben" behandelt wurde.
Caleb Bell
2
Es erlaubt Ihnen auch, die Zuordnung in Retouren zu verwenden:return (HttpContext.Current.Items["x"] = myvar);
Serge Shultz
14

Abgesehen von den bereits genannten Gründen (Zuordnungsverkettung, Setzen und Testen innerhalb von while-Schleifen usw.) benötigen Sie diese Funktion , um die Anweisung richtig zu verwenden using:

using (Font font3 = new Font("Arial", 10.0f))
{
    // Use font3.
}

MSDN rät davon ab, das Einwegobjekt außerhalb der using-Anweisung zu deklarieren, da es auch nach seiner Entsorgung im Geltungsbereich bleibt (siehe den von mir verlinkten MSDN-Artikel).

Martin Törnwall
quelle
4
Ich denke nicht, dass dies wirklich eine Aufgabenverkettung an sich ist.
Kenny
1
@kenny: Äh ... Nein, aber ich habe es auch nie behauptet. Wie ich bereits sagte, wäre die using-Anweisung , abgesehen von den bereits erwähnten Gründen - einschließlich der Verkettung von Zuweisungen - viel schwieriger zu verwenden, wenn der Zuweisungsoperator nicht das Ergebnis der Zuweisungsoperation zurückgeben würde. Dies hängt überhaupt nicht wirklich mit der Verkettung von Aufgaben zusammen.
Martin Törnwall
1
Aber wie Eric oben erwähnt hat, "geben Zuweisungsanweisungen keinen Wert zurück". Dies ist eigentlich nur die Sprachsyntax der using-Anweisung. Ein Beweis dafür ist, dass man in einer using-Anweisung keinen "Zuweisungsausdruck" verwenden kann.
Kenny
@kenny: Entschuldigung, meine Terminologie war eindeutig falsch. Mir ist klar, dass die using-Anweisung implementiert werden könnte, ohne dass die Sprache allgemeine Unterstützung für Zuweisungsausdrücke hat, die einen Wert zurückgeben. Das wäre in meinen Augen allerdings furchtbar inkonsistent.
Martin Törnwall
2
@kenny: Eigentlich kann man entweder eine Definitions- und Zuweisungsanweisung oder einen beliebigen Ausdruck in verwenden using. Alle diese sind legal: using (X x = new X()), using (x = new X()), using (x). In diesem Beispiel ist der Inhalt der using-Anweisung jedoch eine spezielle Syntax, die überhaupt nicht von der Zuweisung eines Werts abhängt. Sie Font font3 = new Font("Arial", 10.0f)ist kein Ausdruck und an keiner Stelle gültig, an der Ausdrücke erwartet werden.
Konfigurator
10

Ich möchte auf einen bestimmten Punkt eingehen, den Eric Lippert in seiner Antwort angesprochen hat, und einen besonderen Anlass ins Rampenlicht rücken, der von niemand anderem berührt wurde. Eric sagte:

[...] Eine Zuordnung hinterlässt fast immer den Wert, der gerade in einem Register zugewiesen wurde.

Ich möchte sagen, dass die Zuweisung immer den Wert hinterlässt, den wir versucht haben, unserem linken Operanden zuzuweisen. Nicht nur "fast immer". Aber ich weiß es nicht, weil ich dieses Problem in der Dokumentation nicht kommentiert habe. Es könnte theoretisch ein sehr effektiv implementiertes Verfahren sein, den linken Operanden "zurückzulassen" und nicht neu zu bewerten, aber ist es effizient?

'Effizient' ja für alle Beispiele, die bisher in den Antworten dieses Threads erstellt wurden. Aber effizient bei Eigenschaften und Indexern, die get- und set-Accessoren verwenden? Überhaupt nicht. Betrachten Sie diesen Code:

class Test
{
    public bool MyProperty { get { return true; } set { ; } }
}

Hier haben wir eine Eigenschaft, die nicht einmal ein Wrapper für eine private Variable ist. Wann immer er aufgefordert wird, wird er wahr zurückkehren, wann immer jemand versucht, seinen Wert festzulegen, wird er nichts tun. Wenn also diese Eigenschaft bewertet wird, muss er wahr sein. Mal sehen was passiert:

Test test = new Test();

if ((test.MyProperty = false) == true)
    Console.WriteLine("Please print this text.");

else
    Console.WriteLine("Unexpected!!");

Ratet mal, was es druckt? Es wird gedruckt Unexpected!!. Wie sich herausstellt, wird der Set-Accessor tatsächlich aufgerufen, was nichts bewirkt. Danach wird der get accessor überhaupt nicht mehr aufgerufen. Die Zuordnung hinterlässt einfach den falseWert, den wir versucht haben, unserem Eigentum zuzuweisen. Und dieser falseWert wird von der if-Anweisung ausgewertet.

Ich werde mit einem Beispiel aus der Praxis abschließen, das mich dazu gebracht hat, dieses Problem zu untersuchen. Ich habe einen Indexer erstellt, der ein praktischer Wrapper für eine Sammlung ( List<string>) war, die eine Klasse von mir als private Variable hatte.

Der an den Indexer gesendete Parameter war eine Zeichenfolge, die als Wert in meiner Sammlung behandelt werden sollte. Der get-Accessor würde einfach true oder false zurückgeben, wenn dieser Wert in der Liste vorhanden ist oder nicht. Daher war der get-Accessor eine andere Möglichkeit, die List<T>.ContainsMethode zu verwenden.

Wenn der Set-Accessor des Indexers mit einer Zeichenfolge als Argument aufgerufen wurde und der rechte Operand ein Bool war true, würde er diesen Parameter zur Liste hinzufügen. Wenn jedoch derselbe Parameter an den Accessor gesendet wurde und der richtige Operand ein Bool war false, löschte er stattdessen das Element aus der Liste. Daher wurde der Set-Accessor als bequeme Alternative zu List<T>.Addund verwendet List<T>.Remove.

Ich dachte, ich hätte eine ordentliche und kompakte "API", die die Liste mit meiner eigenen Logik umschließt, die als Gateway implementiert ist. Mit Hilfe eines Indexers allein konnte ich viele Dinge mit ein paar Tastenanschlägen erledigen. Wie kann ich beispielsweise versuchen, meiner Liste einen Wert hinzuzufügen und zu überprüfen, ob er dort enthalten ist? Ich dachte, dies sei die einzige notwendige Codezeile:

if (myObject["stringValue"] = true)
    ; // Set operation succeeded..!

Aber wie mein früheres Beispiel gezeigt hat, wurde der get-Accessor, der sehen soll, ob der Wert wirklich in der Liste enthalten ist, nicht einmal aufgerufen. Der trueWert blieb immer zurück und zerstörte effektiv die Logik, die ich in meinem get accessor implementiert hatte.

Martin Andersson
quelle
Sehr interessante Antwort. Und es hat mich dazu gebracht, positiv über testgetriebene Entwicklung nachzudenken.
Elliot
1
Dies ist zwar interessant, aber es ist ein Kommentar zu Erics Antwort, keine Antwort auf die Frage des OP.
Jim Balter
Ich würde nicht erwarten, dass ein versuchter Zuweisungsausdruck zuerst den Wert der Zieleigenschaft erhält. Wenn die Variable veränderbar ist und die Typen übereinstimmen, warum sollte es dann wichtig sein, was der alte Wert war? Stellen Sie einfach den neuen Wert ein. Ich bin allerdings kein Fan von Aufgaben in IF-Tests. Gibt es true zurück, weil die Zuweisung erfolgreich war, oder ist es ein Wert, z. B. eine positive Ganzzahl, die zu true gezwungen wird, oder weil Sie einen Booleschen Wert zuweisen? Unabhängig von der Implementierung ist die Logik nicht eindeutig.
Davos
6

Wenn die Zuweisung keinen Wert zurückgeben würde, würde die Zeile a = b = c = 16auch nicht funktionieren.

Auch Dinge wie schreiben zu while ((s = readLine()) != null)können, kann manchmal nützlich sein.

Der Grund dafür, dass die Zuweisung den zugewiesenen Wert zurückgibt, besteht darin, dass Sie diese Dinge tun.

sepp2k
quelle
Niemand hat Nachteile erwähnt - ich habe mich gefragt, warum Zuweisungen nicht null zurückgeben können, damit der Fehler "Gemeinsame Zuweisung gegen Vergleich 'wenn (etwas = 1) {}' nicht kompiliert werden kann, anstatt immer true zurückzugeben. Ich sehe jetzt, dass das ... andere Probleme schaffen würde!
Jon
1
@Jon dieser Fehler könnte leicht verhindert werden, indem das =Auftreten in einem Ausdruck, der nicht in Klammern steht, nicht zugelassen oder gewarnt wird (ohne Berücksichtigung der Parens der if / while-Anweisung selbst). gcc gibt solche Warnungen aus und hat dadurch diese Klasse von Fehlern aus C / C ++ - Programmen, die damit kompiliert wurden, im Wesentlichen beseitigt. Es ist eine Schande, dass andere Compiler-Autoren diesem und einigen anderen guten Ideen in gcc so wenig Aufmerksamkeit geschenkt haben.
Jim Balter
4

Ich denke, Sie verstehen falsch, wie der Parser diese Syntax interpretieren wird. Die Zuordnung wird bewertet zuerst , und das Ergebnis wird dann auf NULL verglichen werden, dh die Aussage entspricht:

s = "Hello"; //s now contains the value "Hello"
(s != null) //returns true.

Wie andere bereits betont haben, ist das Ergebnis einer Zuweisung der zugewiesene Wert. Es fällt mir schwer, mir den Vorteil vorzustellen

((s = "Hello") != null)

und

s = "Hello";
s != null;

nicht gleichwertig sein ...

Dan J.
quelle
Während Sie aus praktischer Sicht Recht haben, dass Ihre beiden Zeilen gleichwertig sind, ist Ihre Aussage, dass die Zuweisung keinen Wert zurückgibt, technisch nicht wahr. Mein Verständnis ist, dass das Ergebnis einer Zuweisung ein Wert ist (gemäß der C # -Spezifikation) und sich in diesem Sinne nicht von dem Additionsoperator in einem Ausdruck wie 1 + 2 + 3 unterscheidet
Steve Ellinger
3

Ich denke, der Hauptgrund ist die (absichtliche) Ähnlichkeit mit C ++ und C. Der Assigment-Operator (und viele andere Sprachkonstrukte) verhält sich wie seine C ++ - Gegenstücke und folgt nur dem Prinzip der geringsten Überraschung, und jeder Programmierer, der von einem anderen Curly- stammt. Bracket-Sprache kann sie verwenden, ohne viel nachzudenken. Für C ++ - Programmierer leicht zu erlernen, war eines der Hauptentwurfsziele für C #.

tdammers
quelle
2

Aus den beiden Gründen, die Sie in Ihren Beitrag aufnehmen,
1) können Sie dies tun, a = b = c = 16
2) um zu testen, ob eine Aufgabe erfolgreich war if ((s = openSomeHandle()) != null)

JamesMLV
quelle
1

Die Tatsache, dass 'a ++' oder 'printf ("foo")' entweder als in sich geschlossene Anweisung oder als Teil eines größeren Ausdrucks nützlich sein kann, bedeutet, dass C die Möglichkeit berücksichtigen muss, dass Ausdrucksergebnisse sein können oder nicht gebraucht. Angesichts dessen gibt es eine allgemeine Vorstellung, dass Ausdrücke, die einen Wert sinnvollerweise "zurückgeben" könnten, dies auch tun könnten. Die Zuordnungsverkettung kann in C leicht "interessant" und in C ++ sogar noch interessanter sein, wenn nicht alle fraglichen Variablen genau denselben Typ haben. Solche Verwendungen werden wahrscheinlich am besten vermieden.

Superkatze
quelle
1

Ein weiterer guter Anwendungsfall, den ich ständig benutze:

var x = _myVariable ?? (_myVariable = GetVariable());
//for example: when used inside a loop, "GetVariable" will be called only once
Jazzkatze
quelle
0

Ein zusätzlicher Vorteil, den ich in den Antworten hier nicht sehe, ist, dass die Syntax für die Zuweisung auf Arithmetik basiert.

Bedeutet jetzt x = y = b = c = 2 + 3etwas anderes in der Arithmetik als eine Sprache im C-Stil; in arithmetischer seine Behauptung, wir sagen , dass x zu y usw. , und in einer C-Stil Sprache gleich ist es ein Befehl ist , dass Marken x zu y usw. gleich nachdem er ausgeführt wird.

Trotzdem gibt es immer noch genug Beziehungen zwischen der Arithmetik und dem Code, so dass es keinen Sinn macht, das Natürliche in der Arithmetik zu verbieten, es sei denn, es gibt einen guten Grund. (Die andere Sache, die Sprachen im C-Stil aus der Verwendung des Gleichheits-Symbols übernommen haben, ist die Verwendung von == für den Gleichheitsvergleich. Hier jedoch wäre diese Art der Verkettung unmöglich, da == ganz rechts einen Wert zurückgibt.)

Jon Hanna
quelle
@ JimBalter Ich frage mich, ob Sie in fünf Jahren tatsächlich ein Argument dagegen vorbringen könnten.
Jon Hanna
@ JimBalter nein, dein zweiter Kommentar ist eigentlich ein Gegenargument und ein vernünftiges. Meine Denkretorte war, weil Ihr erster Kommentar nicht war. Wenn wir jedoch Arithmetik lernen, a = b = cbedeutet Lernen , dass aund bund cdasselbe sind. Wenn eine C - Stil Sprache zu lernen lernen wir , dass nach a = b = c, aund bdasselbe sind wie c. Es gibt sicherlich einen Unterschied in der Semantik, wie meine Antwort selbst sagt, aber als ich als Kind zum ersten Mal lernte, in einer Sprache zu programmieren, die zwar =für Aufgaben verwendet wurde, dies aber nicht zuließ, a = b = cschien mir dies jedoch unvernünftig…
Jon Hanna
… Unvermeidlich, weil diese Sprache auch =für den Gleichheitsvergleich verwendet wird. Das a = b = cmüsste also bedeuten, was a = b == cin Sprachen im C-Stil bedeutet. Ich fand die in C erlaubte Verkettung viel intuitiver, weil ich eine Analogie zur Arithmetik ziehen konnte.
Jon Hanna
0

Ich verwende gerne den Rückgabewert für die Zuweisung, wenn ich eine Reihe von Informationen aktualisieren und zurückgeben muss, ob Änderungen vorgenommen wurden oder nicht:

bool hasChanged = false;

hasChanged |= thing.Property != (thing.Property = "Value");
hasChanged |= thing.AnotherProperty != (thing.AnotherProperty = 42);
hasChanged |= thing.OneMore != (thing.OneMore = "They get it");

return hasChanged;

Sei aber vorsichtig. Sie könnten denken, Sie können es auf diese verkürzen:

return thing.Property != (thing.Property = "Value") ||
    thing.AnotherProperty != (thing.AnotherProperty = 42) ||
    thing.OneMore != (thing.OneMore = "They get it");

Dies stoppt jedoch tatsächlich die Auswertung der oder -Anweisungen, nachdem die erste Wahrheit gefunden wurde. In diesem Fall bedeutet dies, dass die Zuweisung nachfolgender Werte beendet wird, sobald der erste abweichende Wert zugewiesen wird.

Siehe https://dotnetfiddle.net/e05Rh8 , um damit herumzuspielen

Luke Kubat
quelle