Virtual member call in C#: Wie Sie den Konstruktor nicht kaputt machen und den Code beschleunigen

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Virtual member call in a constructor: Wenn der C#-Compiler warnt, und das zu Recht



Jeder C#-Entwickler, der jemals Klassen mit Vererbung geschrieben hat, hat diese Warnung von ReSharper oder Visual Studio sicherlich gesehen: «Virtual member call in a constructor». Sie sieht harmlos aus, ist aber in Wirklichkeit eine rote Flagge, die zu schwer fassbaren Fehlern und Leistungseinbußen führen kann. Heute analysieren wir, was hinter dieser Warnung steckt, wie sie mit der Leistung zusammenhängt und warum selbst erfahrene Programmierer diesen Fehler manchmal machen.



Das Thema ist nicht aus der Luft gegriffen: Die Frage «Virtual member call in a constructor» auf Stack Overflow hat Tausende von Aufrufen und Dutzende von Antworten erhalten. Das bedeutet, dass das Problem real ist und viele betrifft. Aber wir gehen weiter: Wir erklären nicht nur, wie man es behebt, sondern zeigen, wie man den Code optimiert, indem man unnötige Allokationen und unerwartetes Verhalten vermeidet.



Was ist ein «Virtual member call in a constructor»?



Stellen Sie sich folgende Situation vor: Sie erstellen eine Basisklasse, in deren Konstruktor eine virtuelle Methode aufgerufen wird. Was könnte da schon schiefgehen? Schließlich sind virtuelle Methoden genau dafür da, überschrieben zu werden. Das Problem ist jedoch, dass beim Aufruf einer virtuellen Methode aus dem Konstruktor der Basisklasse das Objekt der abgeleiteten Klasse noch nicht vollständig initialisiert ist. Dies kann dazu führen, dass die überschriebene Methode auf einem «rohen» Objekt aufgerufen wird, bei dem Felder noch nicht gesetzt und Abhängigkeiten nicht injiziert sind.



public class BaseClass{    public BaseClass()    {        // Achtung! Virtueller Aufruf im Konstruktor        VirtualMethod();    }

public virtual void VirtualMethod() { Console.WriteLine("BaseClass.VirtualMethod"); }}

public class DerivedClass : BaseClass{ private readonly string _data;

public DerivedClass(string data) { _data = data; }

public override void VirtualMethod() { // _data ist noch nicht initialisiert! Console.WriteLine($"DerivedClass: {_data?.Length ?? 0}"); }}

// Verwendungvar obj = new DerivedClass("Hello"); // Gibt aus: DerivedClass: 0, nicht 5


In diesem Beispiel ist _data zum Zeitpunkt des Aufrufs von VirtualMethod null, da der Konstruktor der abgeleiteten Klasse noch nicht ausgeführt wurde. Dies ist ein klassischer Fall, in dem ein virtueller Aufruf im Konstruktor zu unerwartetem Verhalten führt.



Warum ist das ein Leistungsproblem?



Auf den ersten Blick scheint es sich nur um einen logischen Fehler zu handeln. Aber gehen wir tiefer. Wenn Sie eine virtuelle Methode aus dem Konstruktor aufrufen, ist die Laufzeitumgebung (CLR) gezwungen, zusätzliche Prüfungen und Dispatching durchzuführen. In einer normalen Situation ist die virtuelle Methodentabelle (vtable) bereits bereit, und der Aufruf erfolgt schnell. Im Konstruktor der Basisklasse kann die vtable für den abgeleiteten Typ jedoch noch nicht vollständig aufgebaut sein. Dies führt zu:



  • Zusätzlichem Overhead für das Dispatching – die CLR verbringt Zeit mit der Überprüfung der Korrektheit des Aufrufs.
  • Potentiell nicht optimalem JIT-kompilierten Code – der JIT-Compiler kann einen solchen Aufruf möglicherweise nicht inlinen, was die Leistung verringert.
  • Zusätzlichen Null-Prüfungen – wenn Sie die Methode überschreiben, müssen Sie Schutzcode schreiben, was die IL-Größe und die Ausführungszeit erhöht.


In hochbelasteten Systemen, in denen Konstruktoren tausende Male pro Sekunde aufgerufen werden, können sich diese Mikroverzögerungen summieren. Daher sollte die Optimierung von C#-Code mit solchen scheinbaren Kleinigkeiten beginnen.



Wann kann ein virtueller Aufruf im Konstruktor nützlich sein?



Spoiler: Fast nie. Es gibt jedoch seltene Ausnahmen, wenn Sie genau wissen, was Sie tun. Zum Beispiel im Template Method-Pattern, bei dem die Basisklasse das Skelett eines Algorithmus vorgibt und die abgeleiteten Klassen die Details implementieren. In diesem Fall müssen Sie jedoch äußerst vorsichtig sein und sicherstellen, dass der Zustand des Objekts zum Zeitpunkt des Aufrufs konsistent ist.

Blogs

Buchempfehlungen