În cadrul acestui tutorial o să discutăm despre particularitățile limbajului C# raportate la paradigma de programare care îi determină funcționalitatea, și anume la POO.

1. Ce este o clasă?

Programarea orientată pe obiect este cea mai importantă paradigmă de programare a tuturor timpurilor. Funcționalitatea ei se remarcă prin capacitatea de manipulare atât a obiectelor reale cât și a celor abstracte, prin crearea de obiecte, produse de entități logice create cu ajutorul claselor.

clasă este o entitate logică, adică un element capabil să modeleze (să descrie) acțiuni și stări ale obiectelor abstracte sau reale. Clasele creează noi tipuri de date, numite user-defined data types, prin utilizarea keyword-ului class. După definire, noile tipuri create de programator pot fi utilizate pentru a declara variabile (care de fapt sunt obiecte, în acest caz).

Să luăm un exemplu concret. Avem următoarea clasă:

class masina {
   int vitezaMomentana;
   double procentDistrugere;
   double nivelCarburant;
   double nivelTocirePneuri;
   //...

   void reamplaseazaMasina() { 
    // cod care reamplaseaza masina pe pista
    // ...
   }

   int alimentareMasina() {
    // cod care alimenteaza masina cu carburant
    // ...
   }

   bool reparaMasina(double procentajReparare) {
    // cod care repara masina in proportie egala cu procentul transmis
    // ...
   }
}

Ne imaginăm un joc video cu mașini. Ce vedeți mai sus este definiția unei clase denumită masina care ilustrează funcționalitatea unei mașini ce face parte din respectivul joc, în felul următor: prin câmpurile nivelDamage, nivelCarburant, etc, se memorează diferite stări ale mașinii respective, iar prin metodele (sau funcții membre) alimentareMasina, reparaMasina, etc, se realizează diferite acțiuni asupra mașinii respective.

Aceasta este definiția clasei.

Ce este un obiect?

Am spus mai sus că o clasă definește un nou tip de date (user-defined). Ca urmare, trebuie să declarăm o variabilă de tipul nou creat. Declararea unei variabile de tip masina se face în felul următor:

masina Logan;

Remarcați analogia cu int x; ? Este exact același lucru, numai că masina este un tip definit de programator (capabil să facă multe chestii, din câte se vede), iar int  este un tip definit de creatorii limbajului, capabil să memoreze numere întregi.

În declararea de mai sus, Logan este o variabilă de tip masina. Această variabilă, de tip user-defined, se numește obiect.

Pentru a utiliza obiectul nou declarat, este necesar să se aloce memorie pentru el. Acest lucru se face prin utilizarea operatorului new, în felul următor:

Logan = new masina( );

Ce înseamnă acest lucru: obiectului nou declarat i se alocă memorie în Heap-ul sistemului.

Acest pas se numește instanțierea obiectului.

În acest moment, obiectul poate fi utilizat efectiv, gen: într-un anumit punct al aplicației, atunci când mașina se lovește de un zid, spre exemplu, cu o anumită viteză, este necesar să se mărească nivelul damage-ului, cu un procent proporțional cu viteza de lovire, lucru care se face așa:

Logan.procentDistrugere = Logan.vitezaMomentana + 14/100;                // un exemplu oarecare

Ce observăm? Accesul la câmpurile clasei se realizează prin utilizarea operatorului de accesare (dot), și anume punct: ”.”

De asemenea, metodele se pot utiliza în aceeași manieră: spre exemplu, când mașina s-a răsturnat sau când a intrat într-un parapet și nu mai poate ieși, jucătorul apasă pe ”R” iar mașina se repoziționează pe pistă. Când se apasă pe ”R”, se execută următoarea instrucțiune:

Logan.reamplaseazaMasina( );

De asemenea, când mașina ajunge într-un SafeHouse iar player-ul poate seta nivelul de reparare, în funcție de anumite puncte bonus, se poate executa instrucțiunea:

Logan.reparaMasina(40);   ceea ce impune repararea mașinii în proporție de 40% (un procent eventual maxim).

Paradigmele programării orientate obiect

