Warum sind C # 3.0-Objektinitialisierer-Konstruktorklammern optional?

114

Es scheint, dass die C # 3.0-Objektinitialisierersyntax es ermöglicht, das Klammerpaar open / close im Konstruktor auszuschließen, wenn ein parameterloser Konstruktor vorhanden ist. Beispiel:

var x = new XTypeName { PropA = value, PropB = value };

Im Gegensatz zu:

var x = new XTypeName() { PropA = value, PropB = value };

Ich bin gespannt, warum das Klammerpaar zum Öffnen / Schließen des Konstruktors hier nachher optional ist XTypeName.

James Dunne
quelle
9
Abgesehen davon haben wir dies letzte Woche in einer Codeüberprüfung gefunden: var list = new List <Foo> {}; Wenn etwas missbraucht werden kann ...
Blu
@blu Das ist ein Grund, warum ich diese Frage stellen wollte. Ich habe die Inkonsistenz in unserem Code bemerkt. Inkonsistenzen im Allgemeinen stören mich und ich dachte, ich würde sehen, ob es eine gute Begründung für die Optionalität in der Syntax gibt. :)
James Dunne

Antworten:

143

Diese Frage war das Thema meines Blogs am 20. September 2010 . Die Antworten von Josh und Chad ("Sie bieten keinen Mehrwert, warum also sie benötigen?" Und "Redundanz beseitigen") sind grundsätzlich richtig. Um das ein bisschen mehr zu konkretisieren:

Die Funktion, mit der Sie die Argumentliste als Teil der "größeren Funktion" von Objektinitialisierern entfernen können, hat unsere Messlatte für "zuckerhaltige" Funktionen erfüllt. Einige Punkte, die wir berücksichtigt haben:

  • Die Design- und Spezifikationskosten waren gering
  • Wir wollten den Parser-Code, der ohnehin die Objekterstellung übernimmt, umfassend ändern. Die zusätzlichen Entwicklungskosten für die Optionierung der Parameterliste waren im Vergleich zu den Kosten für das größere Feature nicht hoch
  • Die Testlast war im Vergleich zu den Kosten des größeren Merkmals relativ gering
  • Der Dokumentationsaufwand war im Vergleich ...
  • Der Wartungsaufwand wurde als gering eingeschätzt. Ich kann mich an keine Fehler erinnern, die in den letzten Jahren in dieser Funktion gemeldet wurden.
  • Die Funktion stellt keine unmittelbar offensichtlichen Risiken für zukünftige Funktionen in diesem Bereich dar. (Das Letzte, was wir tun möchten, ist, jetzt eine billige, einfache Funktion zu erstellen, die es viel schwieriger macht, in Zukunft eine überzeugendere Funktion zu implementieren.)
  • Die Funktion fügt der lexikalischen, grammatikalischen oder semantischen Analyse der Sprache keine neuen Mehrdeutigkeiten hinzu. Es stellt keine Probleme für die Art der "Teilprogramm" -Analyse dar, die von der "IntelliSense" -Engine der IDE während der Eingabe durchgeführt wird. Und so weiter.
  • Das Feature trifft einen gemeinsamen "Sweet Spot" für das größere Objektinitialisierungs-Feature. Wenn Sie einen Objektinitialisierer verwenden, liegt dies normalerweise genau daran, dass Sie mit dem Konstruktor des Objekts die gewünschten Eigenschaften nicht festlegen können. Es ist sehr üblich, dass solche Objekte einfach "Eigentumstaschen" sind, die überhaupt keine Parameter im ctor haben.

Warum hast du nicht auch leere Klammern optional machen im Standardkonstruktors Aufruf einer Objekterstellung Ausdruck, der sich nicht um ein Objekt initializer haben?

Schauen Sie sich diese Liste der Kriterien oben noch einmal an. Eine davon ist, dass die Änderung keine neue Mehrdeutigkeit in die lexikalische, grammatikalische oder semantische Analyse eines Programms einführt. Ihre vorgeschlagene Änderung führt zu einer Mehrdeutigkeit der semantischen Analyse:

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

