Konstruktoren und Finalizer

Konstruktoren und Finalizer

Konstruktoren sind Methoden in einer Klasse, die aufgerufen werden, wenn eine Instanz dieser Klasse erstellt wird. Ihre Hauptaufgabe besteht darin, das neue Objekt in einem nützlichen und konsistenten Zustand zu belassen.

Destruktoren/Finalizer sind Methoden in einer Klasse, die aufgerufen werden, wenn eine Instanz davon zerstört wird. In C# werden sie selten explizit geschrieben/verwendet.

# Statischer Konstruktor

Ein statischer Konstruktor wird das erste Mal aufgerufen, wenn irgendein Mitglied eines Typs initialisiert wird, ein statisches Klassenmitglied oder eine statische Methode aufgerufen wird. Der statische Konstruktor ist Thread-sicher. Ein statischer Konstruktor wird üblicherweise für Folgendes verwendet:

  • Statischen Zustand initialisieren, d. h. Zustand, der von verschiedenen Instanzen derselben Klasse gemeinsam genutzt wird.
  • Singleton erstellen

Beispiel:

class Animal
{
    // * A static constructor is executed only once,
    //   when a class is first accessed.
    // * A static constructor cannot have any access modifiers
    // * A static constructor cannot have any parameters
    static Animal()
    {
        Console.WriteLine("Animal initialized");
    }

    // Instance constructor, this is executed every time the class is created
    public Animal()
    {
        Console.WriteLine("Animal created");
    }

    public static void Yawn()
    {
        Console.WriteLine("Yawn!");
    }
}

var turtle = new Animal();
var giraffe = new Animal();

Ausgabe:

Tier initialisiert
Tier geschaffen
Tier erstellt

Demo ansehen

Wenn der erste Aufruf eine statische Methode ist, wird der statische Konstruktor ohne den Instanzkonstruktor aufgerufen. Das ist in Ordnung, weil die statische Methode ohnehin nicht auf den Zustand der Instanz zugreifen kann.

Animal.Yawn();

Dies wird ausgeben:

Tier initialisiert
Gähn!

Siehe auch Ausnahmen in statischen Konstruktoren und generische statische Konstruktoren .

Singleton-Beispiel:

public class SessionManager
{
    public static SessionManager Instance;

    static SessionManager()
    {
        Instance = new SessionManager();
    }
}

# Singleton-Konstruktormuster

public class SingletonClass
{
    public static SingletonClass Instance { get; } = new SingletonClass();

    private SingletonClass()
    {
        // Put custom constructor code here
    }    
}

Da der Konstruktor privat ist, keine neuen Instanzen von SingletonClass kann durch Konsumieren von Code erstellt werden. Die einzige Möglichkeit, auf die einzelne Instanz von SingletonClass zuzugreifen erfolgt durch die Verwendung der statischen Eigenschaft SingletonClass.Instance .

Die Instance -Eigenschaft wird von einem statischen Konstruktor zugewiesen, den der C#-Compiler generiert. Die .NET-Laufzeit garantiert, dass der statische Konstruktor höchstens einmal und vor Instance ausgeführt wird wird zuerst gelesen. Daher werden alle Synchronisierungs- und Initialisierungsbelange von der Laufzeitumgebung ausgeführt.

Beachten Sie, dass Singleton, wenn der statische Konstruktor fehlschlägt Klasse wird für die Lebensdauer der AppDomain dauerhaft unbrauchbar.

Außerdem wird nicht garantiert, dass der statische Konstruktor zum Zeitpunkt des ersten Zugriffs von Instance ausgeführt wird . Vielmehr wird es irgendwann davor ausgeführt . Dadurch wird der Zeitpunkt, zu dem die Initialisierung erfolgt, nicht deterministisch. In praktischen Fällen ruft das JIT den statischen Konstruktor oft während der Kompilierung auf (nicht Ausführung) einer Methode, die auf Instance verweist . Dies ist eine Leistungsoptimierung.

