Caracteristicile limbajului C#




Aşa cum am mai spus, limbajul C# este un limbaj simplu, modern, orientat pe obiecte conceput pentru a dezvolta aplicaţii care să ruleze pe platforma .Net Framework. Combină productivitatea limbajelor de dezvoltare rapidă a aplicaţiilor cu puterea brută a limbajului C++.
Codul C# este compilat ca şi cod de bază, ceea ce înseamnă că beneficiază de serviciile CLR. Aceste servicii includ interoperabilitea limbajelor, garbage collection, securitate ridicată şi suportul versiunilor imbunătăţite.
În cele ce urmează voi descrie câteva caracteristici ale limbajului.
3.4.1. Tipuri de date

În .NET (şi, implicit, în C#), tipurile de date se împart în două categorii principale: tipuri valoare şi tipuri referinţă. Diferenţa dintre ele este că variabilele de tip referinţă conţin referinţe (pointeri) spre datele propriu-zise, care se află în heap, pe când variabilele de tip valoare conţin valorile efective. Această deosebire se observă, de exemplu, la atribuiri sau la apeluri de funcţii.
La o atribuire care implică tipuri referinţă, referinţa spre un obiect din memorie este duplicată, dar obiectul în sine este unul singur (are loc fenomenul de aliasing - mai multe nume pentru acelaşi obiect). La o atribuire care implică tipuri valoare, conţinutul variabilei este duplicat în variabila destinaţie. Tipurile valoare sunt structurile (struct) şi enumerările (enum). Tipuri referinţă sunt clasele (class), interfeţele (interface), tablourile şi delegările (delegate).



Poate părea surprinzător, dar unele din tipurile \"primitive\" cu care suntem obişnuiţi din C (int, de exemplu), sunt tot structuri. Altele, cum ar fi string, sunt clase. Singurul lucru care le face mai speciale în C# este faptul că, la compilare, textul din codul sursă este identificat şi convertit automat în instanţe ale acestor tipuri. În plus, pentru aceste tipuri, există cuvinte cheie C# prin care sunt descrise, dar şi clase .NET care reprezintă implementarea propriu-zisa.

3.4.2. Tablouri

Un tablou este o structură de date care conţine un număr de variabile numite elemente, accesate prin intermediul unuia sau mai multor indici întregi. Tablourile sunt tipuri referinţă, iar pentru a crea un tablou trebuie alocat explicit spaţiu cu operatorul new. În acest caz, un tablou este efectiv creat, iar valorile elementelor sunt iniţializate cu valoarea implicită a tipului. Pentru a crea un tablou de obiecte (tipuri referinţă), trebuie creat întâi tabloul, elementele acestuia vor fi iniţializate implicit cu null, după care tabloul trebuie parcurs şi elementele sale iniţializate explicit de către programator cu valori diferite de null.

3.4.3.Clase

O clasă este o structură de date care poate conţine date (variabile şi constante) şi cod (metode, proprietăţi, evenimente, operatori, constructori şi destructori). Clasele pot fi derivate din alte clase, prin acest mecanism clasele derivate putând extinde, specializa sau modifica comportamentul clasei de bază.
Clasele reprezintă piatra de temelie a programării pe obiecte. Acest concept porneşte de la aserţiunea că datele sunt strâns legate de codul care le prelucrează şi că gruparea lor într-o structură comună (spre deosebire de limbajele procedurale, în care datele sunt separate de cod) duce la o proiectare, implementare, testare şi întreţinere mai simplă.
Clasele sunt tipuri referinţă, ceea ce înseamnă că datele membru ale instanţelor se află în heap şi programul are acces la ele prin referinţe. Referinţele sunt păstrate corespunzător de runtime şi în cazul în care nu mai există referinţe spre o instanţă, memoria folosită de aceasta este recuperată automat.
Sunt eliminate astfel o serie întreagă de defecte software legate de dealocarea memoriei - în C++ pot apărea situaţii în care memoria nu este dealocată şi este pierdută pâna la oprirea programului sau situaţii în care memoria este dealocată de o componentă dar altă componentă încearcă ulterior să scrie la acea adresă pentru că are un pointer pe care îl crede valid.
Membrii unei clase pot fi statici; în acest caz, există o singură instanţă a lor care este accesibilă din toate instanţele clasei respective; în situaţia în care membri statici sunt şi publici, ei vor fi accesibili din alte clase fără a avea nevoie de o instanţă a clasei respective. Mai mult, ei nu pot fi accesaţi prin intermediul unor instanţe, ci numai folosind numele clasei.
De asemenea, pot fi definite metode statice. În mod similar, acestea aparţin clasei şi nu instanţelor acesteia şi pot fi apelate folosind numele clasei. În general, acţionează asupra membrilor statici sau efectuează operaţii pentru care nu este necesară o instanţă a clasei.

3.4.4. Drepturi de acces

Încapsularea reprezintă un aspect esenţial al programării pe obiecte şi înseamnă, pe scurt, posibilitatea de a separa implementarea unui obiect de funcţionalitatea pe care acesta o pune la dispoziţie utilizatorilor săi. Prin implementarea unui obiect ne referim, în general, la variabilele membru ale obiectului respectiv şi la metodele interne, care contribuie la realizarea funcţionalităţii obiectului, dar care nu sunt necesare utilizatorilor obiectului respectiv.
Drepturile de acces reprezintă modalitatea prin care putem ascunde de utilizatorii clasei detaliile de implementare ale acesteia. Cei cinci specificatori de acces în C# şi semnificaţia lor sunt:
-private - Membrii privaţi sunt accesibili numai din clasa în care sunt declaraţi.
-internal - Membrii interni sunt accesibili numai din clase care fac parte din acelaşi assembly cu clasa în care sunt declaraţi.
-protected - Membrii protejaţi sunt accesibili din clasa în care sunt declaraţi şi din clasele derivate din aceasta.
-protected internal - Membrii interni şi protejaţi sunt accesibili din clasa în care sunt declaraţi, din clasele care fac parte din acelaşi assembly şi din clasele derivate, indiferent în ce assembly sunt acestea.
-public - Membrii publici sunt accesibili din orice clasă.

Dacă specificatorul de acces lipseşte, membrii unei clase sunt implicit privaţi.

3.4.5. Date membru

O clasă este alcatuită din membrii moşteniţi din clasa de bază şi din membrii declaraţi în corpul clasei. Datele pot fi variabile, constante, sau variabile read-only. Diferenta dintre constante si variabile read-only este ca valoarea constantelor poate fi calculata la compilare si plasata în fisierul executabil ca atare, pe când valoarea variabilelor read-only este calculata la rulare. Implicit, pot fi declarate doar constante apartinând tipurilor recunoscute de compilator ca "primitive". Variabilele read-only pot fi de orice tip si pot avea valori care nu sunt cunoscute la compilare.

3.4.6. Constructori

Constructorul este o metodă care implementează acţiunile care au loc la crearea şi iniţializarea unei instanţe a clasei. Are acelaşi nume ca şi clasa din care face parte şi nu returnează nici o valoare, nici măcar void. Poate apela un alt constructor al aceleiaşi clase sau un constructor al clasei de baza. Poate avea orice nivel de acces.
Dacă într-o clasă nu este implementat nici un constructor, compilatorul inserează automat un constructor public, fără argumente, care iniţializează automat toate variabilele cu valorile lor implicite.


3.4.7. Moştenire

Prin moştenire se înţelege crearea unei clase derivate care conţine implicit toţi membrii (mai puţin constructorii, constructorul static şi destructorul) unei alte clase, numite de bază. Moştenirea se mai numeşte şi derivare sau, mai rar, specializare.
În C#, moştenirea se realizeaza punând ":" după numele clasei, urmat de numele clasei de bază. O clasă poate avea o singură clasă de bază. Dacă o clasă nu este derivată explicit din nici o clasă, compilatorul o face implicit să fie derivată din object. Object este rădăcina ierarhiei de clase din .NET.
Moştenirea este tranzitivă, în sensul că dacă A este derivată din B şi B este derivată din C, implicit A va conţine şi membrii lui C (şi, evident, pe cei ai lui B). Prin moştenire, o clasă derivată extinde clasa de bază. Clasa derivată poate adăuga noi membri, dar nu îi poate elimina pe cei existenţi.
Deşi clasa derivată conţine implicit toţi membrii clasei de bază, acest fapt nu înseamnă ca îi şi poate accesa. Membrii privaţi ai clasei de baza există şi în clasa derivată, dar nu pot fi accesaţi. În acest fel, clasa de bază îşi poate schimba la nevoie implementarea internă fără a distruge funcţionalitatea claselor derivate existente.
O referinţă la clasa derivată poate fi tratată ca o referintă la clasa de bază. Aceasta conversie se numeşte upcast, din cauză că în reprezentările ierarhiilor de clase, clasele de bază se pun deasupra, cele derivate dedesubtul lor, ca într-un arbore generalizat. Prin upcast se urcă în arbore.
Conversia inversă, de la clasa de bază la cea derivată, se numeşte downcast şi trebuie făcută explicit, deoarece nu ştim dacă referinţa indică spre un obiect din clasa de bază, spre un obiect din clasa derivată la care încercăm să facem conversia sau spre un obiect al altei clase derivate din clasa noastră de bază.
Accesibilitatea trebuie sa fie consistentă şi în cazul în care încercăm să derivăm o clasă din alta. Clasa de bază trebuie sa fie cel puţin la fel de accesibilă ca şi clasa derivată din ea.
O clasă derivată poate ascunde membri ai clasei de bază, declarând membri cu aceeaşi semnătură. Prin aceasta, membrii clasei de bază nu sunt eliminaţi, ci devin inaccesibili prin referinţe la clasa derivată. Ascunderea membrilor se face folosind cuvântul cheie new. Acest cuvânt cheie are rolul de a-l obliga pe programator să-şi declare explicit intenţiile şi face, în acest fel, codul mai lizibil.
3.4.8. Metode
3.4.8.1. Supraîncărcarea metodelor
În C#, este posibil, ca şi în C++ sau Java să avem mai multe metode cu acelaşi nume în cadrul aceleiaşi clase. În C#, la supraîncarcare se ţine cont de numele metodei, de tipul argumentelor, de categoria lor (de intrare, intrare-iesire, iesire), de numărul şi de ordinea lor. Semnătura unei metode nu include tipul returnat, numele argumentelor sau argumentele variabile de la sfârşitul listei de argumente.
Supraîncărcarea metodelor şi operatorilor se mai numeşte şi polimorfism static, deoarece putem obţine comportamente diferite prin acelaşi apel de funcţie, dar comportamentul este totuşi bine determinat la compilare.
3.4.8.2. Metode cu numar variabil de argumente
În C#, este posibil ca o metoda să primească un număr variabil de argumente. Argumentele variabile sunt transmise într-un tablou, iar numele acestui tablou trebuie precedat în declaraţia funcţiei de cuvântul cheie params.
3.4.8.3. Metode virtuale şi polimorfism
Polimorfismul este a treia piatră de temelie a programării pe obiecte. Dacă încapsularea afectează în mod direct uşurinţa de implementare şi utilizare a claselor prin separarea implementării de funcţionalitate, iar moştenirea reprezintă o modalitate simplă de reutilizare a codului, polimorfismul reprezintă componenta esenţiala când vine vorba despre extensibilitate.
O metodă virtuală este o metodă care poate fi suprascrisă într-o clasă derivată.

3.4.9. Proprietăţi

Proprietăţile sunt membri care oferă acces la atributele unui obiect. Exemple de proprietăţi includ lungimea unui şir de caractere, lungimea unui tablou, mărimea unui font, titlul unei ferestre şamd. Proprietăţile pot părea sinonime cu variabilele membru ale unei clase - au nume, au tip şi sunt folosite în mod similar de utilizatorii clasei, cu aceeaşi sintaxă. Cu toate acestea, proprietăţile diferă de variabilele membru prin faptul că nu reprezintă spaţii de memorie, ci funcţii de acces.
Aceste funcţii de acces sunt executate când utilizatorii clasei încearcă să obţină sau să modifice aceste atribute. Ele sunt similare cu modelul get/set din alte limbaje de programare (aşa şi sunt implementate intern), dar oferă o modalitate mai coerenta de lucru împreună cu aceleaşi avantaje - posibilitatea de a valida argumentele furnizate la modificarea atributelor sau de a anunţa alte obiecte despre modificările apărute, de exemplu.

3.4.10. Destructori

În .NET, memoria ocupată de obiecte este automat recuperata de un garbage collector în momentul când nu mai este folosită. Uneori, însă, un obiect este asociat cu resurse care nu depind de .NET şi care trebuie dealocate explicit, într-un fel sau altul. Un exemplu de astfel de resurse îl reprezintă conexiunile TCP/IP, care nu pot fi lăsate la voia întâmplarii.
De obicei, este bine să se elibereze astfel de resurse în momentul când nu mai sunt necesare şi vom vedea puţin mai încolo cum se face acest lucru. Există însă şi o plasă de siguranţă oferită de compilator, reprezentată de destructori.
Destructorii sunt metode care au acelaşi nume cu clasă din care fac parte, precedat de semnul ~. Nu au drepturi de acces, nu au argumente şi nu permit nici un fel de specificatori (static, virtual samd). Nu pot fi invocaţi explicit, ci numai de librariile .NET specializate pe recuperarea memoriei. Ordinea şi momentul în care sunt apelaţi sunt nedefinite, ca şi firul de execuţie în care sunt executaţi. Este bine ca în aceste metode să dealocaţi numai obiectele care nu pot fi dealocate automat de .NET şi să nu faceţi nici un fel de alte operaţii.