Proprietăți
În afară de metodele obișnuite, limbajul C# prevede metode speciale de acces numite proprietăți. Ele asigură accesul simplu la câmpurile claselor și structurilor, aflarea valorii lor sau setarea acesteia.
Definirea proprietăților
Descrierea standard a unei proprietăți are următoarea sintaxă:
[modificatori] tip_proprietate nume_proprietate
{
get { acțiuni, efectuate la obținerea valorii proprietății}
set { acțiuni, efectuate la setarea valorii proprietății}
}
La începutul definiției proprietății pot fi diverși modificatori, în special modificatori de acces. Apoi se indică tipul proprietății, după care urmează numele proprietății. Definiția completă a proprietății conține două blocuri: get și set.
În blocul get se efectuează acțiuni pentru obținerea valorii proprietății. În acest bloc, cu ajutorul operatorului return, returnăm o anumită valoare.
În blocul set se setează valoarea proprietății. În acest bloc, cu ajutorul parametrului value, putem obține valoarea care este transmisă proprietății.
Blocurile get și set se mai numesc accesori sau metode de acces (la valoarea proprietății), precum și getter și setter.
Să considerăm un exemplu:
Person person = new Person();
// Setăm proprietatea - se activează blocul Set
// valoarea "Tom" este transmisă proprietății value
person.Name = "Tom";
// Obținem valoarea proprietății și o atribuim variabilei - se activează blocul Get
string personName = person.Name;
Console.WriteLine(personName); // Tom
class Person
{
private string name = "Undefined";
public string Name
{
get
{
return name; // returnăm valoarea proprietății
}
set
{
name = value; // setăm noua valoare a proprietății
}
}
}
Aici, în clasa Person, este definit un câmp privat name, care stochează numele utilizatorului, și există o proprietate publică Name. Deși acestea au nume aproape identice, cu excepția diferenței de majuscule, acest lucru este doar un stil; numele pot fi arbitrare și nu trebuie neapărat să coincidă.
Prin intermediul acestei proprietăți, putem gestiona accesul la variabila name. În proprietatea din blocul get returnăm valoarea câmpului:
get { return name; }
Iar în blocul set setăm valoarea variabilei name. Parametrul value reprezintă valoarea transmisă, care este atribuită variabilei name.
set { name = value; }
În program, putem accesa această proprietate ca pe un câmp obișnuit. Dacă îi atribuim o valoare, se activează blocul set, iar valoarea transmisă este atribuită parametrului value:
person.Name = "Tom";
Dacă obținem valoarea proprietății, se activează blocul get, care, în esență, returnează valoarea variabilei name:
string personName = person.Name;
Deci, în esență, proprietatea Name nu stochează nimic, ea acționează ca un intermediar între codul extern și variabila name.
Ar putea apărea întrebarea de ce avem nevoie de proprietăți dacă, în această situație, ne putem descurca cu câmpurile obișnuite ale clasei? Dar proprietățile permit includerea logicii suplimentare, care poate fi necesară la setarea sau obținerea valorii. De exemplu, trebuie să stabilim o verificare pentru vârstă:
Person person = new Person();
Console.WriteLine(person.Age); // 1
// modificăm valoarea proprietății
person.Age = 37;
Console.WriteLine(person.Age); // 37
// încercăm să transmitem o valoare inacceptabilă
person.Age = -23; // Vârsta trebuie să fie între 1 și 120
Console.WriteLine(person.Age); // 37 - vârsta nu s-a schimbat
class Person
{
int age = 1;
public int Age
{
set
{
if (value < 1 || value > 120)
Console.WriteLine("Vârsta trebuie să fie între 1 și 120");
else
age = value;
}
get { return age; }
}
}
În acest caz, variabila age stochează vârsta utilizatorului. Nu putem accesa direct această variabilă - doar prin proprietatea Age. În blocul set stabilim valoarea dacă aceasta corespunde unui interval rezonabil. Prin urmare, la transmiterea unei valori pentru proprietatea Age care nu se încadrează în acest interval, valoarea variabilei nu va fi modificată:
person.Age = -23;
Consola programului va afișa:
1
37
Vârsta trebuie să fie între 1 și 120
37
Astfel, proprietatea permite intermediul și controlul accesului la datele obiectului.
Proprietăți doar pentru citire și scriere
Blocurile set și get nu trebuie să fie simultan prezente în proprietate. Dacă proprietatea definește doar blocul get, atunci această proprietate este disponibilă doar pentru citire - putem obține valoarea ei, dar nu o putem seta.
Și, invers, dacă proprietatea are doar blocul set, atunci această proprietate este disponibilă doar pentru scriere - putem doar să stabilim valoarea, dar nu o putem obține:
Person person = new Person();
// proprietatea pentru citire - putem obține valoarea
Console.WriteLine(person.Name); // Tom
// dar nu o putem seta
// person.Name = "Bob"; // ! Eroare
// proprietatea pentru scriere - putem seta valoarea
person.Age = 37;
// dar nu o putem obține
// Console.WriteLine(person.Age); // ! Eroare
person.Print();
class Person
{
string name = "Tom";
int age = 1;
// proprietate doar pentru scriere
public int Age
{
set { age = value; }
}
// proprietate doar pentru citire
public string Name
{
get { return name; }
}
public void Print()=> Console.WriteLine($"Name: {name} Age: {age}");
}
Aici, proprietatea Name este disponibilă doar pentru citire, deoarece are doar blocul get:
public string Name
{
get { return name; }
}
Putem obține valoarea acesteia, dar NU o putem seta:
Console.WriteLine(person.Name); // se poate obține
person.Name = "Bob"; // ! Eroare - nu se poate seta
Iar proprietatea Age, dimpotrivă, este disponibilă doar pentru scriere, deoarece are doar blocul set:
public int Age
{
set { age = value; }
}
Putem seta valoarea acesteia, dar nu o putem obține:
person.Age = 37; // se poate seta
Console.WriteLine(person.Age); // ! Eroare - nu se poate obține valoarea
Proprietăți calculate
Proprietățile nu sunt neapărat legate de o anumită variabilă. Ele pot fi calculate pe baza unor diverse expresii:
Person tom = new("Tom", "Smith");
Console.WriteLine(tom.Name); // Tom Smith
class Person
{
string firstName;
string lastName;
public string Name
{
get { return $"{firstName} {lastName}"; }
}
public Person(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
}
În acest caz, clasa Person are o proprietate Name care este disponibilă doar pentru citire și care returnează o valoare combinată pe baza valorilor variabilelor firstName și lastName.
Modificatori de acces
Putem aplica modificatori de acces nu doar la întreaga proprietate, ci și la blocurile get și set individuale:
Person tom = new("Tom");
// Eroare - set este declarat cu modificatorul private
//tom.Name = "Bob";
Console.WriteLine(tom.Name); // Tom
class Person
{
string name = "";
public string Name
{
get { return name; }
private set { name = value; }
}
public Person(string name) => Name = name;
}
Acum blocul set privat poate fi utilizat doar în această clasă - în metodele sale, proprietăți, constructor, dar nicidecum în altă clasă:
La utilizarea modificatorilor în proprietăți trebuie să se țină cont de câteva restricții:
- Modificatorul pentru blocul set sau get poate fi stabilit dacă proprietatea are ambele blocuri (și set și get)
- Doar un bloc set sau get poate avea modificator de acces, dar nu ambele simultan
- Modificatorul de acces al blocului set sau get trebuie să fie mai restrictiv decât modificatorul de acces al proprietății. De exemplu, dacă proprietatea are modificator public, atunci blocul set/get poate avea doar modificatori protected internal, internal, protected, private protected și private
Proprietăți automate
Proprietățile gestionează accesul la câmpurile clasei. Totuși, dacă avem zeci și mai multe câmpuri, să definim fiecare câmp și să scriem pentru el o proprietate de același tip ar fi obositor. De aceea, în .NET au fost adăugate proprietăți automate. Ele au o declarație prescurtată:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
De fapt, aici se creează câmpuri pentru proprietăți, doar că nu sunt create de programator în cod, ci de compilator automat la compilare.
Care este avantajul proprietăților automate, dacă ele se adresează doar unei variabile create automat, de ce nu ne adresăm direct variabilei fără proprietăți automate?
Ideea este că în orice moment, dacă este necesar, putem transforma proprietatea automată într-o proprietate obișnuită, adăugând în ea o logică specifică.
Trebuie să ținem cont că nu putem crea o proprietate automată doar pentru scriere, ca în cazul proprietăților standard.
Proprietăților automate li se pot atribui valori implicite (inițializare a proprietăților automate):
Person tom = new();
Console.WriteLine(tom.Name); // Tom
Console.WriteLine(tom.Age); // 37
class Person
{
public string Name { get; set; } = "Tom";
public int Age { get; set; } = 37;
}
Și dacă nu indicăm pentru obiectul Person valorile proprietăților Name și Age, atunci vor fi valabile valorile implicite.
Proprietățile automate pot avea și modificatori de acces:
class Person
{
public string Name { private set; get;}
public Person(string name) => Name = name;
}
Putem elimina blocul set și face proprietatea automată disponibilă doar pentru citire. În acest caz, pentru stocarea valorii acestei proprietăți se va crea implicit un câmp cu modificator readonly, de aceea trebuie să ținem cont că astfel de proprietăți get pot fi setate fie din constructorul clasei, ca în exemplul de mai sus, fie la inițializarea proprietății:
class Person
{
// prin inițializarea proprietății
public string Name { get; } = "Tom";
// prin constructor
public Person(string name) => Name = name;
}
Blocul init
Începând cu versiunea C# 9.0, setter-ele în proprietăți pot fi definite cu ajutorul operatorului init (de la cuvântul "inițializare" - acesta este blocul init menit să inițializeze proprietatea). Pentru setarea valorilor proprietăților cu init se poate utiliza doar inițializatorul, constructorul sau la declarare să se indice valoarea pentru acesta.
După inițializarea valorilor acestor proprietăți, valorile lor nu pot fi schimbate - sunt disponibile doar pentru citire. În acest sens, proprietățile init sunt similare cu proprietățile doar pentru citire. Diferența constă în faptul că proprietățile init le putem seta și în inițializator (proprietățile doar pentru citire nu pot fi setate în inițializator). De exemplu:
Person person = new();
//person.Name = "Bob"; //! Eroare - după inițializare valoarea nu poate fi schimbată
Console.WriteLine(person.Name); // Undefined
public class Person
{
public string Name { get; init; } = "Undefined";
}
În acest caz, clasa Person pentru proprietatea Name folosește operatorul init în locul setter-ului. În rezultat, la linia:
Person person = new();
se presupune crearea obiectului cu inițializarea tuturor proprietăților sale. În acest caz, proprietatea Name va primi ca valoare șirul "Undefined". Totuși, deoarece inițializarea proprietății a avut loc deja, la linia:
person.Name = "Bob"; // Eroare
vom primi o eroare.
Cum putem seta o astfel de proprietate? Mai sus a fost demonstrat unul dintre moduri - setarea valorii la definirea proprietății. Al doilea mod - prin constructor:
Person person = new("Tom");
Console.WriteLine(person.Name); // Tom
public class Person
{
public Person(string name) => Name = name;
public string Name { get; init; }
}
Al treilea mod - prin inițializator:
Person person = new() { Name = "Bob"};
Console.WriteLine(person.Name); // Bob
public class Person
{
public string Name { get; init; } = "";
}
De fapt, există și al patrulea mod - setarea printr-o altă proprietate cu modificatorul init:
var person = new Person() { Name = "Sam" };
Console.WriteLine(person.Name); // Sam
Console.WriteLine(person.Email); // Sam@gmail.com
public class Person
{
string name = "";
public string Name
{
get { return name; }
init
{
name = value;
Email = $"{value}@gmail.com";
}
}
public string Email { get; init; } = "";
}
În acest caz, proprietatea Name gestionează câmpul pentru citire name. Datorită acestui fapt, înainte de a seta valoarea proprietății, putem efectua o preprocesare. În plus, în expresia init se setează o altă proprietate init - Email, care pentru setarea valorii folosește valoarea proprietății Name - din nume obținem valoarea pentru adresa de e-mail.
Chiar dacă la declararea proprietății este indicată o valoare, în constructor o putem modifica. Valoarea setată în constructor poate fi modificată în inițializator. Totuși, procesul de inițializare se încheie aici. Și valoarea nu poate fi schimbată.
Scrierea prescurtată a proprietăților
Ca și metodele, putem scurta definițiile proprietăților. Deoarece blocurile get și set reprezintă metode speciale, la fel ca metodele obișnuite, dacă acestea conțin o singură instrucțiune, le putem scurta cu ajutorul operatorului =>:
class Person
{
string name;
public string Name
{
get => name;
set => name = value;
}
}
De asemenea, putem scurta întreaga proprietate:
class Person
{
string name;
// echivalentul public string Name { get { return name; } }
public string Name => name;
}
Modificatorul required
Modificatorul required (adăugat în C# 11) indică faptul că un câmp sau o proprietate cu acest modificator trebuie să fie inițializate. De exemplu, în următorul exemplu vom primi o eroare:
Person tom = new Person(); // eroare - proprietățile Name și Age nu sunt inițializate
public class Person
{
public required string Name { get; set; }
public required int Age { get; set; }
}
Aici, proprietățile Name și Age sunt marcate ca obligatorii pentru inițializare cu ajutorul modificatorului required, de aceea este necesar să folosim inițializatorul pentru a le seta valorile:
Person tom = new Person { Name = "Tom", Age = 38 }; // fără eroare
Nu contează dacă setăm aceste proprietăți în constructor sau le inițializăm la definire, trebuie să folosim inițializatorul pentru a le seta valorile. De exemplu, în următorul exemplu vom primi o eroare:
Person bob = new Person("Bob"); // eroare - proprietățile Name și Age trebuie totuși setate în inițializator
public class Person
{
public Person(string name)
{
Name = name;
}
public required string Name { get; set; }
public required int Age { get; set; } = 22;
}