Weitere Möglichkeiten zur Implementierung des Singleton-Musters finden Sie auf der Seite Singleton-Implementierungen.

# Standardkonstruktor

Wenn ein Typ ohne Konstruktor definiert wird:

public class Animal
{
}

dann generiert der Compiler einen Standardkonstruktor, der dem Folgenden entspricht:

public class Animal
{
    public Animal() {}
}

Die Definition eines beliebigen Konstruktors für den Typ unterdrückt die Standardkonstruktorgenerierung. Wenn der Typ wie folgt definiert wäre:

public class Animal
{
    public Animal(string name) {}
}

dann ein Animal konnte nur durch Aufrufen des deklarierten Konstruktors erstellt werden.

// This is valid
var myAnimal = new Animal("Fluffy");
// This fails to compile
var unnamedAnimal = new Animal();

Für das zweite Beispiel zeigt der Compiler eine Fehlermeldung an:

'Animal' enthält keinen Konstruktor, der 0 Argumente akzeptiert

Wenn Sie möchten, dass eine Klasse sowohl einen parameterlosen Konstruktor als auch einen Konstruktor hat, der einen Parameter akzeptiert, können Sie dies tun, indem Sie beide Konstruktoren explizit implementieren.

public class Animal
{
    
    public Animal() {} //Equivalent to a default constructor.
    public Animal(string name) {}
}

Der Compiler kann keinen Standardkonstruktor generieren, wenn die Klasse eine andere Klasse erweitert, die keinen parameterlosen Konstruktor hat. Zum Beispiel, wenn wir eine Klasse Creature hätten :

public class Creature
{
    public Creature(Genus genus) {}
}

dann Animal definiert als class Animal : Creature {} würde nicht kompilieren.

# Den Aufruf eines statischen Konstruktors erzwingen

Während statische Konstruktoren immer vor der ersten Verwendung eines Typs aufgerufen werden, ist es manchmal nützlich, ihren Aufruf und den RuntimeHelpers zu erzwingen Klasse stellt einen Helfer dafür zur Verfügung:

using System.Runtime.CompilerServices;    
// ...
RuntimeHelpers.RunClassConstructor(typeof(Foo).TypeHandle);

Bemerkung : Alle statischen Initialisierungen (z. B. Feldinitialisierer) werden ausgeführt, nicht nur der Konstruktor selbst.

Mögliche Verwendungszwecke : Erzwingen der Initialisierung während des Begrüßungsbildschirms in einer UI-Anwendung oder Sicherstellen, dass ein statischer Konstruktor in einem Komponententest nicht fehlschlägt.

# Aufruf eines Konstruktors von einem anderen Konstruktor

public class Animal
{
    public string Name { get; set; }

    public Animal() : this("Dog")
    {
    }

    public Animal(string name)
    {
        Name = name;
    }
}

var dog = new Animal();      // dog.Name will be set to "Dog" by default.
var cat = new Animal("Cat"); // cat.Name is "Cat", the empty constructor is not called.

# Aufruf des Basisklassenkonstruktors

Ein Konstruktor einer Basisklasse wird aufgerufen, bevor ein Konstruktor einer abgeleiteten Klasse ausgeführt wird. Wenn beispielsweise Mammal erweitert Animal , dann der im Konstruktor von Animal enthaltene Code wird zuerst aufgerufen, wenn eine Instanz von Mammal erstellt wird .

Wenn eine abgeleitete Klasse nicht explizit angibt, welcher Konstruktor der Basisklasse aufgerufen werden soll, nimmt der Compiler den parameterlosen Konstruktor an.

public class Animal
{
    public Animal() { Console.WriteLine("An unknown animal gets born."); }
    public Animal(string name) { Console.WriteLine(name + " gets born"); }
}

public class Mammal : Animal
{
    public Mammal(string name)
    {
        Console.WriteLine(name + " is a mammal.");
    }
}

In diesem Fall wird ein Mammal instanziiert durch Aufruf von new Mammal("George the Cat") wird gedruckt