Observăm maleabilitatea comportamentului clasei precum și ușurința cu care se pot modela caracteristicile obiectelor. Din analiza acestor elemente de utilizare și componență, putem desprinde următoarele 4 deosebit de importante caracteristici ale limbajelor orientate pe obiecte:

  1. Abstractizarea – se referă la capacitatea acestei paradigme (obiect – orientate) de a simplifica realitatea prin conceptualizarea elementelor reale, lucru realizat cu ajutorul metodelor și câmpurilor clasei. Variabila nivelCarburant este o abstractizare a nivelului real de carburant al mașinii respective (fie reală, fie virtuală).
  2. Încapsularea – este proprietatea unei clase de a menține grupat întregul cod ce descrie funcționalitatea unui obiect. Am amintit într-un tutorial precedent de modificatorii de acces ai unei clase, adică acele cuvinte cheie care dictează din ce nivel poți avea acces la un anumit câmp. Acest ”din ce nivel” se referă la: din interiorul clasei (adică de la un obiect al clasei respective, caz în care modificatorul de acces se numește private); de la obiectele claselor derivate, de oriunde din cadrul aplicației – modificator public, etc.
  3. Moștenirea – se referă la capcitatea acestei paradigme de a defini noi tipuri (user-defined, evident) prin extinderea unora deja existente. Spre exemplu, clasa masina poate fi baza unei noi clase, denumită masinaMare care să moștenească toate caracteristicile clasei masina, dar să adauge unele noi, precum: tonaj, dimensiuneRoti, etc.
  4. Polimorfismul – este proprietatea unei interfețe (toți membrii publici ai unei clase) de a fi folosită cu acțiuni multiple. Spre exemplu, câmpul nivelCarburant indică pentru fiecare mașină (pentru fiecare obiect al clasei masina) exact același lucru concret, însă modificarea acestuia se realizează separat în funcție de consumul fiecărei mașini.

Toate aceste elemente par greu de înțeles, dar sunt foarte importante de fiecare dată când vine vorba de POO, de aceea cunoașterea lor este determinantă în ceea ce privește programarea într-un limbaj orientat pe obiect.

2. Proprietățile unei clase

2.1. Modificatori de acces

Am analizat mai sus definiția unei clase, și evident, am definit conceptul de clasă. Acum o să intrăm în profunzimea acestei noțiuni deosebit de utile în programarea orientată pe obiect.

O clasă, după cum am observat, conține două tipuri de elemente: variabile și funcții. Variabilele de clasă se numesc câmpuri, iar funcțiile se numesc metode. Acestea sunt principalele entități care compun o clasă. Mai sunt, însă, și altele, la fel de importante:

  •  Date membre – Câmpuri, Constante
  • Funcții membre – Metode, proprietăți, constructori, destructori, operatori, indexatori, evenimente.

Vom discuta despre toate aceste elemente în parte.

În primul rând, trebuie să vedem cum arată forma generală a unei clase:

class nume_clasa {
    //Câmpuri (date membre)
    [modificator de acces] Tip nume;

    //Metode
    [modificator de acces] tipReturnare numeMetoda(parametrii);
}

Cam asta ar fi ideea. nume_clasa este numele pe care îl asociați clasei pe care o definiți. Atunci când se declară câmpurile sau metodele (în cadrul clasei, evident), se specifică un modificator de acces, care dictează din ce punct al aplicației poate fi accesat(ă) un anumit câmp sau metodă. Aveți mai jos lista modificatorilor de acces din limbajul C#:

  • public – acces nelimitat, se poate accesa respectivul câmp sau metodă din orice punct al aplicației și chiar din aplicații externe.
  • private – poate fi accesat doar din interiorul clasei în care se află.
  • protected – doar obiectele clasei definite și cele ale claselor derivate pot accesa elementul respectiv.
  • internal – acces limitat la programul care conține clasa (deci la aplicația curentă).
  • protected-internal – doar obiectele clasei componente a aplicației și a claselor derivate pot accesa elementul respectiv.

Trebuie reținut faptul că acești modificatori de acces nu sunt obligatorii. Deci nu este imperios necesară specificarea lor, dar este deosebit de utilă. Dacă nu se specifică niciun modificator de acces, se consideră că metoda/câmpul definit are modificatorul de acces private. Este o regulă care trebuie reținută.

2.2. Primul program în C#

