Ein Anfänger-Thema? Irgendwie schon. Dennoch kommt die Frage immer wieder, in welcher Reihenfolge was aufgerufen wird. Ich kenne ehrlich gesagt niemanden, der aus dem Stegreif die richtigen Abarbeitung nennen kenn, obwohl sie an und für sich ganz einfach ist. Ich selbst muss bei diesem Thema auch immer wieder überlegen. Oder kann jemand sofort sagen, in welcher Reihenfolge die Konstruktoren einer Objekt-Hierarchie aufgerufen werden und wann dabei die statischen Konstruktoren abgearbeitet werden?

Machen wir uns mal ein kleines Beispiel und schauen mal, was passiert:

class BaseClass
{
	public BaseClass()
	{
		Trace.WriteLine("constructor base");
	}
}

class ChildClass : BaseClass
{
	public ChildClass()
		: base()
	{
		Trace.WriteLine("constructor child");
	}
}

Wir instanzieren zu erst einfach mal die ChildClass.

ChildClass child1 = new ChildClass();

Aus der Trace-Ausgabe sehen wir dann die Reihenfolge, wie die Konstruktoren abgearbeitet werden. Das ganze ist ganz einleuchtend und man kann sich das auch ganz leicht merken, wie ich finde. Einfach immer dran denken, dass immer zu erst die BaseClass instanziert wird, weil diese für die ChildClass benötigt wird. Dabei spielt es keine Rolle, ob man beim Konstruktor der ChildClass das* base()* hinzuschreibt oder nicht. Den expliziten Aufruf von base() verwendet man in erster Linie dann, wenn man einen bestimmten Konstruktor der Basisklasse aufrufen will und/oder dem Konstruktor bestimmte Parameter mitgeben will.

Wenn wir schon Thema Parameter sind… Nehmen wir mal an, wir haben einen zweiten Konstruktor in der ChildClass mit einem Parameter. In der Definition geben wir an, dass er mit this() den Standard-Konstruktor aufrufen soll.

class ChildClass : BaseClass
{
	...
	public ChildClass(string variable) : this()
	{
		Trace.WriteLine("constructor child with parameter");
	}
}

In welcher Reihenfolge werden dann hier die beiden Konstruktoren aufgerufen? Richtig… Erst wird der Default-Konstruktor ohne Parameter aufgerufen und dann der zusätzliche mit dem einen Parameter. Der Grund ist, weil wir durch das this() angeben, dass der Default-Konstruktor benötigt wird. wenn man dieses this() weglässt, wird der Default-Konstruktor gar nicht aufgerufen. Lediglich der Default-Konstruktor der BaseClass wird natürlich automatisch davor abgearbeitet.

Nähern wir uns mal langsam einem Sonderfall. Ich nehm dabei aber aber schon mal vorweg, dass wir bei näherem Betrachten feststellen werden, dass es sich nicht wirklich um einen Sonderfall handelt sondern immer noch der Struktur folgt, dass erst das instanziert wird, was für das Ziel benötigt wird.

Also legen wir mal in der Basisklasse einen statischen Konstruktor an.

class BaseClass
{
	...
	static BaseClass()
	{
		Trace.WriteLine("constructor static base");
	}
}

Statische Konstruktoren werden aufgerufen, wenn die Klasse das erste mal verwendet wird. Ein Zugriffsmodifizierer wie public, private oder so ist hier nicht erlaubt, da dieser Konstruktor auf jeden Fall aufgerufen werden muss. Wenn wir uns das vor Augen halten, ist eigentlich auch schon klar, wann dieser Konstruktur abgearbeitet wird. Als erster, sobald auf die BaseClass zugeriffen wird. Also noch vor dem normalen Konstruktor.

Gehen wir noch einen Schritt weiter und legen noch einen statischen Konstruktor in der ChildClass an.

class ChildClass : BaseClass
{
        ...
	static ChildClass()
	{
		Trace.WriteLine("constructor static child");
	}
}

Jetzt wird es ein bisschen verzwickt. Es gilt das Gleiche oben. Also der statische Konstruktor wird aufgerufen, sobald das erste mal auf die Klasse zugegriffen wird. Das erste mal wird auf die Klasse zugegriffen, wenn ausgelesen wird, welche Basisklasse dafür benötigt wird. Das heißt, dass der statische Konstruktor der ChildClass noch vor dem statischen Konstruktor der BaseClass aufgerufen wird.

Damit haben wir eigentlich die Abarbeitungsfolge der Konstruktoren durch. Ein paar Worte möchte zu diesem Thema möchte ich dennoch loswerden. Ist es zum Beispiel sinnvoll einen Konstruktor in einer abstrakten Klasse anzulegen? Ja. Auch das kann man machen. An der Aufrufreihenfolge ändert sich dabei nichts. Sinnvoll ist das zum Beispiel, wenn man auch in der abstrakten Basisklasse Objekte hat, die man für die Verwendung initialisieren muss. Ich persönlich brauche das recht häufig. Von anderen hab ich schon gesehen, dass sie ziemlich überrascht waren, dass das überhaupt geht. Ich denke also, dass das Geschmackssache ist. Aber man sollte es zumindest wissen, dass das geht.

Das Gleiche gilt für statische Konstruktoren in statischen Klassen. Auch die können Sinn machen. Wenn man zum Beispiel eine statische Variable vor der ersten Verwendung mit irgendwas initialisieren will. Dieses Feature hab ich bisher ehrlich gesagt noch nie benötigt. Aber ich muss auch zugeben, dass ich davon auch erst vor Kurzem gehört habe.

So. Kommen wir zum nächsten Teil.

Neben Konstruktoren gibt es auch in C# Destruktoren. Hier ein Beispiel, wie so etwas aussieht.

~BaseClass()
{
	Trace.WriteLine("destructor base");
}

Destruktoren haben genauso wie statische Konstruktoren keine Zugrifssmodifizierer und werden auf jeden Fall immer aufgerufen, wenn das Objekt aufgeräumt wird. Schauen wir uns hier wieder die Reihenfolge an:

class BaseClass
{
	public BaseClass()
	{
		Trace.WriteLine("constructor base");
	}

	static BaseClass()
	{
		Trace.WriteLine("constructor static base");
	}

	~BaseClass()
	{
		Trace.WriteLine("destructor base");
	}
}

class ChildClass : BaseClass
{
	public ChildClass()
		: base()
	{
		Trace.WriteLine("constructor child");
	}

	static ChildClass()
	{
		Trace.WriteLine("constructor static child");
	}

	public ChildClass(string variable)
	{
		Trace.WriteLine("constructor child with parameter");
	}

	~ChildClass()
	{
		Trace.WriteLine("destructor child");
	}
}

Wir halten uns wieder vor Augen, dass immer erst die Methode verwendet wird, die von einer anderen vorausgesetzt wird. Man kann die Basisklasse nicht zerstören, wenn davon noch eine Childklasse abhängt. Daraus folgt, dass bei den Destruktoren erst der Destruktor der ChildClass abgearbeitet wird und dann der der BaseClass.

Fazit: Wie man sieht, ist das eigentlich alles ganz logisch aufgebaut. Zumindest wenn man es sich näher zu Gemüte führt. Wenn man ein bisschen damit rumgespielt hat. Wer sich da ein bisschen Gedanken machen will, dem empfehle ich, ein kleines Projekt zu bauen mit den oben genannten Beispielen und einfach in den Konstruktoren und Destruktoren die entsprechenden Trace-Meldungen auszugeben. Man sieht dann recht schnell, wie das mit den Reihenfolgen funktioniert.