C# Language
Konstruktory i finalizatory
Szukaj…
Wprowadzenie
Konstruktory to metody klasy, które są wywoływane podczas tworzenia instancji tej klasy. Ich głównym obowiązkiem jest pozostawienie nowego obiektu w przydatnym i spójnym stanie.
Destructors / Finalizers to metody w klasie, które są wywoływane po zniszczeniu instancji. W języku C # rzadko są jawnie pisane / używane.
Uwagi
C # tak naprawdę nie ma destruktorów, a raczej finalizatory, które używają składni destruktorów w stylu C ++. Określenie destruktora przesłania Object.Finalize()
, której nie można wywołać bezpośrednio.
W przeciwieństwie do innych języków o podobnej składni, metody te nie są wywoływane, gdy obiekty wykraczają poza zakres, ale są wywoływane, gdy Garbage Collector działa, co występuje w określonych warunkach . W związku z tym nie gwarantuje się, że będą działać w określonej kolejności.
Finalizatory powinien być odpowiedzialny za sprzątanie niezarządzanych zasobów tylko (wskaźniki nabytych za pośrednictwem klasy marszałka, otrzymana przez P / Invoke (wywołania systemowe) lub surowe wskaźniki stosowane w niebezpiecznych bloków). Aby wyczyścić zasoby zarządzane, przejrzyj IDisposable, wzorzec Dispose i instrukcję using
.
(Dalsza lektura: Kiedy powinienem stworzyć destruktor? )
Domyślny konstruktor
Gdy typ jest zdefiniowany bez konstruktora:
public class Animal
{
}
następnie kompilator generuje domyślny konstruktor równoważny do następującego:
public class Animal
{
public Animal() {}
}
Definicja dowolnego konstruktora dla tego typu spowoduje wyłączenie domyślnego generowania konstruktora. Jeśli typ zdefiniowano w następujący sposób:
public class Animal
{
public Animal(string name) {}
}
wtedy Animal
można utworzyć tylko przez wywołanie deklarowanego konstruktora.
// This is valid
var myAnimal = new Animal("Fluffy");
// This fails to compile
var unnamedAnimal = new Animal();
W drugim przykładzie kompilator wyświetli komunikat o błędzie:
„Animal” nie zawiera konstruktora, który przyjmuje 0 argumentów
Jeśli chcesz, aby klasa miała konstruktor bez parametrów i konstruktor, który pobiera parametr, możesz to zrobić, jawnie implementując oba konstruktory.
public class Animal
{
public Animal() {} //Equivalent to a default constructor.
public Animal(string name) {}
}
Kompilator nie będzie w stanie wygenerować domyślnego konstruktora, jeśli klasa rozszerzy inną klasę, która nie ma konstruktora bez parametrów. Na przykład, gdybyśmy mieli klasowe Creature
:
public class Creature
{
public Creature(Genus genus) {}
}
następnie Animal
zdefiniowany jako class Animal : Creature {}
nie skompiluje się.
Wywoływanie konstruktora z innego konstruktora
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.
Konstruktor statyczny
Konstruktor statyczny jest wywoływany przy pierwszym inicjowaniu dowolnego elementu typu, wywoływany jest element klasy statycznej lub metoda statyczna. Konstruktor statyczny jest bezpieczny dla wątków. Konstruktor statyczny jest powszechnie używany do:
- Zainicjuj stan statyczny, czyli stan, który jest współużytkowany przez różne instancje tej samej klasy.
- Utwórz singletona
Przykład:
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();
Wynik:
Zainicjowano zwierzę
Stworzono zwierzę
Stworzono zwierzę
Jeśli pierwsze wywołanie dotyczy metody statycznej, konstruktor statyczny jest wywoływany bez konstruktora instancji. Jest to OK, ponieważ metoda statyczna i tak nie może uzyskać dostępu do stanu instancji.
Animal.Yawn();
Spowoduje to wygenerowanie:
Zainicjowano zwierzę
Ziewać!
Zobacz także Wyjątki w konstruktorach statycznych i ogólnych konstruktorach statycznych .
Przykład singletonu:
public class SessionManager
{
public static SessionManager Instance;
static SessionManager()
{
Instance = new SessionManager();
}
}
Wywoływanie konstruktora klasy podstawowej
Konstruktor klasy bazowej jest wywoływany przed wykonaniem konstruktora klasy pochodnej. Na przykład, jeśli Mammal
rozszerza Animal
, wówczas kod zawarty w konstruktorze Animal
jest wywoływany pierwszy podczas tworzenia instancji Mammal
.
Jeśli klasa pochodna nie określa jawnie, który konstruktor klasy podstawowej powinien zostać wywołany, kompilator zakłada konstruktora bez parametrów.
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.");
}
}
W takim przypadku wydrukowane zostanie wystąpienie Mammal
poprzez wywołanie new Mammal("George the Cat")
Narodziny nieznanego zwierzęcia.
George the Cat jest ssakiem.
Wywołanie innego konstruktora klasy podstawowej odbywa się poprzez umieszczenie : base(args)
między podpisem konstruktora a jego ciałem:
public class Mammal : Animal
{
public Mammal(string name) : base(name)
{
Console.WriteLine(name + " is a mammal.");
}
}
Wywołanie new Mammal("George the Cat")
wyświetli teraz:
George the Cat się rodzi.
George the Cat jest ssakiem.
Finalizatory klas pochodnych
Po sfinalizowaniu wykresu obiektowego kolejność jest odwrotnością konstrukcji. Np. Supertyp jest finalizowany przed typem podstawowym, jak pokazuje następujący kod:
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!
Wzorzec konstruktora Singleton
public class SingletonClass
{
public static SingletonClass Instance { get; } = new SingletonClass();
private SingletonClass()
{
// Put custom constructor code here
}
}
Ponieważ konstruktor jest prywatny, nie można tworzyć nowych instancji SingletonClass
poprzez wykorzystanie kodu. Jedynym sposobem na uzyskanie dostępu do pojedynczego wystąpienia SingletonClass
jest użycie właściwości statycznej SingletonClass.Instance
.
Właściwość Instance
jest przypisywana przez konstruktora statycznego generowanego przez kompilator C #. Środowisko wykonawcze .NET gwarantuje, że konstruktor statyczny jest uruchamiany maksymalnie raz i jest uruchamiany przed pierwszym odczytem Instance
. Dlatego wszystkie problemy z synchronizacją i inicjalizacją są wykonywane przez środowisko wykonawcze.
Zauważ, że jeśli konstruktor statyczny zawiedzie, klasa Singleton
się trwale niezdatna do użytku przez okres użytkowania AppDomain.
Ponadto nie można zagwarantować, że konstruktor statyczny zostanie uruchomiony w momencie pierwszego dostępu do Instance
. Zamiast tego będzie działał w pewnym momencie wcześniej . To sprawia, że czas inicjalizacji nie jest deterministyczny. W praktycznych przypadkach JIT często wywołuje konstruktora statycznego podczas kompilacji (nie wykonywania) metody odwołującej się do Instance
. Jest to optymalizacja wydajności.
Zobacz stronę Implementacje singletonu, aby zapoznać się z innymi sposobami implementacji wzorca singletonu.
Wymuszanie wywołania konstruktora statycznego
Podczas gdy statyczne konstruktory są zawsze wywoływane przed pierwszym użyciem typu, czasem przydatne może być wymuszenie ich wywołania, a klasa RuntimeHelpers
zapewnia pomocnika:
using System.Runtime.CompilerServices;
// ...
RuntimeHelpers.RunClassConstructor(typeof(Foo).TypeHandle);
Uwaga : Wszystkie statyczne inicjalizacje (na przykład inicjalizatory pól) zostaną uruchomione, nie tylko sam konstruktor.
Potencjalne zastosowania : wymuszanie inicjalizacji podczas ekranu powitalnego w aplikacji interfejsu użytkownika lub zapewnienie, że konstruktor statyczny nie zawiedzie w teście jednostkowym.
Wywoływanie metod wirtualnych w konstruktorze
W przeciwieństwie do C ++ w C #, możesz wywołać metodę wirtualną z konstruktora klasy (OK, możesz także w C ++, ale zachowanie na początku jest zaskakujące). Na przykład:
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
Jeśli pochodzisz z języka C ++, jest to zaskakujące, konstruktor klasy podstawowej już widzi tabelę metod wirtualnej klasy pochodnej!
Uważaj : klasa pochodna może jeszcze nie zostać w pełni zainicjowana (jej konstruktor zostanie wykonany po konstruktorze klasy podstawowej), a ta technika jest niebezpieczna (jest też ostrzeżenie StyleCop). Zwykle jest to uważane za złą praktykę.
Ogólne statyczne konstruktory
Jeśli typ, na którym deklarowany jest konstruktor statyczny, jest ogólny, konstruktor statyczny zostanie wywołany raz dla każdej unikalnej kombinacji ogólnych argumentów.
class Animal<T>
{
static Animal()
{
Console.WriteLine(typeof(T).FullName);
}
public static void Yawn() { }
}
Animal<Object>.Yawn();
Animal<String>.Yawn();
Spowoduje to wygenerowanie:
System.Object
System.String
Zobacz także Jak działają konstruktory statyczne dla typów ogólnych?
Wyjątki w konstruktorach statycznych
Jeśli konstruktor statyczny zgłosi wyjątek, nigdy nie jest ponawiany. Tego typu nie można używać przez cały okres istnienia AppDomain. Każde dalsze użycie tego typu spowoduje TypeInitializationException
wokół oryginalnego wyjątku.
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());
}
Spowoduje to wygenerowanie:
Statyczny ctor
System.TypeInitializationException: Inicjator typu dla „Zwierząt” zgłosił wyjątek. ---> System.Exception: Zgłoszono wyjątek typu „System.Exception”.
[...]
System.TypeInitializationException: Inicjator typu dla „Zwierząt” zgłosił wyjątek. ---> System.Exception: Zgłoszono wyjątek typu „System.Exception”.
gdzie widać, że rzeczywisty konstruktor jest wykonywany tylko raz, a wyjątek jest ponownie wykorzystywany.
Inicjalizacja konstruktora i własności
Czy przypisanie wartości właściwości powinno być wykonane przed czy po konstruktorze klasy?
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 };
Czy w powyższym przykładzie wartość TestProperty
wynosi 1
w konstruktorze klasy, czy po konstruktorze klasy?
Przypisywanie wartości właściwości do tworzenia instancji w następujący sposób:
var testInstance = new TestClass() {TestProperty = 1};
Zostanie wykonany po uruchomieniu konstruktora. Jednak inicjowanie wartości właściwości we właściwości klasy w C # 6.0 w następujący sposób:
public class TestClass
{
public int TestProperty { get; set; } = 2;
public TestClass()
{
}
}
zostanie wykonane przed uruchomieniem konstruktora.
Łącząc dwie powyższe koncepcje w jednym przykładzie:
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
}
Ostateczny wynik:
"Or shall this be executed"
"1"
Wyjaśnienie:
Wartość TestProperty
zostanie najpierw przypisana jako 2
, a następnie zostanie uruchomiony konstruktor TestClass
, co spowoduje wydrukowanie
"Or shall this be executed"
A następnie TestProperty
zostanie przypisany jako 1
ze względu na new TestClass() { TestProperty = 1 }
, dzięki czemu końcowa wartość TestProperty
wydrukowana przez Console.WriteLine(testInstance.TestProperty)
"1"