În acest tutorial vom învăța despre unele dintre cele mai importante concepte ale limbajului C#, ca limbaj orientat pe obiect. Sunt elemente cu o mare aplicabilitate în practică, drept pentru care merită o atenție deosebită.

1. Proprietăți

Proprietățile sunt membrii ai clasei care permit accesarea câmpurilor private, ca și cum ele ar fi declarate public.

Concret, să vedem despre ce e vorba:

using System;


namespace App {  

	class Numar {        
		private int numarIntreg;
		public int NumarIntreg {
			get { return numarIntreg; }
			set { numarIntreg = value; }
		}    
	}    

	class Program {
		public static void Main() {
			Numar obiect = new Numar();
			obiect.NumarIntreg = 19;
			Console.WriteLine(obiect.NumarIntreg);
			Console.ReadKey();
		}
	}

}

Remarcăm cele două clase: Numar si Program. Clasa Numar are un câmp privat, denumit numarIntreg, care, evident, nu poate fi accesat cu ajutorul obiectului, întrucât este declarat private. De aceea, am declarat proprietatea Numar care îndeplinește acest task.

Să vedem care este structura unei proprietăți:

[modificator de acces] [tip de retur] nume_proprietate {
    get { return camp_privat; }
    set {
        // alte instructiuni de protectie
	    camp_privat = value;          
    }
}

get și set se numesc accesori. Sunt metode speciale care: returnează valoarea câmpului privat (get) atunci când proprietatea este utilizată într-o asemenea manieră; modifică valoarea câmpului privat (set), analog.

Cum se utilizează proprietatea:

obiect.nume_proprietate = 19;  // merge pe set, deci setează valoarea câmpului
privatConsole.WriteLine(obiect.nume_proprietate);  // merge pe get, deci accesează valoarea câmpului privat pentru a-l afișa pe ecran

Deci accesorii se apelează în funcție de modul în care este utilizată proprietatea.

value  este o variabilă care reține valoarea pe care trebuie să o atribuim câmpului privat. Mai exact, atunci când utilizăm proprietatea într-o manieră set, spre exemplu obiect.nume_proprietate = 19; , acel 19 trebuie să fie atribuit câmpului privat, dar programatorul poate vrea să verifice dacă acel 19 este o valoare validă pentru câmpul privat (adică dacă acel câmp are o plajă de valori restricționată), de aceea, limbajul dispune de această variabilă specială, denumită value. În câmpul set, programatorul face oricâte prelucrări ale variabilei speciale, până când valoarea ei ajunge la o formă validă.

Un exemplu care ilustrează această situație:

public int NumarIntreg {
	get { return numarIntreg; }
	set { 
		if(value < 10) {
			numarIntreg = value + 1;
		}            
	}        
}

Acesta este nucleul simplificat al proprietăților. Printre practicile recomandate ale programării orientate pe obiect, se numără și încercarea de a menține o încapsulare coerentă a datelor, ceea ce s-ar traduce prin utilizarea cât mai bogată a modificatorului de acces private. De ce? Pentru a micșora șansa de a accesa din greșeală date importante (declarate public), și deci de a genera damage serios în cadrul aplicației (mai ales când vine vorba de aplicații mari).

Proprietățile permit accesul controlat la membrii privați ai unei clase, un lucru deosebit de important atunci când vine vorba de siguranța codului. Programatorul decide nivelul de acces strict raportat la situația în fapt: dacă e vorba de numere, pune pe set o condiție referitoare la numere, etc.

Un tip special de proprietăți este redat de proprietățile automatice  sau proprietăți implementate în mod automat. Aceste proprietăți echivalează cu emularea accesului la un singur câmp privat, numai că ele nu se referă la nicun câmp. Sintaxa este următoarea:

public int NumarIntreg {
    get; set;
}

Se poate include această proprietate într-o clasă fără niciun câmp privat. Compilatorul alocă memorie (atât cât e nevoie) pentru a memora valoarea de tip intreg (în acest caz) într-o zonă de memorie rezervată în mod special.

2. Indexatori

