Nachdem ich mir das Webinar Jon Skeet Inspects ReSharper angesehen hatte , begann ich ein wenig mit rekursiven Konstruktoraufrufen zu spielen und stellte fest, dass der folgende Code gültiger C # -Code ist (mit gültig meine ich, dass er kompiliert wird).
class Foo
{
int a = null;
int b = AppDomain.CurrentDomain;
int c = "string to int";
int d = NonExistingMethod();
int e = Invalid<Method>Name<<Indeeed();
Foo() :this(0) { }
Foo(int v) :this() { }
}
Wie wir alle wahrscheinlich wissen, wird die Feldinitialisierung vom Compiler in den Konstruktor verschoben. Wenn Sie also ein Feld wie haben int a = 42;
, haben Sie a = 42
in allen Konstruktoren. Wenn Sie jedoch einen Konstruktor haben, der einen anderen Konstruktor aufruft, haben Sie Initialisierungscode nur in aufgerufenem.
Wenn Sie beispielsweise einen Konstruktor mit Parametern haben, die den Standardkonstruktor aufrufen, haben Sie die Zuweisung a = 42
nur im Standardkonstruktor.
Um den zweiten Fall zu veranschaulichen, folgender Code:
class Foo
{
int a = 42;
Foo() :this(60) { }
Foo(int v) { }
}
Kompiliert in:
internal class Foo
{
private int a;
private Foo()
{
this.ctor(60);
}
private Foo(int v)
{
this.a = 42;
base.ctor();
}
}
Das Hauptproblem ist also, dass mein Code, der zu Beginn dieser Frage angegeben wurde, wie folgt kompiliert ist:
internal class Foo
{
private int a;
private int b;
private int c;
private int d;
private int e;
private Foo()
{
this.ctor(0);
}
private Foo(int v)
{
this.ctor();
}
}
Wie Sie sehen, kann der Compiler nicht entscheiden, wo die Feldinitialisierung abgelegt werden soll, und legt sie daher nirgendwo ab. Beachten Sie auch, dass es keine base
Konstruktoraufrufe gibt. Natürlich können keine Objekte erstellt werden, und Sie werden immer am Ende sein, StackOverflowException
wenn Sie versuchen, eine Instanz von zu erstellen Foo
.
Ich habe zwei Fragen:
Warum erlaubt der Compiler überhaupt rekursive Konstruktoraufrufe?
Warum beobachten wir ein solches Verhalten des Compilers für Felder, die innerhalb einer solchen Klasse initialisiert wurden?
Einige Hinweise: ReSharper warnt Sie mit Possible cyclic constructor calls
. Darüber hinaus werden solche Konstruktoraufrufe in Java nicht als Ereignis kompiliert, sodass der Java-Compiler in diesem Szenario restriktiver ist (Jon erwähnte diese Informationen im Webinar).
Dies macht diese Fragen interessanter, da der C # -Compiler in Bezug auf die Java-Community zumindest moderner ist.
Dies wurde mit C # 4.0- und C # 5.0- Compilern kompiliert und mit dotPeek dekompiliert .
int a = null; int b = AppDomain.CurrentDomain; int c = "string to int"; int d = NonExistingMethod(); int e = Invalid<Method>Name<<Indeeed();
Man sollte ein Quiz machen: "In welcher Situation sind diese Felddeklarationen in Ordnung?" (Es gibt eine Warnung, dass die Felder nicht verwendet werden, aber Sie können diese Warnung entfernen, indem Sie jedes Feld im Körper eines der Intance-Konstruktoren (oder anderswo) lesen.)Antworten:
Interessanter Fund.
Es scheint, dass es wirklich nur zwei Arten von Instanzkonstruktoren gibt:
: this( ...)
Syntax verkettet.: base()
die Standardeinstellung ist.(Ich habe den Instanzkonstruktor ignoriert, dessen
System.Object
Sonderfall ein Sonderfall ist. ErSystem.Object
hat keine Basisklasse! AberSystem.Object
auch keine Felder.)Die Instanzfeldinitialisierer, die möglicherweise in der Klasse vorhanden sind, müssen in den Anfang des Hauptteils aller Instanzkonstruktoren vom Typ 2 oben kopiert werden , während keine Instanzkonstruktoren vom Typ 1 den Feldzuweisungscode benötigen.
Anscheinend muss der C # -Compiler also keine Analyse der Konstruktoren vom Typ 1 durchführen, um festzustellen, ob Zyklen vorhanden sind oder nicht.
Ihr Beispiel zeigt nun eine Situation, in der alle Instanzkonstruktoren vom Typ 1 sind . In dieser Situation muss der Feldinitiierungscode nirgendwo platziert werden. Es wird also anscheinend nicht sehr tief analysiert.
Es stellt sich heraus, dass Sie, wenn alle Instanzkonstruktoren vom Typ 1 sind , sogar von einer Basisklasse ableiten können, für die kein zugänglicher Konstruktor vorhanden ist. Die Basisklasse darf jedoch nicht versiegelt sein. Wenn Sie beispielsweise eine Klasse nur mit
private
Instanzkonstruktoren schreiben , können Personen weiterhin von Ihrer Klasse abgeleitet werden, wenn alle Instanzkonstruktoren in der abgeleiteten Klasse vom Typ 1 oben sind. Ein neuer Objekterstellungsausdruck wird jedoch natürlich nie beendet. Um Instanzen der abgeleiteten Klasse zu erstellen, müsste man "schummeln" und Dinge wie dieSystem.Runtime.Serialization.FormatterServices.GetUninitializedObject
Methode verwenden.Ein weiteres Beispiel: Die
System.Globalization.TextInfo
Klasse hat nur eineninternal
Instanzkonstruktor. Sie können diese Klasse jedoch auch in einer anderen Assembly alsmscorlib.dll
mit dieser Technik ableiten .Schließlich in Bezug auf die
Syntax. Nach den C # -Regeln ist dies als zu lesen
weil der Linksverschiebungsoperator
<<
eine höhere Priorität hat als sowohl der Klein-als-Operator<
als auch der Größer-als-Operator>
. Die beiden letztgenannten Operaroren haben den gleichen Vorrang und werden daher nach der linksassoziativen Regel bewertet. Wenn die Typen wärenund wenn das
MySpecialType
eine(MySpecialType, int)
Überladung des einführtoperator <
, dann der Ausdruckwäre legal und sinnvoll.
Meiner Meinung nach wäre es besser, wenn der Compiler in diesem Szenario eine Warnung ausgeben würde. Zum Beispiel könnte es sagen
unreachable code detected
und auf die Zeilen- und Spaltennummer des Feldinitialisierers zeigen, die niemals in IL übersetzt wird.quelle
class Example { int field = 42; internal Example() { /* some code here */ field = 100; } }
Dann setzt die von ihr erzeugte IL die42
Zuweisung vor allem anderen in den Instanzkonstruktor, genau so, als hätten Sie geschrieben:class Example { int field; internal Example() { field = 42; /* some code here */ field = 100; } }
Ich denke, weil die Sprachspezifikation nur ausschließt, dass derselbe Konstruktor, der definiert wird, direkt aufgerufen wird.
Ab 10.11.1:
...
Dieser letzte Satz scheint nur auszuschließen, dass ein direkter Aufruf selbst einen Fehler bei der Kompilierungszeit erzeugt, z
ist illegal.
Ich gebe jedoch zu - ich kann keinen bestimmten Grund dafür erkennen. Natürlich sind solche Konstrukte auf IL-Ebene zulässig, da zur Laufzeit verschiedene Instanzkonstruktoren ausgewählt werden könnten, glaube ich - Sie könnten also eine Rekursion haben, vorausgesetzt, sie wird beendet.
Ich denke, der andere Grund, warum es nicht darauf hinweist oder warnt, ist, dass es nicht notwendig ist, diese Situation zu erkennen . Stellen Sie sich vor , durch Hunderte von unterschiedlichen Herstellern zu jagen, nur um zu sehen , ob ein Zyklus tut exist - wenn jede versuchte Nutzung wird schnell (wie wir wissen) sprengt zur Laufzeit, für einen ziemlich Rand Fall.
Bei der Codegenerierung für jeden Konstruktor werden
constructor-initializer
lediglich die Feldinitialisierer und der Hauptteil des Konstruktors berücksichtigt - es wird kein anderer Code berücksichtigt:Wenn
constructor-initializer
es sich um einen Instanzkonstruktor für die Klasse selbst handelt, werden die Feldinitialisierer nicht ausgegeben, sondern derconstructor-initializer
Aufruf und dann der Body.Wenn
constructor-initializer
es sich um einen Instanzkonstruktor für die direkte Basisklasse handelt, werden die Feldinitialisierer, dann derconstructor-initializer
Aufruf und dann der Body ausgegeben.In keinem Fall muss es woanders gesucht werden - es ist also nicht so, dass es "nicht in der Lage" ist, zu entscheiden, wo die Feldinitialisierer platziert werden sollen - es folgt lediglich einigen einfachen Regeln, die nur den aktuellen Konstruktor berücksichtigen.
quelle
int e = Invalid<Method>Name<<Indeeed();
. Ich sage, das ist ein Compiler-Fehler.int e = Invalid < Method > Name << Indeed();
bei binären Operatoren "kleiner als", "größer als" und "Linksverschiebung" interpretiert werden . Das ist syntaktisch in Ordnung, aber es wäre eine wirklich verrückte Überlastung der Operatoren, es mit starker Eingabe in Ordnung zu bringen.Invalid
etc, die sie gültig machen.) Der Fehler wird normalerweise bei der Codegenerierung erkannt, aber Sie haben einen Weg gefunden, Code zu schreiben, der niemals generiert wird. Sie haben eine hinterhältige Lücke im Compiler gefunden (eine Möglichkeit, Code zu schreiben, der niemals kompiliert wird), aber keine ernsthafte, da der fehlerhafte Code ohnehin nicht erreichbar ist.Dein Beispiel
funktioniert gut, in dem Sinne, dass Sie dieses Foo-Objekt ohne Probleme instanziieren können. Das Folgende entspricht jedoch eher dem Code, nach dem Sie fragen
Sowohl das als auch Ihr Code erzeugen einen Stapelüberlauf (!), Da die Rekursion niemals endet. Ihr Code wird also ignoriert, da er nie ausgeführt werden kann.
Mit anderen Worten, der Compiler kann nicht entscheiden, wo der fehlerhafte Code abgelegt werden soll, da er erkennen kann, dass die Rekursion niemals zu Ende geht. Ich denke, das liegt daran, dass es dort platziert werden muss, wo es nur einmal aufgerufen wird, aber die rekursive Natur der Konstruktoren macht dies unmöglich.
Rekursion im Sinne eines Konstruktors, der Instanzen von sich selbst im Körper des Konstruktors erstellt, ist für mich sinnvoll, da dies beispielsweise verwendet werden kann, um Bäume zu instanziieren, bei denen jeder Knoten auf andere Knoten zeigt. Eine Rekursion über die Vorkonstruktoren der in dieser Frage dargestellten Art kann jedoch niemals einen Tiefpunkt erreichen. Daher wäre es für mich sinnvoll, wenn dies nicht zulässig wäre.
quelle
ambiguous method call
ausgegeben und ein solcher Methodenaufruf nicht übersprungen. Wenn ich ein Compiler wäre, würde ich auch in diesem Szenario einen Fehler auslösen.Ich denke, das ist erlaubt, weil man die Ausnahme noch fangen kann (könnte) und etwas Sinnvolles damit machen kann.
Die Initialisierung wird niemals ausgeführt und löst mit ziemlicher Sicherheit eine StackOverflowException aus. Dies kann jedoch immer noch erwünscht sein und bedeutete nicht immer, dass der Prozess abstürzen sollte.
Wie hier erklärt https://stackoverflow.com/a/1599236/869482
quelle