Zeile 1 erstellt ein neues C, ruft den Standardkonstruktor auf und ruft dann die Instanzmethode M für das neue Objekt auf. Zeile 2 erstellt eine neue Instanz von BM und ruft den Standardkonstruktor auf. Wenn die Klammern in Zeile 1 optional wären, wäre Zeile 2 mehrdeutig. Wir müssten uns dann eine Regel ausdenken, um die Mehrdeutigkeit zu lösen. Wir konnten es nicht zu einem Fehler machen, da dies dann eine wichtige Änderung wäre, die ein vorhandenes legales C # -Programm in ein fehlerhaftes Programm umwandelt.

Daher müsste die Regel sehr kompliziert sein: Im Wesentlichen sind die Klammern nur dann optional, wenn sie keine Mehrdeutigkeiten verursachen. Wir müssten alle möglichen Fälle analysieren, die zu Mehrdeutigkeiten führen, und dann Code in den Compiler schreiben, um sie zu erkennen.

In diesem Licht gehen Sie zurück und schauen Sie sich alle Kosten an, die ich erwähne. Wie viele von ihnen werden jetzt groß? Komplizierte Regeln verursachen hohe Kosten für Design, Spezifikation, Entwicklung, Test und Dokumentation. Komplizierte Regeln verursachen in Zukunft viel häufiger Probleme mit unerwarteten Interaktionen mit Funktionen.

Alles für was? Ein winziger Kundennutzen, der der Sprache keine neue Repräsentationskraft verleiht, sondern verrückte Eckfälle hinzufügt, die nur darauf warten, eine arme, ahnungslose Seele, die darauf stößt, mit "Gotcha" anzuschreien. Solche Funktionen werden sofort gekürzt und in die Liste "Niemals tun" aufgenommen.

Wie haben Sie diese besondere Mehrdeutigkeit festgestellt?

Dieser war sofort klar; Ich bin mit den Regeln in C # ziemlich vertraut, um festzustellen, wann ein gepunkteter Name erwartet wird.

Wie stellen Sie bei der Betrachtung einer neuen Funktion fest, ob sie zu Mehrdeutigkeiten führt? Von Hand, durch formale Beweise, durch maschinelle Analyse, was?

Alle drei. Meistens schauen wir uns nur die Spezifikation und die Nudeln an, wie ich es oben getan habe. Angenommen, wir möchten C # einen neuen Präfixoperator namens "frob" hinzufügen:

x = frob 123 + 456;

(UPDATE: frobist natürlich await; die Analyse hier ist im Wesentlichen die Analyse, die das Designteam beim Hinzufügen durchlaufen hat await.)

"frob" ist hier wie "neu" oder "++" - es kommt vor einem Ausdruck irgendeiner Art. Wir erarbeiten die gewünschte Priorität und Assoziativität usw. und stellen dann Fragen wie "Was ist, wenn das Programm bereits einen Typ, ein Feld, eine Eigenschaft, ein Ereignis, eine Methode, eine Konstante oder einen lokalen Namen namens frob hat?" Das würde sofort zu Fällen führen wie:

frob x = 10;

Bedeutet das "Führen Sie die frob-Operation für das Ergebnis von x = 10 aus oder erstellen Sie eine Variable vom Typ frob mit dem Namen x und weisen Sie ihr 10 zu?" (Oder wenn frobbing eine Variable erzeugt, kann es sich um eine Zuweisung von 10 zu handeln frob x. Immerhin wird *x = 10;analysiert und ist legal , wenn dies der Fall xist int*.)

G(frob + x)

Bedeutet das "frob das Ergebnis des unären Plus-Operators auf x" oder "füge Ausdruck frob zu x hinzu"?

Und so weiter. Um diese Unklarheiten zu beseitigen, könnten wir Heuristiken einführen. Wenn Sie "var x = 10;" sagen das ist nicht eindeutig; es könnte bedeuten "den Typ von x ableiten" oder es könnte bedeuten "x ist vom Typ var". Wir haben also eine Heuristik: Wir versuchen zuerst, einen Typ namens var nachzuschlagen, und nur wenn einer nicht existiert, schließen wir auf den Typ von x.