Indexatorii sunt metode speciale, asemănătoare proprietăților, care au capacitatea de a accesa elementele unei colecții private, membră a clasei, dar în mod indexabil, deci cu ajutorul unui index asociat obiectului (instanței de clasă):

using System;


namespace App {

    class indexTest {
        private int[] tablouPrivat = new int[30];
        public int this[int index] {
            get {
                if (index < 30)
                    return tablouPrivat[index];
                else return 0;
            }
            set {
                if (index < 30 && index > 0)
                    tablouPrivat[index] = value;
            }
        }
    }

    class Program {
        public static void Main() {
            indexTest obiect = new indexTest();

            for (int i = 1; i <= 20; i++) {
                obiect[i] = i + 2;
            }

            for (int i = 1; i <= 20; i++) {
                Console.WriteLine(obiect[i]);
            }

            Console.ReadKey();
        }
    }

}

Clasa indexTest are vector privat de numere întregi, și un indexator (așa se numește chestia aia ciudată de sub vector). Remarcăm accesorii get și set de la proprietăți, dar apar acolo mai multe chestii care individualizează un indexator, și anume:

  • au întotdeauna numele this
  • tot ce se întâmplă în corpul accesorilor trebuie să depindă de acel index.
  • se utilizează, de regulă, atunci când clasa are o colecție privată (vector, de regulă, declarat privat)
  • se accesează cu obiectul indexat, după cum se observă (obiect[ i ]).

În Main() – ul exemplului dat, declarăm un obiect de tip testIndex după care, în două for-uri secvențiale setăm valorile din vector astfel: obiect[1] = 3; obiect[2] = 4; … obiect[20] = 22. Evident, valorile se memorează în acel vector, membru al clasei, deci de fapt avem: tablouPrivat[1] = 3; tablouPrivat[2] = 4; … tablouPrivat[20] = 22;

În al doilea for, afișăm aceste valori în consolă, deci se merge pe get.

3. Clase parțiale

Este un subiect scurt, însă merită abordat. Limbajul C# permite definirea de clase parțiale, adică porțiuni de clase, dispuse separat. Acest lucru, practic, premite programatorului să creeze o clasă parțială într-un fișier A, apoi să creeze cealaltă parte a clasei într-un fișier B, după care să adauge ambele fișiere într-un proiect. El va putea astfel să acceseze date din ambele porțiuni ale clasei, chiar dacă ea a fost secționată în două fișiere. Concret:

using System;

namespace App {

    partial class Class {
        public void metodaClass1() {
            Console.WriteLine("Metoda clasa 1");
        }
    }

    partial class Class {
        public void metodaClass2() {
            Console.WriteLine("Metoda clasa 2");
        }
    }

    class Program {
        public static void Main() {
            Class obiect = new Class();

            obiect.metodaClass1();
            obiect.metodaClass2();

            Console.ReadKey();
        }
    }

}

Avem cuvântul cheie partial, care specifică faptul că se declară o porțiune de clasă, și nu o clasă completă. Evident, cealaltă clasă partial are același nume cu prima, de unde deducem că ea este de fapt o completare a clasei definite la început.

În Main(), observăm funcționalitatea, și anume faptul că metodele definite în părți diferite de clasă pot fi apelate. Se afișază secvențial cele două mesaje care se observă.

4. Supraîncărcarea operatorilor

Supraîncărcarea operatorilor este o facilitate foarte importantă a limbajelor orientate pe obiect. Practic, operatorii predefiniți în limbaj, pot căpăta noi funcționalități, adaptate obiectelor create de programator.

Mai exact: avem o clasă care memorează numere complexe (parte reală, parte imaginară, etc.) și vrem să facem într-un fel să adunăm două numere din astea. Nu putem să facem direct: numărComplex1 + numărComplex2, pentru că cei care au creat limbajul au definit operatorul + pentru operanzi care au tipuri predefinite, precum: int + int, double + double, etc. ei nu au definit acest operator și pentru operanzi de tip complex + complex, unde complex este tipul definit de noi.