Ein unbekanntes Tier wird geboren.
George the Cat ist ein Säugetier.

Demo ansehen

Der Aufruf eines anderen Konstruktors der Basisklasse erfolgt durch Platzieren von : base(args) zwischen der Signatur des Konstruktors und seinem Körper:

public class Mammal : Animal
{
    public Mammal(string name) : base(name)
    {
        Console.WriteLine(name + " is a mammal.");
    }
}

Rufen Sie new Mammal("George the Cat") an wird jetzt drucken:

George die Katze wird geboren.
George the Cat ist ein Säugetier.

Demo ansehen

# Finalizers für abgeleitete Klassen

Wenn ein Objektgraph fertiggestellt ist, ist die Reihenfolge die Umkehrung der Konstruktion. Z.B. Der Supertyp wird vor dem Basistyp finalisiert, wie der folgende Code zeigt:

class TheBaseClass
{
    ~TheBaseClass() 
    {
        Console.WriteLine("Base class finalized!");
    }
}

class TheDerivedClass : TheBaseClass
{
    ~TheDerivedClass() 
    {
        Console.WriteLine("Derived class finalized!");
    }
}

//Don't assign to a variable
//to make the object unreachable
new TheDerivedClass();

//Just to make the example work;
//this is otherwise NOT recommended!
GC.Collect();

//Derived class finalized!
//Base class finalized!

# Ausnahmen in statischen Konstruktoren

Wenn ein statischer Konstruktor eine Ausnahme auslöst, wird dies nie wiederholt. Der Typ ist für die Lebensdauer der AppDomain unbrauchbar. Jede weitere Verwendung dieses Typs löst einen TypeInitializationException aus um die ursprüngliche Ausnahme gewickelt.

public class Animal
{
    static Animal()
    {
        Console.WriteLine("Static ctor");
        throw new Exception();
    }

    public static void Yawn() {}
}