Oder wir ändern die Syntax so, dass sie nicht mehrdeutig ist. Als sie C # 2.0 entwarfen, hatten sie dieses Problem:

yield(x);

Bedeutet das "Ausbeute x in einem Iterator" oder "Aufruf der Ertragsmethode mit Argument x"? Durch Ändern auf

yield return(x);

es ist jetzt eindeutig.

Bei optionalen Parens in einem Objektinitialisierer ist es einfach zu überlegen, ob Mehrdeutigkeiten eingeführt wurden oder nicht, da die Anzahl der Situationen, in denen es zulässig ist, etwas einzuführen, das mit {beginnt, sehr gering ist . Grundsätzlich nur verschiedene Anweisungskontexte, Anweisungs-Lambdas, Array-Initialisierer und das war's auch schon. Es ist einfach, alle Fälle zu durchdenken und zu zeigen, dass es keine Unklarheiten gibt. Es ist etwas schwieriger sicherzustellen, dass die IDE effizient bleibt, kann aber ohne allzu große Probleme durchgeführt werden.

Diese Art des Herumspielens mit der Spezifikation ist normalerweise ausreichend. Wenn es eine besonders schwierige Funktion ist, ziehen wir schwerere Werkzeuge heraus. Beim Entwerfen von LINQ haben sich beispielsweise einer der Compiler-Mitarbeiter und einer der IDE-Mitarbeiter, die beide über einen Hintergrund in der Parser-Theorie verfügen, einen Parser-Generator erstellt, der Grammatiken auf Mehrdeutigkeiten analysieren und dann vorgeschlagene C # -Grammatiken für das Abfrageverständnis einspeisen kann ;; Dabei wurden viele Fälle festgestellt, in denen Abfragen nicht eindeutig waren.

Oder als wir in C # 3.0 eine erweiterte Typinferenz für Lambdas durchgeführt haben, haben wir unsere Vorschläge geschrieben und sie dann über den Teich an Microsoft Research in Cambridge gesendet, wo das dortige Sprachteam gut genug war, um einen formalen Beweis für den Typinferenzvorschlag auszuarbeiten theoretisch gesund.

Gibt es heute Unklarheiten in C #?

Sicher.

G(F<A, B>(0))

In C # 1 ist klar, was das bedeutet. Es ist das gleiche wie:

G( (F<A), (B>0) )

Das heißt, es ruft G mit zwei Argumenten auf, die Bools sind. In C # 2 könnte dies bedeuten, was es in C # 1 bedeutete, aber es könnte auch bedeuten, "0 an die generische Methode F übergeben, die die Typparameter A und B verwendet, und dann das Ergebnis von F an G übergeben". Wir haben dem Parser eine komplizierte Heuristik hinzugefügt, die bestimmt, welchen der beiden Fälle Sie wahrscheinlich gemeint haben.

In ähnlicher Weise sind Casts auch in C # 1.0 nicht eindeutig:

G((T)-x)

Ist das "-x nach T umwandeln" oder "x von T subtrahieren"? Wieder haben wir eine Heuristik, die eine gute Vermutung macht.

Eric Lippert
quelle
3
Oh, tut mir leid, ich habe vergessen ... Der Ansatz des Fledermaussignals scheint, obwohl er zu funktionieren scheint, (IMO) einem direkten Kontaktmittel vorzuziehen, bei dem man nicht die für die öffentliche Bildung gewünschte öffentliche Bekanntheit in Form eines SO-Postings erhalten würde ist indexierbar, durchsuchbar und kann leicht referenziert werden. Sollen wir uns stattdessen direkt an uns wenden, um einen inszenierten SO-Post / Answer-Tanz zu choreografieren? :)
James Dunne
5
Ich würde empfehlen, dass Sie es vermeiden, einen Beitrag zu inszenieren. Das wäre nicht fair gegenüber anderen, die möglicherweise zusätzliche Einblicke in die Frage haben. Ein besserer Ansatz wäre, die Frage zu posten und dann einen Link per E-Mail zu senden, in dem Sie um Teilnahme gebeten werden.
Chilltemp
1
@ James: Ich habe meine Antwort aktualisiert, um Ihre Folgefrage zu beantworten.
Eric Lippert
8
@Eric, ist es möglich, dass du über diese Liste "Mach das nie" bloggen kannst? Ich bin gespannt auf andere Beispiele, die niemals Teil der C # -Sprache sein werden :)
Ilya Ryzhenkov
2
@ Eric: Ich schätze deine Geduld mit mir wirklich sehr, sehr , sehr :) Danke! Sehr informativ.
James Dunne
12