De aceea noi trebuie să facem asta: așa cum cei care au definit limbajul au făcut posibilă scrierea int + int, așa o să facem și noi posibil complex + complex.

Mai întâi să vedem cine e acest complex:

class complex {
    // consideram numarul complex de forma algebrica z = x + y * i
    public int x;
    public int y;
}

Evident, este o clasă care are două câmpuri ce corespund componentelor unui număr complex: partea imaginară și partea reală.

Pentru a realiza adaptarea operatorului ”+ ” la adunarea a două numere complexe, trebuie să scriem o funcție specială, așa cum observăm aici:

class complex {
    // consideram numarul complex de forma algebrica z = x + y * i
    public int x;
    public int y;

    public static complex operator+ (complex numarComplex1, complex numarComplex2) {
        complex suma = new complex();

        suma.x = numarComplex1.x + numarComplex2.x;
        suma.y = numarComplex1.y + numarComplex2.y;

        return suma;
    }
}

Să descriem această funcție: are modificator de acces public, are keyword-ul static, are tip de retur complex , are keyword-ul special operator  urmat de operatorul efectiv pe care îl supraîncărcăm, în acest caz +. Funcția acceptă ca parametrii două obiecte de tip complex, numarComplex1 și numarComplex2. Acești parametrii sunt de fapt operanzii, și anume numerele complexe care se adună (numarComplex1 + numarComplex2). În cadrul corpului funcției vom face adunarea efectivă a lor, adică parte reală cu parte reală și parte imaginară cu parte imaginară.

Stocăm această sumă într-o a treia variabilă de tip complex, și o returnăm, ca efect al sumei celor două numere complexe.

Să vedem funcționalitatea:

public static void Main() {
    complex z, w, s;

    z = new complex();
    w = new complex();
    s = new complex();

    z.x = 2; z.y = -2;
    w.x = 8; w.y = 1;

    s = z + w;

    Console.WriteLine("S = {0} + ({1} * i)", s.x.ToString(), s.y.ToString());
    Console.ReadKey();
}

Declarăm 3 variabile de tip complex și le inițializăm (practic formăm un număr complex efectiv). Facem suma celor două numere, moment în care intră în scenă acea funcție definită mai sus, apoi afișăm formatat suma. Metoda ToString() convertește la string numerele întregi s.x și s.y. Este necesară această conversie întrucât se afișează numai string-uri, în afișarea formatată.

În aceeași manieră, definim următoarea funcție care testează egalitatea a două numere complexe:

public static bool operator == (complex numarComplex1, complex numarComplex2) {
    if (numarComplex1.x != numarComplex2.x)
        return false;
    else if (numarComplex1.y != numarComplex2.y)
        return false;
    else return true;
}

La fel, funcția este declarată public static, returnează o valoare booleană, adică adevărat sau fals, care corespunde condiției de egalitate a celor două numere complexe. În corpul funcției, testăm dacă părțile reale și cele imaginare sunt egale (returnăm true), în caz contrar returnăm false.

Nu este ceva foarte complicat, trebuie doar gândită necesitatea utilizării acestor elemente și adaptarea la sintaxă.

Comparativ cu C++, supraîncărcarea operatorilor în C# este un task foarte ușor de elaborat datorită faptului că se realizează prin funcții membru, iar operanzii sunt foarte clar de observat, fiind simpli parametrii ai funcției. În C++ există două metode de supraîncărcare a operatorilor – prin funcții friend și prin funcții membru. Eu am lucrat doar cu a doua variantă și pot spune că este simliară într-o oarecare măsură cu varianta în C#, numai că funcției i se transmite ca parametru doar al doilea operand (cum ar fi la noi doar numarComplex2), primul operand este pointerul la obiectul curent (și anume this).

Pentru cei interesați, propun următoarele subiecte:

  1. Clase interioare.
  2. Supraîncărcarea operatorilor unari.
  3. Lista operatorilor ce pot fi supraîncărcați.

Sper că m-am făcut înțeles.

Respectfully, Ioniță Cosmin.

Leave a comment

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

Send this to a friend