Având în vedere toate aceste elemente pe care le-am prezentat, putem crea primul nostru program în C#.

  1. Deschideți Visual Studio (versiunile specificate în primul tutorial din această serie).
  2. Mergeți la File – New – Project
  3. Selectați Visual C# – Console Application
  4. Denumiți proiectul oricum doriți, eu o să îi spun ConsoleApp. Numele proiectului se setează în câmpul Name din partea inferioară a ferestrei din care ați executat pasul 3.
  5. Apăsați OK.

Ce observați în această fereastră este un cod generat în C# de către mediul de dezvoltare, pentru a ușura crearea aplicației, programatorul fiind responsabil, în principal, de crearea nucleului aplicației, și mai puțin de elementele organizatorice (crearea numelui de spațiu, a clasei, specificarea referințelor, etc.). Evident, asta nu e o regulă, ci doar un simplu considerent.

Să vedem ce conține un simplu program în consolă redactat în C#:

În partea superioară a codului regăsim niște directive, adică niște comenzi care specifică ce referințe trebuie să conțină aplicația noastră. Keyword-ul using are rolul de a marca aceste directive. Toate acele cuvinte aflate după using sunt denumirea unor spații de nume (namespaces).

Un nume de spațiu este o entitate care încapsulează clase. Adică o metodă prin care se pot grupa mai multe clase. Probabil intuiți că într-un program nu se pot declara mai multe clase cu același nume, așa cum nu pot exista mai multe variabile cu același nume. De aceea, namespace-urile pot organiza mult mai eficient clasele, deci întregul cod al aplicației. System este numele de spațiu care încorporează toate numele de spații din BCL. De aceea, prezența lui în cadrul oricărui program în C# este imperios necesară.

Fiecare program în C# conține cel puțin un nume de spațiu. În cazul nostru, mediul a generat automat un nume de spațiu denumit ConsoleApp, deci chiar numele proiectului.

După cum am spus, în interiorul spațiilor de nume  se declară clase. În exemplul dat, clasa se numește Program (nume ce poate fi modificat).

Mai departe, în interiorul clasei observăm o metodă care are numele Main, tipul de returnare void, un singur parametru (și anume un tablou unidimensional de șiruri de caractere denumit args), și nu în ultimul rând, are calificativul static (care înseamnă că poate fi accesată fără o referință a clasei, deci fără un obiect, vom discuta în cele ce urmează despre acest calificativ).

Orice program în C# conține această metodă. Este entry-point-ul fiecărei aplicații în C#, deci punctul de inițiere al programului.

 2.3. Să lucrăm cu clasele

În interiorul metodei Main scrieți următoarea linie de cod:

Console.WriteLine("Salut!");

Ce înseamnă asta: Din clasa Console (creată de cei de la Microsoft), care aparține spațiului de nume System, se apelează metoda statică WriteLine cu parametrul “Salut”. Efectul acestei instrucțiuni este afișarea în consolă a mesajului transmis.

Executați programul (prin apăsarea butonului F5 – Debug), și veți observa o consolă (tradiționala fereastră neagră, uneori enervantă), în care se află scris mesajul “Salut”.

În baza celor învățate despre clase de mai sus, să analizăm următorul program, un pic mai elaborat:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
    class Test
    {
        public int numar = 0;
        int backup = 0;
        public static int testVariabilaStatica = 10;
        
        public void afisazaNumere() {
            Console.WriteLine("Numarul este: {0}", numar);
            Console.WriteLine("Backup-ul este: {0}", backup);
        }
        public void incrementeazaNumar() {
            numar++;
        }

        public void initializeazaBackup(){
            backup = numar;
        }


    }
    class Program {
        static void Main(string[] args) {
            Console.WriteLine("Am intrat in Main()");
            Test obiect = new Test();
            obiect.numar = 19;
            obiect.afisazaNumere();
            obiect.incrementeazaNumar();
            obiect.afisazaNumere();
            obiect.initializeazaBackup();
            obiect.afisazaNumere();
            Console.WriteLine(Test.testVariabilaStatica);
            Console.ReadKey();
        }
    }
}

Se afișează:

Am intrat in Main()
Numarul este: 19
Backup-ul este: 0
Numarul este: 20
Backup-ul este: 0
Numarul este: 20
Backup-ul este: 20
10

Să vedem: în cadrul acestui cod distingem două clase: clasa Test și clasa Program. Prima clasă definește 3 câmpuri (date membre) și 3 metode (funcții membre). După cum observați, toate metodele și toate câmpurile, cu excepția câmpului backup au modificatorul de acces public, deci pot fi accesate din afara clasei Test.

Analizăm funcția Main. După afișarea acelui mesaj, cum am văzut mai sus, declarăm un nou obiect al clasei Test, denumit obiect, după care alocăm memorie pentru el, cu ajutorul operatorului new.

Acum că avem obiectul alocat, începem să ne jucăm cu câmpurile și cu metodele clasei. Cum? Cu ajutorul operatorului dot (deci punct). Câmpul numar, fiind câmp public, poate fi accesat direct de către obiect. Practic, în instrucțiunea:

obiect.numar = 19;

se observă că variabila numar este utilizată la fel ca orice altă variabilă de tip întreg. Se pot face atribuiri, comparații, etc.

Același lucru și cu funcțiile membre. Ele se apelează la fel ca orice funcție normală, numai că sunt asociate obiectului declarat:

obiect.afisazaNumere();

De reținut faptul că atât datele membre ale unei clase cât și metodele asociate ale acesteia se referă doar la instanța curentă.

Spre exemplu, să considerăm următoarea secvență de instrucțiuni:

Test obiect1 = new Test();
Test obiect2 = new Test();

obiect1.numar = 10;

Console.WriteLine(obiect2.numar);

Se va afișa zero, și nu zece! De ce? Pentru că obiect1 are memorat în câmpul numar  valoarea 10, după cum se observă. Însă obiect2 nu are memorat în câmpul numar altă valoare decât zero, valoarea de inițializare din clasă.

Deci fiecare obiect are propriul set de câmpuri și de metode. Pentru fiecare obiect se alocă un nivel de memorie fix, care depinde de numărul de elemente ale clasei.

Afișarea formatată:

Următoarea linie de cod pare un pic cam ciudată, nu?

Console.WriteLine("Numarul este: {0}", numar);

De înseamnă de fapt: se afișază: Numarul este: 10 (dacă valoarea din variabila numar este 10). Practic, unde avem {0} între ghilimele, se înlocuiește cu prima variabilă de după virgulă. Mai exact:

numar = 19;Console.WriteLine("Numarul este: {0}", numar);

Se afișază: Numarul este: 19.

Încă un exemplu:

numar1 = 10;
numar2 = 100;
numar3 = 1000;
Console.WriteLine("Numar1 este: {0}, numar2 este: {1}, numar3 este: {2}", numar1, numar2, numar3);

Se afișază: Numar1 este 10, numar2 este: 100, numar3 este: 1000 

Acum sigur v-ați prins cum stă treaba. Ar fi interesant să încercați!

2.4 Membrii statici ai unei clase:

După cum ați observat în exemplul anterior, câmpul testVariabilaStatica este precedat de keyword-ul static. Ce înseamnă asta?

Înseamnă că el poate fi accesat direct cu numele clasei, fără declararea unui obiect. Mai exact:

class Test {
      public static int variabila = 20;
      // ...
      // ...
}

class Program {    
       public static void Main(string[] args) {       
             Test.variabila = 21;     //nu declar obiect, accesez cu numele clasei
       }
}

Deci sintaxa e nume_clasă.nume_variabilă. 

2.5 Constructori:

În momentul în care un obiect este creat, se alocă memorie pentru el, așa cum am văzut mai sus. Zona de memorie în care se realizează acest lucru se numește Heap. 

Un constructor este o metodă specială a clasei care are rolul de crea obiecte, prin alocarea unui nivel de memorie necesar, precum și prin executarea unor eventuale instrucțiuni impuse de programator.

Am văzut cum se instanțiază un obiect:

Test obiect = new Test();

Acele paranteze indică apelul unei metode care nu acceptă niciun parametru. Această metodă care nu acceptă niciun parametru se numește constructorul implicit al clasei considerate (Test, în acest caz). Crearea acestui constructor se realizează în mod automat de către platformă, de aceea se numește implicit. Evident, programatorul își poate declara proprii constructori care pot accepta sau nu parametrii:

class Test {      
    public int variabila = 0;
               
    public Test() {
    	variabila = 1;
    }
    
    public Test(int valoare) {
        variabila = valoare;
    }
}

class Program {
    public static void Main(string[] args) {
		Test obiect1 = new Test();
		Console.WriteLine(obiect1.variabila);
		Test obiect2 = new Test(30);
		Console.WriteLine(obiect2.variabila);
    }
}

Avem aceeași clasă Test care are un câmp public denumit variabila și doi constructori: unul care nu acceptă parametrii, iar unul care acceptă un parametru de tip întreg.

Remarcăm în Main( ) modul în care se instanțiază obiectele: obiect1 este instanțiat în mod implicit, deci se apelează constructorul fără parametrii, care modifică valoarea câmpului variabila în 1. Deci se va afișa 1.

Pe de altă parte, obiect2 este instanțiat în mod parametrizat, deci se apelează constructorul cu parametrul întreg, ceea ce va genera afișarea valorii 30.

Există câteva reguli care trebuie reținute atunci când vine vorba de constructori:

  1. Se apelează de fiecare dată când se instanțiază un obiect.
  2. Nu au tip de returnare.
  3. Au nume identic cu cel al clasei.
  4. Pot avea sau nu parametrii.
  5. Principalul rol al constructorilor este acela de a aloca memorie pentru obiectul nou creat, deci pregătirea acestuia pentru utilizare.
  6. De regulă se declară public dar pot fi declarați și private atunci când clasa are, în principal, numai câmpuri statice.

Un tip special de constructor este constructorul de copiere, adică un constructor care are ca parametru un obiect de tipul clasei. Mai concret:

class Test {      
    public int variabila = 0;
               
    public Test() {
    	variabila = 1;
    }
    
    public Test(Test obiect) {
        variabila = obiect.variabila;
    }
}

class Program {
    public static void Main(string[] args) {
		Test obiect1 = new Test();
		Console.WriteLine(obiect1.variabila);
		Test obiect2 = new Test(obiect1);
		Console.WriteLine(obiect2.variabila);
    }
}

Observăm că al doilea constructor are ca parametru un obiect de tip Test, deci este un constructor de copiere. În Main( ), se instanțiază un obiect în mod implicit, iar al doilea obiect, obiect2, se instanțiază cu ajutorul constructorului de copiere, deci, având în vedere corpul constructorului de copiere, câmpul variabila al obiectului 2, va memora valoarea 1. Deci secvența de instrucțiuni va afișa 1 1.

Se utilizează, de regulă, atunci când se dorește modificarea unui obiect, pornind de la o copie fidelă a unui obiect deja existent.

2.6 Destructori

Destructorii sunt metode speciale care au rolul de a distruge obiectele. A ”distruge” un obiect  înseamnă a elibera memoria pe care el o ocupă. Sintaxa pentru un destructor este:

class Test {
    ~Test() {      
         // Cod care eliberează memorie
    }
}

Ce trebuie reținut în ceea ce privește destructorii:

  1. După cum se vede, are numele clasei și nu acceptă niciun parametru.
  2. Nu are tip de returnare.
  3. Este precedat de caracterul tilde “~”.
  4. Nu acceptă modificatori de acces.
  5. Sunt apelați în mod automat.
  6. O clasă poate avea un singur destructor.
  7. Destructorii nu sunt necesari atunci când vine vorba de cod specific platformei .NET, deci atunci când nu se programează la nivel de memorie în mediu unsafe, deci pointeri & stuff.

În principiu, aceasta este o introducere în mediul orientat pe obiect. Am prezentat principiile acestei paradigme, precum și elementele de bază care definesc acest limbaj din punct de vedere al POO. Urmează, în tutorialul viitor, să vorbim despre indexatori, proprietăți, supraîncărcare de operatori și multe alte lucruri interesante.

Mult succes! Ioniță Cosmin.

Join the Conversation

1 Comment

  1. Multumesc pentru aceasta introducere in POO, explica f bine multe concepte dificile

Leave a comment

Your email address will not be published. Required fields are marked *

Send this to a friend