try
{
    Animal.Yawn();
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

try
{
    Animal.Yawn();
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}

Dies wird ausgeben:

Statischer ctor

System.TypeInitializationException:Der Typinitialisierer für „Animal“ hat eine Ausnahme ausgelöst. ---> System.Exception:Ausnahme vom Typ 'System.Exception' wurde geworfen.

[...]

System.TypeInitializationException:Der Typinitialisierer für „Animal“ hat eine Ausnahme ausgelöst. ---> System.Exception:Ausnahme vom Typ 'System.Exception' wurde ausgelöst.

wo Sie sehen können, dass der eigentliche Konstruktor nur einmal ausgeführt wird und die Ausnahme wiederverwendet wird.

# Aufruf virtueller Methoden im Konstruktor

Im Gegensatz zu C++ können Sie in C# eine virtuelle Methode vom Klassenkonstruktor aufrufen (OK, das können Sie auch in C++, aber das Verhalten ist zunächst überraschend). Zum Beispiel:

abstract class Base
{
    protected Base()
    {
        _obj = CreateAnother();
    }

    protected virtual AnotherBase CreateAnother()
    {
        return new AnotherBase();
    }

    private readonly AnotherBase _obj;
}

sealed class Derived : Base
{
    public Derived() { }

    protected override AnotherBase CreateAnother()
    {
        return new AnotherDerived();
    }
}

var test = new Derived();
// test._obj is AnotherDerived

Wenn Sie von einem C++-Hintergrund kommen, ist dies überraschend, der Basisklassenkonstruktor sieht bereits die virtuelle Methodentabelle der abgeleiteten Klasse!

Seien Sie vorsichtig :Die abgeleitete Klasse ist möglicherweise noch nicht vollständig initialisiert (ihr Konstruktor wird nach dem Konstruktor der Basisklasse ausgeführt) und diese Technik ist gefährlich (dafür gibt es auch eine StyleCop-Warnung). Normalerweise wird dies als schlechte Praxis angesehen.

# Generische statische Konstruktoren

Wenn der Typ, für den der statische Konstruktor deklariert ist, generisch ist, wird der statische Konstruktor einmal für jede eindeutige Kombination generischer Argumente aufgerufen.

class Animal<T>
{
    static Animal()
    {
        Console.WriteLine(typeof(T).FullName);
    }

    public static void Yawn() { }
}

Animal<Object>.Yawn();
Animal<String>.Yawn();

Dies wird ausgeben:

System.Object
System.String

Siehe auch Wie funktionieren statische Konstruktoren für generische Typen?

# Konstruktor- und Property-Initialisierung

Soll die Zuweisung des Eigenschaftswertes vorher ausgeführt werden oder nach der Konstruktor der Klasse?

public class TestClass 
{
    public int TestProperty { get; set; } = 2;
    
    public TestClass() 
    {
        if (TestProperty == 1) 
        {
            Console.WriteLine("Shall this be executed?");
        }

        if (TestProperty == 2) 
        {
            Console.WriteLine("Or shall this be executed");
        }
    }
}

var testInstance = new TestClass() { TestProperty = 1 };

Im Beispiel oben soll das TestProperty sein Wert ist 1 im Konstruktor der Klasse oder nach dem Konstruktor der Klasse?

Eigenschaftswerte bei der Instanzerstellung wie folgt zuweisen:

var testInstance = new TestClass() {TestProperty = 1};

Wird nach ausgeführt Der Konstruktor wird ausgeführt. Initialisieren Sie jedoch den Eigenschaftswert in der Eigenschaft der Klasse in C# 6.0 wie folgt:

public class TestClass 
{
    public int TestProperty { get; set; } = 2;

    public TestClass() 
    {
    }
}

wird vorher erledigt der Konstruktor wird ausgeführt.

Kombinieren der beiden obigen Konzepte in einem einzigen Beispiel:

public class TestClass 
{
    public int TestProperty { get; set; } = 2;
    
    public TestClass() 
    {
        if (TestProperty == 1) 
        {
            Console.WriteLine("Shall this be executed?");
        }

        if (TestProperty == 2) 
        {
            Console.WriteLine("Or shall this be executed");
        }
    }
}

static void Main(string[] args) 
{
    var testInstance = new TestClass() { TestProperty = 1 };
    Console.WriteLine(testInstance.TestProperty); //resulting in 1
}

Endergebnis:

"Or shall this be executed"
"1"

Erklärung:

Der TestProperty Der Wert wird zuerst als 2 zugewiesen , dann TestClass Der Konstruktor wird ausgeführt, was zur Ausgabe von

führt
"Or shall this be executed"

Und dann die TestProperty wird als 1 zugewiesen wegen new TestClass() { TestProperty = 1 } , wodurch der endgültige Wert für TestProperty entsteht gedruckt von Console.WriteLine(testInstance.TestProperty) sein

"1"

# Bemerkungen

C# hat eigentlich keine Destruktoren, sondern Finalizer, die Destruktorsyntax im C++-Stil verwenden. Die Angabe eines Destruktors überschreibt den Object.Finalize() Methode, die nicht direkt aufgerufen werden kann.

Im Gegensatz zu anderen Sprachen mit ähnlicher Syntax sind diese Methoden nicht Wird aufgerufen, wenn Objekte den Gültigkeitsbereich verlassen, wird aber aufgerufen, wenn der Garbage Collector ausgeführt wird, was unter bestimmten Bedingungen auftritt. Als solche sind sie nicht garantiert in einer bestimmten Reihenfolge auszuführen.

Finalisierer sollten nur für die Bereinigung nicht verwalteter Ressourcen verantwortlich sein (Zeiger, die über die Marshal-Klasse erworben, über p/Invoke (Systemaufrufe) oder Rohzeiger, die in unsicheren Blöcken verwendet werden, empfangen wurden). Um verwaltete Ressourcen zu bereinigen, lesen Sie bitte IDisposable, das Dispose-Muster und using Aussage.

(Weiterführende Literatur:Wann sollte ich einen Destruktor erstellen? )