Polimorfismul
Polimorfismul este consecința directă a perechii virtual/override, prezentată în capitolul anterior. Odată ce mai multe clase definesc versiuni diferite ale aceleiași metode, apare o întrebare fundamentală: când apelezi metoda pe un obiect al cărui tip exact nu îl cunoști la momentul scrierii codului, ce versiune rulează?
Răspunsul este întotdeauna: versiunea corespunzătoare tipului real al obiectului din memorie, nu tipului variabilei prin care îl accesezi.
Aceasta este definiția practică a polimorfismului. Nu este un concept abstract, ci un comportament concret și verificabil al runtime-ului C#.
Un exemplu pas cu pas
Creăm patru obiecte de tipuri diferite și le plasăm într-o listă de tip Bilet:
List<Bilet> bilete = new List<Bilet>
{
new Bilet(..., pretBaza: 30),
new BiletStudent(..., pretBaza: 30),
new BiletSenior(..., pretBaza: 30, varstaClient: 65),
new BiletVIP(..., pretBaza: 60, includePopcorn: true, includeBautura: false)
};Lista este de tip List<Bilet>. Compilatorul acceptă toate cele patru obiecte deoarece fiecare este fie un Bilet, fie o clasă derivată din el. Toate respectă relația „este un" prezentată la moștenire. Din perspectiva listei, toate sunt bilete.
Acum parcurgem lista și apelăm GetReducere() pe fiecare element:
foreach (Bilet b in bilete)
{
Console.WriteLine($"Reducere: {b.GetReducere()} RON");
}Variabila b este de tip Bilet la fiecare iterație. Dar obiectul din memorie la care b pointează este de tipul real cu care a fost creat. C# caută implementarea lui GetReducere() pe acel tip real:
Același apel b.GetReducere(), scris o singură dată în cod, produce patru rezultate diferite în funcție de tipul real al fiecărui obiect. Acesta este polimorfismul.
De ce este utilă această proprietate?
Fără polimorfism, codul care lucrează cu colecții mixte ar trebui să verifice manual tipul fiecărui obiect și să aplice logica corespunzătoare:
Această abordare are o problemă gravă de mentenabilitate: de fiecare dată când adăugăm un tip nou de bilet, trebuie să găsim și să modificăm toate locurile din cod care fac astfel de verificări manuale. Dacă uităm unul singur, obținem un bug silențios. Codul compilează fără erori, dar produce rezultate greșite.
Cu polimorfism, fiecare clasă își cunoaște propria logică și o exprimă prin override. Codul care parcurge colecția nu știe și nu trebuie să știe câte tipuri există:
Dacă adăugăm mâine BiletCopil cu reducere de 50%, nu modificăm nimic în foreach. Pur și simplu clasa nouă implementează override double GetReducere() și totul funcționează automat, fără nicio intervenție în codul care consumă colecția.
Polimorfismul în CasaBilete
CasaBileteCasaBilete menține o colecție privată de tip List<Bilet> și calculează totalul încasărilor fără să cunoască tipurile concrete ale biletelor stocate:
CasaBilete a fost scrisă o singură dată și funcționează corect pentru orice combinație de tipuri de bilete, inclusiv tipuri care nu existau când a fost scrisă. Aceasta este puterea reală a polimorfismului: codul care consumă obiecte nu depinde de tipurile concrete ale acelor obiecte.
Polimorfism prin interfețe
Polimorfismul funcționează și prin interfețe, nu doar prin moștenire. Dacă mai multe clase implementează aceeași interfață, pot fi tratate uniform prin tipul interfeței, indiferent dacă au vreo relație de moștenire între ele:
IPretCalculabil nu știe nimic despre reduceri, extras-uri sau tipuri concrete. Știe doar că obiectele care o implementează pot calcula un preț final. Atât îi este necesar. Fiecare obiect din listă aplică propria logică, conform propriei implementări a interfeței.
is și as - când tipul concret contează
is și as - când tipul concret conteazăPolimorfismul rezolvă elegant cazurile în care vrem să tratăm toate obiectele uniform. Există totuși situații în care tipul concret al obiectului chiar contează (de exemplu, când vrem să afișăm informații specifice fiecărui tip, informații care nu există pe clasa de bază).
Pattern matching cu is verifică tipul și, dacă se potrivește, extrage o referință tipizată într-un singur pas:
Dacă b este de tipul BiletStudent, condiția este true și variabila bs este populată automat cu referința tipizată, gata de utilizat în blocul if. Dacă nu, condiția este false și blocul este ignorat.
Ordinea ramurilor contează: tipurile mai specifice trebuie verificate înaintea celor mai generale. Dacă BiletStudentVIP moștenește din BiletVIP, ramura b is BiletVIP ar prinde și obiectele de tip BiletStudentVIP. Tipul mai specific trebuie verificat primul.
Regulă practică: polimorfism prin virtual/override pentru logică care se aplică uniform tuturor obiectelor, is/as pentru cazurile în care comportamentul diferă structural și informațiile necesare nu există pe clasa de bază.
Last updated