Fundamentals of C# 2010
Sumar
1. Mostenirea................................................................................................................................................... 3
1.1. Apelul constructorilor........................................................................................................................... 4
1.2. Atribuirea intre obiecte ........................................................................................................................ 5
1.3. Metode virtuale ................................................................................................................................... 6
1.4. Metode suprascrise .............................................................................................................................. 6
1.5. Metode virtuale si Polimorfismul ......................................................................................................... 7
2. Interfete si clase abstracte........................................................................................................................... 8
2.1. Interfete ............................................................................................................................................... 8
2.2. Clase abstracte................................................................................................................................... 10
3. Proprietati .................................................................................................................................................. 11
4. Tipul generic............................................................................................................................................... 13
5. Supraincarcarea operatorilor..................................................................................................................... 14
2
Fundamentals of C# 2010
1. Mostenirea
1.1. Notiuni introductive
Declararea unei clase derivate dintr-o alta clasa (parinte) se face cu sintxa:
class ClasaDerivata : ClassaParinte
{
//campuri specifice clasei derivate
}
In C# este permis ca o clasa sa fie derivata din cel mult o alta clasa. Nu se poate ca o clasa sa fie
derivata din mai mult de o clasa de baza.
Daca ne referim la exemplul de la inceputul capitolului, atunci putem defini clasele Mamifer, Cal si
Balena astfel :
class Mamifer
{
public void Respira()
{
3
Fundamentals of C# 2010
}
}
Toate clasele din C# sunt derivate din clasa radacina parinte System.Object. Deci, pentru clasa
Mamifer pe care am definit-o, compilatorul va adauga :
class Mamifer : System.Object
{
....................
}
}
}
}
}
Asa cum amvazut in capitolele anterioare, este posibil sa facem atribuiri intre obiecte, dar
respectand anumite reguli. In cazul in care avem obiecte ale unor clase derivate din clase parinte,
vom utiliza exemplul celor trei clase de mai sus pentru exemplificare :
class Mamifer
{
}
Pe de alta parte, insa, este posibil sa facem referinta la un obiect dintr-o variabla de un alt tip atat
timp cat tipul utilizat este o clasa situata mai sus in ierarhia data de mostenirea claselor. Deci,
urmatoarea atribuire este corecta :
Cal horse = new Cal("Cezar");
Mamifer m = horse; //corect
Aceasta atribuire este corecta intrucat toti Caii sunt Mamifere, deci atribuirea unui obiect de tip Cal
unui obiect de tip Mamifer este logica. Aceasta ierarhizare prin derivare are semnificatia ca un Cal
este pur si simplu un tip special de Mamifer. Desi exemplul de mai sus este perfect corect, atunci
cand fac referire la un obiect de tip Cal sau Balena printr-un obiect de tip Mamifer, pot accesa
numai campurile si metodele definite in clasa Mamifer. Campurile si metodele definite la nivelul
claselo Cal sau Balena nu vor fi visibile prin intermediul clasei Mamifer.
Cal horse = new Cal("Cezar");
5
Fundamentals of C# 2010
Atribuirea in sens invers decat exemplul de mai sus (adica horse = m) nu se poate face direct. Am
spus ca toti Caii sunt Mamifere, dar nu toate Mamiderele sunt Cai (unele mai sunt si Balene). Pot
face aceasta atribuire atata timp cat m a fost incarcat inainte cu o referinta la un obiect de tip Cal.
Uneori se doreste sa se ascunda modul in care este implementata o metoda in clasa de baza.
Sa luam de exemplu metoda ToString() din clasa System.Object. Scopul acestei metode este sa
converteasca un obiect in reprezentarea sa ca string. Pentru ca aceasta metoda este foarte utila, ea
a fost plasata in clasa cea mai de sus din ierarhia claselor, System.Object. Prin urmare, toate
clasele au acces la aceasta metoda (pentru ca toate clasele sunt derivate din System.Object). Si
totusi, cum stie metoda ToString() definita in System.Object sa transforme o instanta a clasei
derivate in string? Raspunsul este ca implementarea metodei in clasa System.Object este una
foarte simplista. Tot ceea ce face este sa intoarca un string care contine de fapt tipul obiectului.
Deci, se pare ca nu este prea utila in forma asta. Mai este metoda ToString() utila, fiind atat de
simpla? Raspunsul este da. Sa explicam de ce.
Din clasa de baza System.Object se pot deriva clase atat de variate, incat ar fi imposibil ca doar
implementarea din System.Object a metodei ToString() sa poate face conversia oricarei clase in
string. Pentru ca aceasta metoda sa-si indeplineasca scopul, ar trebui sa in fiecare clasa derivata
din System.Object (si cum toate sunt derivate, deci in toate clasele existente), sa implementam
clasa ToString() conform propriilor necesitati, specifice fiecarei clase in parte. Deci, practic, trebuie
sa suprascriem metoda ToString(). O metoda care este propusa pentru a fi suprascrisa (overridden)
se numeste metoda virtuala. Suprascrierea unei metoda este mecanismul prin care se dau
implementari diferite aceleasi metode, functie de logica clasei in care implementez metode, deci in
maniere diferite. Pentru a marca o metoda ca fiind virtuala, se utilizeaza cuvantul-cheie virtual in
declararea metodei :
public class Object
{
........
public virtual string ToString();
}
Daca intr-o clasa de baza o metda este declarata ca virtuala, in clasa derivata se poate folosi
cuvantul-cheie override pentru a declara o alta implementare a acelei metode. Noua implementare
a metodei in clasa derivata poate apela metoda din clasa de baza folosind cuvantul-cheie base:
6
Fundamentals of C# 2010
Fenomenul precizat in capitolul precedent, de a declara o metoda virtuala intr-o clasa de baza si
apoi de a putea sa o rescriem in clase diferite, face ca metoda rescrisa sa aiba rezultate diferite, in
functie de implementarea din fiecare clasa derivata. Acest fenomen se numeste polimorfism. Deci
pentru a realiza acest comportament polimorfic, este necesar mecanismul de clase virtuale si
suprascrise.
7
Fundamentals of C# 2010
Derivarea dintr-o clasa, prin mostenire, este un mecanism puternic, dar adevarata putere a
mostenirii vine cand cea derivata este o interfata . O interfata nu contine nicio implementare si nicio
data; ea specifica doar metodele si proprietatile pe care clasa derivata din interfata trebuie sa le
ofere, sa le implementeze. Interfata permite o delimitare totala a numelui si semnaturii unei metode
de implementarea ei.
Clasele abstracte sunt similare interfetelor, doar ca ele pot contine cod si date. Totusi se pot
specifica ca anumite metode ale unei clase abstracte sunt virtuale, astfel incat o clasa care
mosteneste o astfel de clasa abstracta trebuie sa ofere propria implementare a acestor metode.
2.1. Interfete
Sa presupune ca dorim crearea unei noi clase colectie care permite unei aplicatii sa retina
obiecte intr-o ordine care depinde de tipul obiectelor pe care le contine colectia (colectia este o
multime de obiecte, de orice tip; pentru o colectie definita de la inceput ca avand elemente de un
anumit tip, atunci toate elementele din colectie sunt de tipul respectiv). De exemplu, daca colectia
contine obiecte alfanumerice precum stringurile, atunci colectia ar trebui sa ordoneze obiectele in
ordine alfabetica, iar daca colectia contine obiecte de tipul integer de exemplu, atunci colectia sa
ordoneze aceste obiecte dupa un criteriu numeric.
Cand se defineste clasa colectie, nu se doreste sa se restrictioneze tipul de obiecte care se retin
(obiectele pot fi chiar de tip structura sau clasa), si prin urmare nu cunoastem de la inceput cum sa
ordonam aceste obiecte. Intrebarea care se pune este cum putem crea o metoda in clasa colectie
care sa ordoneze obiecte pentru care nu cunoastem tipul in momentul scrierii clasei? La prima
vedere, problema pare asemanatoare cu cea descrisa pentru metoda ToString(), deci rezolvarea ar
fi implementarea unui metode virtuale pe care clasele derivate din clasa colectie initiala sa o
suprascrie. Solutia nu este tocmai una potrivita, pentru ca de obicei nu exista o relatie de mostenire
intre clasa colectie si obiectele retinute. Solutia este de a impune ca toate obiectele sa ofere o
metoda, cum ar fi CompareTo(), pe care colectia sa o apeleze, permitand astfel colectiei sa
compare obiectele unul cu celalt:
int CompareTo(object obj)
{
// return 0 if this instance is equal to obj
// return < 0 if this instance is less than obj
// return > 0 if this instance is greater than obj
}
Prin urmare, clasa colectie poate folosi aceasta metoda pentru a sorta obiectele pe care le contine.
Prin urmare se poate defini o interfata pentru a colecta obiecte care contin metoda CompareTo()
si apoi sa se specifice ca clasa colectie de mai sus poate colecta doar clase care implementeaza
aceasta interfata. In acest fel o interfata este similara unui contract daca o clasa implementeaza
o interfata, atunci interfata garanteaza ca toate metodele din ea vor fi incluse in clasa. Acest
mecanism asigura ca vom putea apela metoda CompareTo() pentru toate obiectele din colectie si le
vom putea sorta.
8
Fundamentals of C# 2010
Altfel spus, interfata permite o separare clare intre ce si cum. In interfata se declara numai
numele, tipul returnat si parametrii unei metode. Cum este mai exact implementata metoda, nu este
treaba interfetei. Interfata descrie functionalitatea pe care o clasa trebuie sa o implementeze, nu si
cum sa o implementeze.
Definirea unei interfete se face folosind cuvantul-cheie interface in loc de class sau struct. In
interfata se definesc metode exact ca intr-o clasa sau o structura, cu diferenta ca nu se specifica
modificator de acces (public, private, protected). De asemenea, metodei nu i se furnizeaza nicio
implementare, corpul ei fiind inlocuit de ;. De exemplu :
public interface IComparable
{
int CompareTo(object obj);
}
Implementarea unei interfete se face declarand o clasa (sau structura) care mosteneste (e
derivata din) interfata si implementeaza toate metodele declarate in interfata. Daca revenim la
exemplul de mai devreme, clasele Mamifer, Cal si Balena, presupunem ca dorim definirea unei
metode NumarPicioare() care sa intoarca un int (pentru numarul de picioare) pentru mamiferele
care traiesc pe uscat. Putem defini o interfata IMamiferTerestru care contine o metoda :
interface IMamiferTerestru
{
int NumarPicioare();
}
Putem implementa aceasta interfata in clasa Cal (evident, balena nefiind mamifer terestru si
neavand picioare):
class Cal : Mamifer, IMamiferTerestru
{
public int NumarPicioare()
{
return 4;
}
}
O clasa poate sa extinda (sa fie derivata din) o singura alta clasa, dar oricate interfete. Regula
este inca ca prima dupa : fie clasa, apoi interfetele, separate prin ,.
9
Fundamentals of C# 2010
La fel cum am vazut ca se poate referi un obiect utilizand o variabila definita de tipul unei clase
aflate la un nivel superior in ierarhia claselor, tot asa se poate face referinta la un obiect utilizand o
variabila de tipul interfata pe care clasa o implementeaza:
Aceasta tehnica a referirii unui obiect printr-o interfata este utila pentru ca permite definirea de
metoda care sa primeasca diverse tipuri de parametri cat timp tipul respectiv implementeaza o
interfata. De exmplu, putem defini o metoda VitezaPeUscat() care ia ca si parametru de intrare orice
obiect care implementeaza interfata IMamiferTerestru:
int VitezaPeUscat(IMamiferTerestru mamiferTerestru)
{
if (mamiferTerestru is Cal)
{
return 60;
}
return 0;
}
Exista situatii cand se doreste ca o clasa definita sa nu poate fi instantiata. Asta pentru ca ea a
fost creata doar pentru a oferi o implementare comuna implicita a unor metode definite intr-o
eventuala interfata pe care clasa o implementeaza. In acest caz, clasa in cauza este numita
abstracta si se declara folosind cuvantul-cheie abstract prefixat declaratiei clasei.
O clasa abstracta este poate contine metode abstracte. O metoda abstracta este in principiu
similara uneia virtuale, cu execptia faptului ca aceasta nu contine nicio instructiune (nu are corp). O
metoda abstracta este utila atunci cand nu are sens sa i se furnizeze vreo implementare implicita in
clasa abstracta, ci se doreste ca ea sa fie implementata in clasa derivata (clasa derivata sa ofere
propria implementare).
10
Fundamentals of C# 2010
3. Proprietati
Asa cum am prezentat in capitolele anterioare, unul dintre avantajele si principiile POO este
incapsularea. Ideea incapsularii este ca avand definita o clasa, sa stabilim noi ce anume se va
vedea din clasa de afara (ce este public) si ce anume nu trebuie sa se vada (ce este privat). Am
vazut atunci ca o metoda comuna era declararea campurilor din clasa ca si private si initializarea lor
cu valori date la instantierea clasei, in constructorul clasei. Aceasta metoda este buna pana la un
punct daca se doreste ca aceste campuri private sa poate fi initializate din afara o singura data, la
construirea obiectului, si apoi sa nu mai existe acces la ele. In practica de multe ori este nevoie insa
ca anumite campuri dintr-o clasa sa poate fi modificate din exteriorul clasei si dupa ce obiectul a fost
creat (deci nu mai putem utiliza constructorul). Mai mult, e nevoie uneori ca anumite campuri ale
clasei sa poata fi doar citite (dupa ce in prealabil au fost initializate o data). Aceasta problema se
poate rezolva cu ajutorul proprietatilor.
Proprietatile sunt un fel de mixt intre un camp dintr-o clasa si o metoda din clasa. Nu e nici
camp, nici metoda. Seamana cu o metoda, dar se comporta ca si un camp.
Sintaxa pentru declararea unei proprietati este :
<specificator_de_acces> <tip> <nume_proprietate>
{
get
{
//cod pentru a accesa
}
set
{
//cod pentru a scrie
}
}
public int X
{
11
Fundamentals of C# 2010
public int Y
{
get { return this.y; }
set { this.y = rangeCheckedY(value); }
}
Utilizarea proprietatilor in expresii se poate face fie in citire (cand se citeste valoarea
proprietatii), fie in scriere (cand i se da proprietatii o valoare). De exemplu :
ScreenPosition origin = new ScreenPosition(0, 0);
int xpos = origin.X; // calls origin.X.get
int ypos = origin.Y; // calls origin.Y.get
origin.X = 10;
origin.Y = 10;
Daca pentru o proprietate s-a definit doar blocul get, atunci am obtinut o proprietate read-only.
Prin urmare proprietatea respectiva poate fi poar citita, in ea nu se poate scrie nimic din exterior.
Daca pentru o proprietate s-a definit doar blocul set, atunci am obtinut o proprietate write-only.
Prin urmare proprietatea respectiva poate fi poar scrisa, din ea nu se poate citi nimic din exterior.
Proprietatile pot fi declarate in cadrul interfetelor. In acest caz se definesc get si set, doar ca nu
contin nimic:
interface IScreenPosition
{
int X { get; set; }
int Y { get; set; }
}
Orice clasa sau structura care implementeaza interfata trebuie sa implementeze si proprietatile
definite in interfata.
12
Fundamentals of C# 2010
4. Tipul generic
Pentru a intelege tipul generic (generics), revenim mai in detalii la o problema pe care o
introduce folosirea tipului obiect (obiect).
Tipul obiect se poate folosi oricand pentru a ne referi la o valoarea sau o variabila de orice tip.
Toate tipurile referinta sunt derivate (sau mostenesc) direct sau indirect clasa System.Object.
Datorita acestui lucru, se pot crea clase si metode super-generale, care sa lucreze doar cu obiecte.
De exemplu, multe dintre clasele namespace-ului System.Collections utilizeaza aceasta
oportunitate, deci putem crea colectii care sa contina aproape orice tip de data. Daca revenim la
unul dintre tipurile de colectii descrise in capitolul 4 din cursul trecut, si anume la clasa
System.Collections.Queue, am observat ca putem crea cozi (liste queue) care sa contina practic
orice. Reluam exemplul utilizarii clasei :
c = (Cerc)coadaCercuri.Dequeue();
Metoda Enqueue() adauga un obiect in coada, iar metoda Dequeue() scoate un element din coada.
Deoarece aceste doua metode manipuleaza obiecte, se poate crea o coada de obiecte Cerc, Cal,
Mamifer etc, deci de orice tip doresc. Pe de alta parte este important a se observa ca a trebuit sa se
faca o conversie explicita (cast) a valorii returnate de catre metoda Dequeue() (adica un obiect) la
tipul de data potrivit (in acest caz la tipul Cerc), deoarece compilatorul nu face implicit conversia
unui obiect la un tip anume. Daca nu se face conversia explicita, atunci se obtine o eroare de
compilare. Drept urmare, aceasta necesitate de a face o conversie explicita stirbeste mult din
flexibilitatea pe care o ofera tipul obiect. Mai ales ca este foarte simplu sa gresim scriind :
Mamifer m = (Mamifer)coadaCercuri.Dequeue();
Desi codul de mai sus va compila, el nu este valid, generandu-se o exceptie la rulare. Cauza este
evident, incerc sa pun intr-un obiect de tip Mamifer un obiect de tip Cerc. Aceasta eroare nu poate fi
descoperita decat la run-time.
Un alt dezavantaj al utilizarii tipului obiect pentru a crea clase si metode cat mai generale este
surplusul de memorie si de timp procesor necesare atunci cand la run-time este necesara o
conversie intre tipul obiect si alt tip, sau invers.
Solutia la aceste probleme cauzate de folosirea tipului obiect peste tot, este asa-numitul tip
generic. Aceasta solutia aduce o serie de avantaje precum inlaturarea nevoii de conversie explicita,
creste stabilitatea aplicatiei avant tipuri bine definite, reduce operatiile de boxing/unboxing, si
pastreaza capacitatea de a crea clase si metode generale. Spre deosebire de ce am vazut pana
acum, clasele si metodele generice accepta ca si parametri direct tipul de data, care are
13
Fundamentals of C# 2010
semnificatia de tipul de date cu care clasele si metodele vor opera. Librariile din framework-ul .NET
ofera versiuni generice ale multora dintre clasele de tip colectie si interfetelor, in namespace-ul
System.Collections.Generic. De exemplul :
Queue<Cerc> coadaCercuri = new Queue<Cerc>();
Cerc c = new Cerc();
coadaCercuri.Enqueue(c);
c = coadaCercuri.Dequeue();
Tipul definit intre semnele <> reprezinta tipul de date pe care coadaCercuri le accepta. Orice
metoda s-ar apela din aceasta clasa Queue, ea (metoda) se astepata sa primeasca un obiect de tip
Cerc in primul rand. Mai mult decat atat,verificarile de proptrivire de tipuri se fac chiar la compilare,
generandu-se erori daca este cazul. Atfel, erorile la run-time, cum am vazut in primul exemplu, se
elimina din start.
Este de asemenea posibil ca o clasa generica sa aiba mai multi parametri tip. De exemplu,
clasa generica System.Collections.Generic.Dictionary astepata doi parametri : unul pentru tipul cheii
din dictionar, celalalt pentru tipul valorii puse in dictionar, corespunzator cheii.:
public class Dictionary<TKey, TValue>
5. Supraincarcarea operatorilor
Daca ne amintim ce am spus in capitolele de inceput, operatorii sunt simboluri care actioneaza
asupra unor date, numite operanzi. Cei mai cunoscuti operatori sunt, evident, cei matematici. Pana
acum, operatorii actionau doar asupra operanzilor numerici. Ar fi interesant deci daca am putea face
ca un operator aritmetic (matematic) sa actioneze si asupra altor tipuri de date, inclusiv date de
tipuri referinta.
Asa cum am vazut pana acum, limbajul C# permite sa supraincarcam metode care se defineste
propriul tip de date (propria clasa). Similar, C# sa supraincarcam multi dintre operatorii obisnuiti.
Totusi, exista niste reguli pentru a realiza asta :
- Nu se poate modifica prioritatea si asociativitatea operatorilor;
- Nu se poaet schimba numarul de operanzi la care se aplica un operator; de exemplu, operatorul
* (multiplicare) este un operator binar, adica are nevoie de doi operanzi;
- Nu se pot inventa noi operatori;
- Exista si operatori care nu se pot supraincarca. De exemplu, nu se poate supraincarca
operatorulm . (punct) care indica accesul la un membru al unei clase.
class Complex
{
14
Fundamentals of C# 2010
private float a;
private float b;
public Complex()
{
a = 0;
b = 0;
}
Putem apoi defini foarte bine doua numere complexe conform acestei clase:
Complex z1 = new Complex(1, 2);
Complex z2 = new Complex(3, 4);
Ca si in matematica, adunarea a doua numere complexe este perfect valabila. Dar, daca declar :
Complex z3 = z1 + z2;
voi obtine o eroare de compilare, caci operatorul + nu stie sa actioneze asupra obiecteleor de tip
Complex. Prin urmare, ceea ce incercam prin supraincarcarea operatorilor este sa-i invatam sa
efectueze operatii si asupra altor tipuri de date decat cele pe care ei implicit le cunosc.
Sintaxa pentru supraincarcarea operatorilor este asemanatoare cu cea a declararii unei metode,
cu deosebirea ca, in cazul nostru, numele metodei trebuie sa fie cuvantul-rezervat operator si urmat
imediat de simbolul, operatorul, supraincarcat. Pentru exemplul de mai sus, putem supraincarca
operatorul + astfel :
class Complex
{
public float Re
{
get { return this.a; }
}
public float Im
{
get { return this.b; }
}
.
Bineinteles, asa cum spune si numele, supraincarcarea unui operator poate merge mai departe si
poate primi diverse implementari. De exemplu, o implementare ar fi sa permitem adunarea la un
numar complez a unei valoari numerice pur si simplu :
Complex z3 = z1 + 10;
rezultatul fiind, evident, tot un numar complex:
public static Complex operator +(Complex z1, Complex z2)
{
return new Complex(z1.Re + z2.Re, z1.Im + z2.Im);
}
Exact in acelasi mod se pot supraincarcarea si operatorii unari (++, --), cu conditia ca fiecare sa
primeasca un singur parametru, precum si operatorii de egalitate (==), respectiv inegalitate (!=).
16