Denn so wurde die Sprache angegeben. Sie bieten keinen Mehrwert. Warum sollten sie also einbezogen werden?

Es ist auch sehr ähnlich zu implizit typisierten Arrays

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

Referenz: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
quelle
1
Sie fügen keinen Wert in dem Sinne hinzu, dass es offensichtlich sein sollte, was die Absicht ist, aber es bricht mit der Konsistenz darin, dass wir jetzt zwei unterschiedliche Objektkonstruktionssyntaxen haben, eine mit erforderlichen Klammern (und durch Kommas getrennte Argumentausdrücke darin) und eine ohne .
James Dunne
1
@ James Dunne, es ist tatsächlich eine sehr ähnliche Syntax wie implizit typisierte Array-Syntax, siehe meine Bearbeitung. Es gibt keinen Typ, keinen Konstruktor, und die Absicht ist offensichtlich, so dass es nicht notwendig ist, sie zu deklarieren
CaffGeek
7

Dies wurde getan, um die Konstruktion von Objekten zu vereinfachen. Die Sprachdesigner haben (meines Wissens) nicht ausdrücklich gesagt, warum sie dies für nützlich hielten, obwohl dies auf der Seite mit den C # Version 3.0-Spezifikationen ausdrücklich erwähnt wird :

Ein Objekterstellungsausdruck kann die Konstruktorargumentliste und Klammern weglassen, vorausgesetzt, er enthält ein Objekt oder einen Sammlungsinitialisierer. Das Weglassen der Konstruktorargumentliste und das Einschließen von Klammern entspricht der Angabe einer leeren Argumentliste.

Ich nehme an, dass sie der Meinung waren, dass die Klammer in diesem Fall nicht erforderlich war, um die Entwicklerabsicht anzuzeigen, da der Objektinitialisierer die Absicht anzeigt, stattdessen die Eigenschaften des Objekts zu konstruieren und festzulegen.

Reed Copsey
quelle
4

In Ihrem ersten Beispiel schließt der Compiler, dass Sie den Standardkonstruktor aufrufen (die C # 3.0-Sprachspezifikation besagt, dass der Standardkonstruktor aufgerufen wird, wenn keine Klammern angegeben sind).

Im zweiten Schritt rufen Sie explizit den Standardkonstruktor auf.

Sie können diese Syntax auch verwenden, um Eigenschaften festzulegen und Werte explizit an den Konstruktor zu übergeben. Wenn Sie die folgende Klassendefinition hatten:

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

Alle drei Aussagen sind gültig:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};
Justin Niessner
quelle
Richtig. Im ersten und zweiten Fall in Ihrem Beispiel sind diese funktional identisch, richtig?
James Dunne
1
@ James Dunne - Richtig. Dies ist der Teil, der in der Sprachspezifikation angegeben ist. Leere Klammern sind überflüssig, aber Sie können sie trotzdem angeben.
Justin Niessner
1

Ich bin kein Eric Lippert, daher kann ich nicht sicher sagen, aber ich würde annehmen, dass der Compiler die leere Klammer nicht benötigt, um auf das Initialisierungskonstrukt schließen zu können. Daher werden Informationen redundant und nicht benötigt.

Josh
quelle
Richtig, es ist überflüssig, aber ich bin nur neugierig, warum die plötzliche Einführung optional ist. Es scheint mit der Konsistenz der Sprachsyntax zu brechen. Wenn ich nicht die offene geschweifte Klammer hatte, um einen Initialisierungsblock anzuzeigen, sollte dies eine unzulässige Syntax sein. Komisch, dass Sie Mr. Lippert erwähnen, ich habe öffentlich nach seiner Antwort gefischt, damit ich und andere von einer müßigen Neugier profitieren. :)
James Dunne