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: frob
ist 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 x
ist 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.