Constructorii, inițializatoriiși deconstructorii
Crearea constructorilor
În articolul anterior, pentru crearea unui obiect a fost utilizat constructorul implicit. Cu toate acestea, putem să ne definim propriii constructori. De regulă, constructorul realizează inițializarea obiectului. Dacă în clasă sunt definiți constructori proprii, aceasta nu mai are constructorul implicit.
La nivel de cod, constructorul reprezintă o metodă care poartă numele clasei, poate avea parametri, dar nu trebuie să definească tipul de returnare. De exemplu, să definim un constructor simplu în clasa Person:
Person tom = new Person(); // Crearea obiectului clasei Person
tom.Print(); // Nume: Tom Vârstă: 37
class Person
{
public string name;
public int age;
public Person()
{
Console.WriteLine("Crearea obiectului Person");
name = "Tom";
age = 37;
}
public void Print()
{
Console.WriteLine($"Nume: {name} Vârstă: {age}");
}
}
Așadar, aici este definit un constructor care afișează un mesaj pe consolă și inițializează câmpurile clasei.
public Person()
{
Console.WriteLine("Crearea obiectului Person");
name = "Tom";
age = 37;
}
Constructorii pot avea modificatori care se specifică înaintea numelui constructorului. Astfel, în acest caz, pentru ca constructorul să fie accesibil în afara clasei Person, este definit cu modificatorul public.
Definind constructorul, îl putem apela pentru a crea obiectul Person:
Person tom = new Person(); // Crearea obiectului Person
În acest caz, expresia Person() reprezintă apelul constructorului definit în clasă (nu mai este constructorul implicit automat, pe care clasa nu-l mai are). Prin urmare, la executare, pe consolă va fi afișat mesajul "Crearea obiectului Person".
În mod similar, putem defini și alți constructori în clasă. De exemplu, să modificăm clasa Person astfel:
Person tom = new Person(); // apelul primului constructor fără parametri
Person bob = new Person("Bob"); // apelul celui de-al doilea constructor cu un parametru
Person sam = new Person("Sam", 25); // apelul celui de-al treilea constructor cu doi parametri
tom.Print(); // Nume: Necunoscut Vârstă: 18
bob.Print(); // Nume: Bob Vârstă: 18
sam.Print(); // Nume: Sam Vârstă: 25
class Person
{
public string name;
public int age;
public Person() { name = "Necunoscut"; age = 18; } // primul constructor
public Person(string n) { name = n; age = 18; } // al doilea constructor
public Person(string n, int a) { name = n; age = a; } // al treilea constructor
public void Print()
{
Console.WriteLine($"Nume: {name} Vârstă: {age}");
}
}
Acum, în clasă sunt definiți trei constructori, fiecare dintre ei acceptă un număr diferit de parametri și stabilește valorile câmpurilor clasei. Și putem apela unul dintre acești constructori pentru a crea obiectul clasei.
Afișarea pe consolă a acestei programe:
Nume: Necunoscut Vârstă: 18
Nume: Bob Vârstă: 18
Nume: Sam Vârstă: 25
Trebuie menționat că începând cu versiunea C# 9 putem scurta apelul constructorului, eliminând din el numele tipului:
Person tom = new (); // echivalent cu new Person();
Person bob = new ("Bob"); // echivalent cu new Person("Bob");
Person sam = new ("Sam", 25); // echivalent cu new Person("Sam", 25);
Cuvântul cheie this
Cuvântul cheie this reprezintă referința la instanța/obiectul curent al clasei. În ce situații ne poate fi util?
Person sam = new("Sam", 25);
sam.Print(); // Nume: Sam Vârstă: 25
class Person
{
public string name;
public int age;
public Person() { name = "Necunoscut"; age = 18; }
public Person(string name) { this.name = name; age = 18; }
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void Print() => Console.WriteLine($"Nume: {name} Vârstă: {age}");
}
În exemplul de mai sus, în al doilea și al treilea constructor, parametrii sunt numiți la fel ca și câmpurile clasei. Și pentru a diferenția parametrii de câmpurile clasei, se folosește cuvântul cheie this la câmpurile clasei. Astfel, în expresia:
this.name = name;
prima parte - this.name indică faptul că name este câmpul clasei curente, și nu numele parametrului name. Dacă parametrii și câmpurile noastre ar fi numite diferit, nu ar fi necesar să folosim cuvântul this. De asemenea, prin cuvântul cheie this putem accesa orice câmp sau metodă.
Lanțul apelurilor constructorilor
În exemplul de mai sus, sunt definiți trei constructori. Toți cei trei constructori realizează acțiuni similare - stabilesc valorile câmpurilor name și age. Dar aceste acțiuni repetitive ar putea fi mai multe. Și putem să nu duplicăm funcționalitatea constructorilor, ci să ne adresăm dintr-un constructor către altul tot prin cuvântul cheie this, transmițând valorile necesare pentru parametri:
class Person
{
public string name;
public int age;
public Person() : this("Necunoscut") // primul constructor
{ }
public Person(string name) : this(name, 18) // al doilea constructor
{ }
public Person(string name, int age) // al treilea constructor
{
this.name = name;
this.age = age;
}
public void Print() => Console.WriteLine($"Nume: {name} Vârstă: {age}");
}
În acest caz, primul constructor îl apelează pe al doilea, iar al doilea constructor îl apelează pe al treilea. În funcție de numărul și tipul parametrilor, compilatorul recunoaște ce constructor anume este apelat. De exemplu, în al doilea constructor:
public Person(string name) : this(name, 18)
{ }
se apelează al treilea constructor, căruia îi sunt transmise două valori. Mai întâi se va executa al treilea constructor și abia apoi codul celui de-al doilea constructor.
Trebuie menționat că în exemplul de mai sus, de fapt, toți constructorii nu definesc alte acțiuni decât transmiterea unor valori către al treilea constructor. Prin urmare, în realitate, în acest caz, este mai simplu să lăsăm un singur constructor, definind pentru parametrii săi valori implicite:
Person tom = new();
Person bob = new("Bob");
Person sam = new("Sam", 25);
tom.Print(); // Nume: Necunoscut Vârstă: 18
bob.Print(); // Nume: Bob Vârstă: 18
sam.Print(); // Nume: Sam Vârstă: 25
class Person
{
public string name;
public int age;
public Person(string name = "Necunoscut", int age = 18)
{
this.name = name;
this.age = age;
}
public void Print() => Console.WriteLine($"Nume: {name} Vârstă: {age}");
}
Și dacă la apelul constructorului nu transmitem valoare pentru un parametru, se aplică valoarea implicită.
Constructorii primari
Începând cu versiunea C# 12, limbajul C# a adăugat suport pentru constructorii primari (Primary constructors). Constructorii primari permit adăugarea de parametri la definiția clasei și utilizarea acestor parametri în cadrul clasei:
var tom = new Person("Tom", 38);
Console.WriteLine(tom);
public class Person(string name, int age)
{
public Person(string name) : this(name, 18) { }
public void Print() => Console.WriteLine($"name: {
name}, age: {age}");
}
Aici, pentru clasa Person este definit un constructor primar cu doi parametri - name și age. Acești parametri sunt utilizați în metoda Print.
În fundal, pentru fiecare parametru al constructorului primar se creează un câmp privat în clasă, care stochează valoarea parametrului. Astfel, ele pot fi utilizate în corpul clasei.
Pe lângă constructorii primari, clasa poate defini și constructori suplimentari, ca în exemplul de mai sus. Dar acești constructori suplimentari trebuie să apeleze constructorul primar:
public Person(string name) : this(name, 18) { }
Inițializatorii obiectelor
Pentru inițializarea obiectelor claselor, putem folosi inițializatorii. Inițializatorii reprezintă transmiterea în acolade a valorilor către câmpurile și proprietățile disponibile ale obiectului:
Person tom = new Person { name = "Tom", age = 31 };
// sau așa
// Person tom = new() { name = "Tom", age = 31 };
tom.Print(); // Nume: Tom Vârstă: 31
Cu ajutorul inițializatorului obiectelor, putem atribui valori tuturor câmpurilor și proprietăților disponibile ale obiectului în momentul creării acestuia. La utilizarea inițializatorilor, trebuie să ținem cont de următoarele aspecte:
- Cu ajutorul inițializatorului putem stabili valori doar pentru câmpurile și proprietățile obiectului accesibile din afara clasei. De exemplu, în exemplul de mai sus, câmpurile name și age au modificatorul de acces public, astfel sunt accesibile din orice parte a programului
- Inițializatorul se execută după constructor, deci dacă atât în constructor, cât și în inițializator sunt stabilite valori pentru aceleași câmpuri și proprietăți, valorile stabilite în constructor sunt înlocuite cu valorile din inițializator
- Inițializatorii sunt convenabili de utilizat când câmpul sau proprietatea clasei reprezintă altă clasă:
Person tom = new Person{ name = "Tom", company = { title = "Microsoft"} };
tom.Print(); // Nume: Tom Companie: Microsoft
class Person
{
public string name;
public Company company;
public Person()
{
name = "Necunoscut";
company = new Company();
}
public void Print() => Console.WriteLine($"Nume: {name} Companie: {company.title}");
}
class Company
{
public string title = "Necunoscut";
}
Observați cum este stabilit câmpul company:
company = { title = "Microsoft"}
Deconstructorii
Deconstructorii (deconstructorii nu trebuie confundați cu destructorii) permit realizarea decompunerii obiectului în părți separate.
De exemplu, să presupunem că avem următoarea clasă Person:
class Person
{
string name;
int age;
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void Deconstruct(out string personName, out int personAge)
{
personName = name;
personAge = age;
}
}
În acest caz, am putea realiza decompunerea obiectului Person astfel:
Person person = new Person("Tom", 33);
(string name, int age) = person;
Console.WriteLine(name); // Tom
Console.WriteLine(age); // 33
Valorile variabilelor din deconstructor se transmit prin poziție. Adică prima valoare returnată ca parametru personName se transmite primei variabile - name, iar a doua valoare returnată - variabilei age.
În esență, deconstructorii sunt doar o modalitate mai convenabilă de a descompune un obiect în componente. Este echivalent cu:
Person person = new Person("Tom", 33);
string name; int age;
person.Deconstruct(out name, out age);
La obținerea valorilor din deconstructor, trebuie să oferim atâtea variabile câte valori returnează deconstructorul. Totuși, se poate întâmpla ca nu toate aceste valori să fie necesare. Și în locul valorilor returnate, putem folosi underscore _. De exemplu, dacă avem nevoie doar de vârsta utilizatorului:
Person person = new Person("Tom", 33);
(_, int age) = person;
Console.WriteLine(age); // 33
Deoarece prima valoare returnată este numele utilizatorului, care nu este necesar, în acest caz, în loc de variabilă se folosește underscore.