Tutorial C#
Tutorial C#
De ce a fost nevoie de namespaces? Practic, programele mari sunt impartite in module si sunt
dezvoltate separat de mai multe persoane. Din acest motiv, exista posibilitatea de a aparea
identificatori cu acelasi nume. Solutia la aceasta problema a fost introducerea cuvantului cheie
namespace, care, in teorie, se defineste ca o regiune declarativa ce ofera o modalitate de a separa
un set de nume de un altul. In concluzie, numele din cadrul unui spatiu nu va intra in conflict cu
acelasi nume declarat in alt spatiu.
Concret, am definita o clasa cu numele Exemplu. Aceasta ar putea intra in conflict cu o alta
clasa Exemplu folosita de programul nostru si care este dezvoltata de o alta persona. Folosind
spatiile de nume, evitam acest tip de problema. Forma generala pentru declararea unui spatiu de
nume este :
namespace Nume
{
//membri
Nume reprezinta numele spatiului. Toate entitatile definite in interiorul spatiului fac parte din
domeniul de valabilitate al acelui spatiu de nume ( se pot adauga clase, structuri, delegari,
enumerari, interfete si surpriza! chiar un alt spatiu de nume ).
Intr-un fisier sursa, se pot adauga oricat de multe spatii de nume, iar fiecare spatiu poate
contine oricat de multe entitati.
namespace A
{
public class Exemplu
{
//proprietate
public string Mesaj
{
get;
set;
}
}
}
Clasa Exemplu, care contine o proprietate numita Mesaj, este definita in domeniul de
valabilitate al spatiului de nume A.
Atunci cand e nevoie de crearea unei instanta a clasei, numele clasei trebuie sa fie calificat
cu numele spatiului. Acest mod este cunoscut ca “fully qualified name“.
A.Exemplu ex;
ex = new A.Exemplu();
Nu mai este necesar sa calific cu numele spatiului obiectul sau pe oricare din membrii sai. In
cazul nostru, proprietatea Mesaj, poate fi apelata direct.
ex.Mesaj;
Normal, situatia va deveni dificila cand programul va include referinte mai multe catre un
spatiu de nume si va trebui specificat numele de fiecare data. Solutia vine de la directiva using,
care va asigura vizibilitatea spatiului de nume.
using A;
Exemplu ex;
ex = new Exemplu();
Compilatorul va cauta in mod automat in spatiul de nume A si in cazul in care va gasi clasa
Exemplu, o va folosi.
In cazul in care va gasi aceeasi clasa in doua spatii de nume diferite, va trebui specificata clasa si
spatiul de nume dorit.
Acest lucru ajuta la divizarea in parti mai mici a codului pentru un mod de lucru mai organizat si
mai eficient.
Pentru a instantia clasa ExempluA1, trebuie neaparat sa folosesc modul “fully qualified name”.
Folosirea “fully qualified name” nu pare foarte eficienta in scrierea codului , dar este
recomandata pentru usurinta identificarii spatiilor de nume, mai ales in lucru in echipa.
Pot exista mai multe declaratii namespace cu acelasi nume. Acest lucru permite distribuirea unui
spatiu de nume in mai multe fisiere sau chiar separarea sa in cadrul aceluiasi fisier.
namespace B
{
public class Student
{
//
}
}
namespace B
{
public class Profesor
{
//
}
}
Un exemplu de spatiu de nume folosit pe care il foloseste biblioteca arhitecturii .Net (implicit,
biblioteca limbajului c#) este System. Astfel, la inceputul oricarui program, este inclusa linia de
cod:
using System;
Acest spatiu reprezinta radacina pentru toate spatiile de nume din .Net. De asemea, el contine
si tipurile de date. De exemplu, se poate declara si initializa o variabila de tip int, astfel:
System.Int16 numar = 0;
Important de stiut este faptul ca acest spatiu contine clasa Object, care este radacina intregii
ierarhii de mostenire din Framework. Daca se defineste o clasa fara a se specifica in mod explicit
faptul ca aceasta mosteneste o anumita clasa, clasa Object va fi clasa implicita de baza. Ea
furnizeaza toate metodele si proprietatile pe care toate obiectele trebuie sa le suporte.
Exista multe alte spatii de nume subordonate lui System, care contin alte parti ale bibliotecii
limbajului c#.
O lista cu cele mai utilizate spatii de nume nu poate fi stabilita exact, dar printre cele mai
cunoscute sunt:
System.IO
Spatiul Input Output contine clase folosite pentru operatiile de intrare-iesire cu fisiere. De
exemplu, clasele File, StreamWriter, BinaryReader, BinaryWriter au functii ce folosesc la
accesarea unui fisier de pe disc. Acest namespace include si clase folosite in manipularea datelor
aflate in memoria aplicatiei: MemoryStream. Tot in acest namespace sunt definite clase folosite
in manipularea fisierelor si directorilor: DirectoryInfo, DriveInfo, FileInfo.
In acest namespace sunt definiti 3 delegates: ErrorEventHandler, FileSystemEventHandler si
RenamedEventHandler.
System.Collection
Permite dezvoltatorilor sa lucreze usor cu grupurile de obiecte. Exista colectii utile pentru
cresterea performantei in diferite scenarii (ArrayList, Queue, Stack, etc) si exista Dictionaries
(Hashtable, SortedList, StringDictionary, etc) cu principiul cheie-valoare.
System.Data
Pentru orice operatie cu o sursa de date externa, trebuie stabilita o conexiune cu acea sursa.
Acest spatiu de nume contine clase in mai multe subspatii (SqlClient, OleDb, etc) responsabile
cu: date referitoare la sursa de date, functii pentru deschiderea / inchiderea conexiunii, tranzactii
si multe altele.
System.Text
Contine clase care permit codarea si decodarea caracterelor, Encoder, Decoder. Foarte
utilizate sunt si sub-spatiul System.Text.RegularExpressions si clasa StringBuilder.
System.Diagnostics
E un spatiu de nume foarte util in “depanarea” aplicatiei (debug). Are clasa EventLog ce
permite interactiunea cu event log din Windows, clasa Process care furnizeaza o functionalitate
pentru monitorizarea proceselor sistem din retea, pornirea si oprirea proceselor sistem locale.
Clasa PerformanceCounter pentru monitorizarea performantei sistemului.
Tipuri de date
Tipurile de date si operatorii stau la baza oricarui limbaj de programare. Ele stabilesc limitele
unui limbaj si determina tipurile de activitati pentru care poate fi utilizat.
Un program opereaza cu date. Daca un limbaj de programare nu furnizeaza un mod pentru
stocarea datelor, atunci el este inutil.
O variabila este o locatie de memorie cu nume, careia ii poate fi stocata o valoare. Valoare se
poate modifica pe parcursul executarii programului. Nu exista conceptul de variabila fara “tip”
pentru ca tipul unei valori determina in mod exact operatiile permise asupra sa (nu toate
operatiile sunt permise asupra tuturor tipurilor).
O constanta (literal), spre deosebire de variabila, desemneaza o valoare fixa. Pentru fiecare
tip de data, C# are un mod de declarare.
De ce tipurile de date sunt importante ?
C# este un limbaj puternic tipizat. Asta inseamna ca pentru toate operatiile, compilatorul
efectueaza verificari privind compatibilitatea tipurilor. Aceste verificari sunt necesare pentru
prevenirea erorilor si cresterea fiabilitatii programelor.
Pentru urmatoarele exemple, o sa adoptam urmatoarea conventie de scriere, numita conventia
Pascal : in cazul numelor compuse din mai multe cuvinte, fiecare cuvant este scris cu majuscula
(ex, AnFabricatie) – valabila pentru numele claselor, metodelor, proprietatilor, enumerarilor,
interfetelor, spatiilor de nume. In cazul variabilelor, primul cuvant incepe cu minuscula
(variabila anFabricatie).
Tipuri valorice in C#
I – tip valoare.
In cazul acestor tipuri de date, atunci cand se declara o variabila, va fi nevoie si de alocare de
spatiu pentru ea. Initial,variabilele contin valoarea implicita specifica tipului. Cand se face
atribuirea, are loc o copiere a datelor in variabila destinatie care nu mai este legata de variabila
initiala (transmitere prin valoare, ”value semantics”).
using System;
using System.Collections.Generic;
using System.Text;
namespace ExempluTipuriValoare
{
public struct Masina
{
//variabila instanta
public int anFabricatie;
}
class Program
{
static void Main(string[] args)
{
//creez obiectul masina, numit Seat
Masina seat = new Masina();
//atribui o valoare variabilei an a instantei seat
seat.anFabricatie = 2009;
Masina opel = seat;
// se initializeaza prin copiere variabila sb
Console.WriteLine("Masina Seat este din anul {0}.",
seat.anFabricatie);
Console.WriteLine("Masina Opel este din anul {0} prin
initializare.", opel.anFabricatie);
//schimb valoarea variabile an a instantei seat
seat.anFabricatie = 2010;
Console.WriteLine("Masina Seat este din anul {0}.",
seat.anFabricatie);
//valoare variabilei an a instantei opel ramane aceeasi
Console.WriteLine("Masina Opel este din anul este {0}.",
opel.anFabricatie);
Console.ReadLine();
}
}
}
II – tip referinta.
In cazul acestor tipuri de date, la declararea unei variabile nu are loc automat alocarea de
spatiu. Initial, referintele sunt null. E nevoie de alocare explicita de memorie pentru obiectele
propriu-zise, iar la atribuire este copiata referinta in destinatie, obiectul spre care indica
ramanand acelasi (“aliasing”, “reference semantics”).
Console.WriteLine(seat.anFabricatie);
Console.WriteLine(opel.anFabricatie);
Console.Read();
Sistemul de operare si Common Language Runtime impart memoria folosita pentru stocarea
datelor in memorie stiva (stack) si memorie heap, fiecare functionand in mod diferit.
Memoria stack are o structura de tip FIFO (first in, last out) si este foarte eficienta. Ca un
exemplu practicStiva retine variabilele de tip valoare.
Heap retine variabilele create dinamic. Ca avantaj este faptul ca obiectele pot fi alocate si
sterse intr-o ordine aleatoare. Pentru a putea detine mai mult control si ordine, memoria heap are
nevoie de un “memory manager” si un “garbage collector”.
Tipurile de date reprezinta un subiect foarte vast. Vom continua cu tipurile valorice
fundamentale in C#.
Limbajul C# include doua categorii de tipuri de date : tipuri valorice si tipuri referinta.
Pentru ca C# respecta un domeniu de valori, iar fiecare tip valoric are un comportament,
limbajul asigura portabilitate. De exemplu, o variabila declarata de tip int, va ramane tot de tip
int, indiferent de mediul de executie. Asta ne ajuta la evitarea rescrierii codului pentru adaptarea
programului la o anumita platforma.
Tipuri numerice
Intregi
In C# sunt definite 9 tipuri de intregi : char, byte, sbyte, short, ushort, int, uint, long,
ulong. Cu exceptia lui char, toate sunt folosite la calcule numerice.
Variabilele de tip int se utilizeaza in controlul buclelor, indexarea tablourilor, aritmetica
normala cu numere intregi. Se recomanda ca atunci cand e nevoie de o valoare ( fara semn ) care
depaseste domeniul lui int,sa se foloseasca uint. In cazul valorilor mari este recomandat long, iar
la cele fara semn, ulong.
//declararea unui int cu valoare implicita 0
int aria;
//declarare si initializare
int lungime = 40;
//CALCUL ARIA
aria = latime * lungime;
In virgula mobila
Tipurile in virgula mobila, se utilizeaza pentru reprezentarea numerelor care au parte
fractionara. Exista doua tipuri : float, double. Acesta din urma, este cel mai intrebuintat.
//declarea si initializare
double mediaSemUnu = 8.45;
//CALCUL MEDIA
mediaAn = (mediaSemUnu + mediaSemDoi) / 2;
Decimal
Tipul decimal este folosit in calculele monetare. El are avantajul ca elimina erorile de
rotunjire atunci cand se lucreaza cu valori fractionare, pentru ca poate reprezenta in mod precis
pana la 28 de pozitii zecimale. Tipul decimal nu exista in C, C++, Java.
dobanda = 0.1m;
Tipuri Bool
Tipul bool retine valorile de adevar ( “true” ) si fals ( “false” ). Orice expresie de tipul bool
va lua una din aceste valori. Nu se poate converti. Teora
bool a;
a = false;
Console.WriteLine("a este {0}", a);
bool b;
b = true;
Console.WriteLine("b este {0}", b);
Caractere
In C#, pentru caractere, se utilizeaza modelul Unicode.
ch = 'b';
//afisarea valorii din standardul UNICODE pentru un caracter
//are loc conversia catre un int
Console.WriteLine("Valoarea lui {0} in standardul UNICODE este
{1}",ch,(int)ch);
Este asemantor cu cel din C++. Pentru ca este un tip valoric, va fi stocata in stiva. Atat timp
cat structura este rezonabila in ceea ce priveste marimea, nu vor fi probleme cu administrarea
memoriei.
O structura poate contine declaratii de constante, campuri, metode, proprietati, indexatori,
operatori, constructori.
Cele mai simple structuri, sunt System.Int32, System.Int64, System.Single, etc, pentru tipurile
de baza int, long, float.
struct Persoana
{
Tipul enumerare se defineste de programator si se aseamana cu cel din C++. Acest tip permite
folosirea numelor carora li se atribuie o valoare.
Este recomandat sa declaram si sa definim o enumerare direct in interiorul unui spatiu de
nume, pentru a putea fi folosita de toate clasele.
namespace tipulEnum
{
enum ZileSaptamana
{
Luni = 1,
Marti,
Miercuri,
Joi,
Vineri,
Sambata,
Duminica
}
}
Pentru a converti un tip string, de exemplu avem numele zilei, in Enum, se procedeaza in felul
urmator:
Observatii:
Implicit, valoare primului membru din enumerare este 0. Pentru un exemplu simplu si practic,
initializat variabila ‘Luni’ cu 1; Apoi, fiecare variabila care va urma va avea o valoare implicita
mai mare decat precendenta, cu o unitate.
Daca nu foloseam conversia in exemplu ( int ), rezultatul ar fi aratat numele elementului.
Tipurile enum sunt derivate din clasa System.Enum, derivata din System.ValueType.
Tipurile nullable
Sunt tipuri valoare pentru care se pot memora valori posibile din aria tipurilor de baza,
inclusiv valoare null.
Valoarea unei variabile contine valoarea implicita a tipului, daca nu este initializata explicit.
Exista cazuri, in care se doreste ca valoarea implicita a variabilei sa nu fie definita.
Nullable<int> a ;
Un alt mod de a declara o variabila de acest tip este: int? a = null; iar verificarea se poate face in
felul urmator: Console.WriteLine(“valoarea variabilei este {0}”, a ?? “null”);
Tipuri repetitive
In limbajul c# exista mai multe posibilitati de a executa in mod repetat anumite blocuri de cod
(ciclu). Aceste posibilitati sunt:
for
while
do…while
foreach
Aceste instructiuni sunt specifice multor limbaje si intelegerea folosiri lor ar trebui sa fie usoara
pentru oriceine le-a mai folosit cu alte limbaje.
Cel mai simplu ciclu este ciclul for. Sintaxa generala este:
for (; ; )
{
// ...
}
care este un ciclu infinit. Acest bloc de cod ar trebui intrerupt (printr-o intructiune break, ca in
exemplul urmator:
int i = 0;
for (; ; )
{
// ...
if(i++ == 5) break; //intrerupe ciclul
}
O alta modalitate de a scrie un bloc ce se repeta la infinit este cu ajutorul keyword-ului while:
int i = 0;
while(true)
{
// ...
if(i++ == 5) break; //intrerupe ciclul
}
Acest bloc de cod este echivalent (functional) cu cel anterior (folosinf for).
Instructiunile do…while sunt o alta metoda de a executa inmod repetat anumite linii de cod:
int x = 0;
do
{
Console.WriteLine(x);
x++;
} while (x < 5);
Diferenta dintre un bloc while si un bloc do…while este ca instructiunile din do…while se
executa cel putin o data – conditia de iesire din loop se verifica dupa cel putin o executie. Codul
dintr-un ciclu while poate sa nu fie executat niciodata.
De exemplu:
int x = int.Parse(Console.ReadLine());
while (x % 5 != 0)
{
Console.WriteLine(x++);
}
Daca userul introduce un numar multiplu de 5, codul blocului while nu se executa nici macar o
data.
Am lasat la final foreach, deoarece este o metoda de a executa in mod repetitiv anumite operatii,
insa putin diferit de modurile cum lucreaza blocurile descrise anteorior. foreach executa blocul
de comenzi asociat o singura data pentru fiecare element dintr-un array sau colectie de obiecte –
conditia ca foreach sa poata fi aplicat colectiei este ca acea colectie sa implementeze interfata
IEnumerable (sau IEnumerable<T>).
Acest exemplu va afisa perechi de tipul n -> 2n pentru fiecare numar din array-ul de int folosit.
O conversie permite ca o expresie de un anumit tip, sa fie tratata ca o expresie de un alt tip.
Conversii implicite
Conversia implicita se efectueaza automat doar daca nu este afectata valoarea convertita ( tipul
destinatie este mai cuprinzator decat tipul sursa ) si cele doua tipuri sunt compatibile. Daca sunt
indeplinite cele doua conditii, atunci valoarea din partea din dreapta este convertita la tipul din
partea stanga.
int i;
float f;
i = 10;
Tipul float este suficient de cuprinzator pentru a memora orice valoare de tip int(conversie prin
largire ).
Chiar daca se poate efectua conversia automata din int in float, invers nu se poate efectua
automat.
int i;
float f;
f = 30.4F;
i = 10;
Eroare la compilare: “Cannot implicitly convert type ‘float’ to ‘int’. An explicit conversion exists
(are you missing a cast?)“
Conversii explicite
Pentru a rezolva eroare de mai sus, este nevoie de folosirea unui cast (o directiva catre
compilator pentru convertirea unui tip in altul).
(tip-tinta) expr;
int i;
float f;
f = 30.4F;
Console.WriteLine("Valoarea inainte de conversie {0} ", f);
//conversie cu trunchiere
i = (int)f;
Console.WriteLine("Valoarea dupa conversia de la float la int este {0} ", i);
Se observa ca partea fractionara se pierde, atunci cand valoarea in virgula mobila este
convertita la un tip intreg ( conversie prin restrangere ).
char ch;
byte b;
//codul ASCII pentru X
b = 88;
//cast intre tipuri incompatibile
ch = (char)b;
Console.WriteLine("ch : " + ch);
Una din cele mai intalnite conversii in programare este cea din tipul numeric in sir de
caractere. Aceasta se realizeaza cu metoda ToString din clasa Object.
int a = 2009; string s = a.ToString(); Console.WriteLine("{0} este {1}", a, a.GetType());
Console.WriteLine("{0} este {1}",s, s.GetType());
La fel de folosita este si conversia din caractere in numar, care se realizeaza cu metoda Parse
din clasa Object.
Daca sirul de caractere nu reprezinta un numar valid, conversia sirului la numar va esua.
int a = 2009;
string s = a.ToString();
Boxing (“impachetare”) permite ca tipurile valoare sa fie tratate ca obiecte. Boxing este o
conversie implicita. “Impachetarea” unei valori a unui tip valoare se realizeaza prin alocarea unui
obiect si copierea valorii in obiectul nou creat.
Rezultatul va fi crearea unui obiect 0, pe stiva,care va fi referinta catre o valoare de tip int, pe
stiva. Valoarea este o copie a valorii asignate variabilei a.
//schimba valoarea
a = 2010;
“Unboxing” permite o conversie explicita de la tipul obiect la orice alta valoare de tip valoare.
O operatie de unboxing consta in verificarea faptului ca obiectul instantiaza o valoare “boxed” a
unui tip vlaoare, apoi copiaza valoare catre instanta.
int a = 2009;
//boxing
object o = a;
int j = 0;
//unboxing
j = (int)o;
O alta conversie pe care vreau sa o mentionez este cea din Enum in int si invers.
Daca avem enumeratia prezentata in exemplul din postarea anterioara, Tipuri de date in limbajul
c# – tipuri valorice, ZileSaptamana putem scrie:
int luni = (int)tipulEnum.ZileSaptamana.Luni; //luni va fi egal cu 1
Tipurile referinta permit accesul la datele necesare din locuri diferite ale programului.
Declararea tipurilor referinta nu implica automat alocarea de spatiu, ca in cazul tipurilor valoare.
E nevoie de alocare explicita de memorie pentru obiectele propriu-zise, iar la atribuire este
copiata referinta in destinatie, obiectul spre care indica ramanand acelasi (“aliasing”, “reference
semantics”). O imagine explicativa se gaseste in articolul Tipuri de date in C#.
Clasa este unitatea de baza pe care este contruit limbajul C#. Clasele ofera baza pentru
programarea orientata pe obiect. Ele definesc natura obiectelor si reprezinta un sablon prin care
se defineste forma unui obiect. Intr-o clasa sunt definite datele si codul care prelucreaza acele
date. Pentru a stapani bine C#, elementul clasa este esential ( intreaga activitate a unui program
C# se desfasoara in interiorul unei clase).
Exemplu :
//metoda de afisare
public void Afiseaza()
{
Console.WriteLine("{0} {1} are nota {2}", nume, prenume, nota);
}
}
Tipurile clasa suporta mostenirea, un mecanism prin care o clasa ( derivata ) preia anumite
caracteristici ale unei alte clase ( de baza). Un exemplu concret va fi dezbatut in articolul despre
mostenire.
O interfata defineste un set de metode care vor fi implementate de o clasa. Interfata poate fi
privita ca un “contract” iar clasa sau structura care o implementeaza, trebuie sa il respecte.
Prin cuvantul cheie delegate se defineste o delegare care nu inseamna decat o referinta catre o
metoda. Asadar, la crearea unui tip delegat se creaza un obiect care poate memora o referinta
catre o metoda, metoda care poate fi apelata prin intermediul referintei. Un exemplu concret va fi
dezbatut in articolul despre delegate.
Pe langa exemplele din articolul Conversii despre boxing si unboxing unde se prezinta atribuirea
valorilor de orice fel variabilelor de tip object, adaug un exemplu in care se atribuie o valoare
referinta.
object a;
a = new Student();
Student student;
student = (Student)a;
Console.WriteLine(student.nota);
Tipul string este unul din cele mai importante si utilizate tipuri de date. El defineste si
implementeaza sirurile de caractere. Spre deosebire de alte limbaje, unde tipul string este un
tablou de caractere, in C# tipul string este un obiect. Clasa string are o dimensiune relativ mare.
“verbatim” – se folosesc cand se fac referiri la fisiere, la prelucrarea lor, la registri. Tipul acesta
de sir incepe cu “@” inainte ghilimelor.
Pentru concatenarea (unirea) sirurilor de caractere se mai foloseste operatorul “+”. Exemplu :
Pentru comparare se folosesc “==” si “!=” – egalitate inseamna daca ambele sunt null sau daca
ambele au aceeasi lungime si pe fiecare pozitie au caractere respectiv identice. Un exemplu
complet in articolul despre clasa System.Text.
Despre ARRAYS
Un tablou reprezinta o colectie de variabile de acelasi tip, referite prin intermediul unui nume
comun. Tablourile se utilizeaza oriunde exista nevoia de a grupa mai multe variabile de acelasi
tip la un loc pentru a putea fi gestionate si sortate cu usurinta.Accesul se face printr-o variabila
referinta.
Diferenta dintre tablourile din alte limbaje si cele din C#, este ca, aici, tablourile sunt
implementate ca obiecte, fapt care conduce la colectarea automata a spatiului ocupat de
tablourile neutilizate.
La declarea unui tablou, se creeaza o instanta a clasei .Net, System.Array. Compilatorul va
traduce operatiile asupra tablourilor, apeland metode ale System.Array.
La declararea unui tablou folosim tipul variabilei, apoi avem nevoie de “[]” si la sfarsit
identificatorul.
Tip [] nume;
Un mod in care se pot face in acelasi timp si operatiile de declarare, instantiere, initializare,
este :
Numarul valorilor dintre acolade trebuie sa fie egal cu numarul care indica marimea
vectorului, altfel va aparea eroare la compilare.
Se poate omite declararea marimii vectorului si folosirea operatorului new. Compilatorul va
calcula marimea din numarul de initialzari.
int[] colectie = { 1, 3, 4, 5, 6 };
Un element din tablou poate fi accesat utilizand un index ( descrie pozitia elementului din cadrul
tabloului ). In C#, toate tablourile au indexul primului element egal cu zero.
tip nume-var specifica tipul si numele unei variabile de iterare, care va primi la fiecare iteratie
valoarea unui element din colectie. Trebuie ca tip sa fie compatibil cu tipul de baza al tabloului.
Variabila de iteratie nu poate fi modificata in timpul utilizarii ei in accesul la tablou.
Exemplu :
Incepand cu versiunea 3.0, C# introduce cuvantul cheie var. In articolul acesta, il vom folosi
pentru a creea vectori de tip implicit.
Exemplu :
Metoda Array.Sort permite sortarea elementelor dintr-un tablou, indiferent de tipul acestuia.
System.Array.Sort(listaNume);
De remarcat ca Array.Sort sorteaza doar obiecte care implementeaza IComparable, interfata care
defineste relatie de ordine intre oricare doua elemente de tipul dat.
Un exemplu de sortare “custom” poate fi vazut pe programare.org: .Net IComparer.
Un avantaj al faptului ca, in C#, tablourile sunt implementate ca obiecte, este atribuirea pentru
fiecare tablou a proprietatii Length, care contine numarul de elemente pe care tabloul le poate
memora.
Exemplu :
Console.WriteLine("Numarul de elemente din vector este {0}",
listaNume.Length);
Cand incercam sa atribuim o valoare de tipul referinta la un tablou al unei altei valori de tip
referinta ( copiere ), se modifica doar obiectul referit de catre acea variabila. Nu se produce o
copiere a tabloului si nici continutul unui tablou nu se copiaza in celalalt.
int[] colectie1 = { 4, 4, 5, 5 };
Console.WriteLine("Elemente colectia1:");
foreach (int numar in colectie1)
{
Console.WriteLine(numar);
}
Console.WriteLine("Elemente colectia1:");
Tablourile multidimensionale
Tablourile multidimensionale au doua sau mai multe dimensiuni, iar un element oarecare
poate fi accesat utilizand combinatia a doi sau mai multi indici.
Se face distinctie intre tablouri regulate si tablouri neregulate (tablouri de tablouri – jagged
arrays ).
Cele mai simple tablouri multidimensionale regulate sunt cele bidimensionale. In cadrul
acestui tablou, pozitia unui anumit element este data de doi indici.
//dimensiune 2X2
int [,] matrice = new int[2,2];
//atribuim valoare elementului de pe linia 2 si coloana 1
matrice[1,0] = 230;
Tablourile reprezinta un raspuns la multe intrebari din programare si sunt utilizate cu succes
intr-o mare varietate de scopuri pentru modalitatea convenabila de grupare a variabilelor de
acelasi tip la un loc.
Despre operatori
Un operator este un simbol care indica o actiune. Operandul este valoarea asupra careia se
executa operatia. Operatorii alaturi de operanzi formeaza o expresie.
Clasificarea operatorilor
Operatori ternari – actioneaza asupra a trei operanzi. Exista doar unul de acest tip, operatorul
conditional.
Operatorii aritmetici
Operator Semnificatie
+ Adunare ( si plus unar )
- Scadere ( si minus unar )
* Inmultire
/ Impartire
% Impartire Modulo ( rest )
++ Incrementare
– Decrementare
Exemplu :
int numar = 5;
int numar2 = 13;
//Adunare
Console.WriteLine(numar + numar2);
//Scadere
Console.WriteLine(numar - numar2);
//Inmultire
Console.WriteLine(numar * numar2);
//Impartire
//restul este trunchiat
Console.WriteLine(numar2 / numar);
//conversie la tipul double, restul nu este trunchiat
Console.WriteLine((double)numar2 / numar);
//Impartire modulo
//returneaza restul impartirii
Console.WriteLine(numar2 % numar);
Exemplu incrementare :
Exemplu decrementare
Console.WriteLine("DECREMENTARE POSTFIXATA");
Console.WriteLine(numar--);
Console.WriteLine(numar2--);
Console.WriteLine("5"+"5");
Operatorii relationali
Operatorii relationali se refera la relatiile de ordine care pot exista intre doua valori. Deoarece acesti
operatori produc rezultate de tip adevarat sau fals, sunt folositi des cu operatorii logici.
Operator Semnificatie
== egal cu
!= diferit de
< mai mic
> mai mare
<= mai mic sau egal
>= mai mare sau egal
Exemplu
//obiecte diferite
object a = 1;
object b = 1;
Console.WriteLine(a == b);
int numar1 = 1;
int numar2 = 1;
Console.WriteLine(numar1 != numar2);
Operatorii logici
Pentru operatorii logici, operanzii trebuie sa fie de tipul bool, rezultatul fiind tot de acest tip.
Operator Semnificatie
! negatie logica
&& SI logic
|| SAU logic
Intr-o expresie logica ce are in componenta operatorul &&, daca primul operand are valoare
false, rezultatul va fi false, indiferent de valoarea celui de-al doilea operand, iar expresia nu va
mai fi evaluata.
Exemplu:
//Operatorul &&
Console.WriteLine(false && false);
Console.WriteLine(false && true);
Console.WriteLine(true && false);
Console.WriteLine(true && true);
Intr-o expresie logica ce are in componenta operatorul ||, daca primul operator are valoare
true, rezultatul va fi true, indiferent de valoarea celui de-al doilea operand, iar expresia nu va mai
fi evaluata.
Exemplu
//Operatorul ||
Console.WriteLine(true || false);
Console.WriteLine(false || true);
Console.WriteLine(false || false);
Console.WriteLine(true || true);
Operatorul de atribuire
Operatorul de atribuire “=” se foloseste intr-o constructie de forma variabila = expresie.
Dupa evaluarea expresiei, rezultatul va fi atribuit variabilei. Pentru atribuire se mai folosesc si
operatorii +=, –=, *=, /=, %=.
Exemplu :
int a = 5;
Console.WriteLine(a += 5);
//echivalent
Console.WriteLine(a = a + 5);
Operatorul conditional
Daca expresie1 este adevarata, atunci va fi returnata valoarea lui expresie2. Daca expresie1 este
falsa, atunci va fi returnata valoarea lui expresie3.
Exemplu :
int x = 6;
int y = 10;
int z = 67;
In C# sunt foarte multi operatori iar in cazul in care o expresie nu are paranteze, operatiile se
executa conform prioritatii operatorilor. Lista poate fi consultata aici.
Supraincarcarea operatorilor
Prin supraincarea operatorilor se redefineste semnificatia unui operator in contextul dat de o
anumita clasa creata. Prin supraincarea unui operator, se extinde aria de utilizare a acestuia la
clasa creata. Efectele operatorului pot diferi de la o clasa la alta.
Supraincarcarea operatorilor este strans legata de supraincarcarea metodelor. Desi ajuta la
sporirea expresivitatii codului, supraincarcarea operatorilor poate duce si la crearea confuziilor.
Despre polimorfism
“O singura interfata, mai multe metode” – sintagma pe care se bazeaza conceptul de
polimorfism. Se incearca stabilirea unei interfete generice pentru un intreg grup de activitati
asemanatoare.
Un obiect polimorfic este capabil sa ia mai multe forme, sa se afle in diferite stari, sa aiba
comportamente diferite.
Polimorfismul parametric
Cand cream o metoda, de regula se stie numarul parametrilor care vor fi transmisi. Sunt cazuri
in care nu se intampla acest lucru si va fi nevoie de un numar arbitrar de parametri. Se va recurge
la un tip special de parametru, de tipul params. Acesta va declara un tablou de parametri, care
poate memora zero sau mai multe elemente.
Exemplu :
int minim;
//in cazul in care nu e transmis functiei
//nici un parametru, afiseaza un mesaj
if (numere.Length == 0)
{
Console.WriteLine("Nu sunt parametri");
return 0;
}
//initializam variabila
//cu primul element al tabloului
minim = numere[0];
if (i < minim)
//atribuim valoarea minima variabilei minim
minim = i;
//inapoi la programul apelant al functiei
return minim;
Polimorfismul ad-hoc
Se mai numeste si supraincarcarea metodelor, una dintre cele mai interesante facilitati oferite de
limbaju C#. Cu ajutorul acesteia, se pot defini in cadrul unei clase mai multe metode, toate avand
acelasi nume, dar cu tipul si numarul parametrilor diferit. La compilare, se va apela functia dupa
numarul parametrilor folositi la apel.
Pentru a supraincarca o metoda, pur si simplu trebuie doar declararea unor versiuni diferite ale
sale. Nu este suficient insa, ca diferenta dintre doua metode sa fie facuta doar prin tipul valorii
returnate, ci e nevoie si de tipurile sau numarul parametrilor.
Exemplu:
class SupraincarcareMetoda
{
Polimorfismul de mostenire
Intr-o ierarhie de clase, se pune problema apelarii metodelor care au aceeasi lista de parametri,
dar care sunt in clase diferite.
Exemplu:
class Baza
{
public void Afiseaza()
{
Console.WriteLine("Apelul functiei Afiseaza din clasa de baza\n");
}
La compilare se rezolva problema apelarii metode Afiseaza, pe baza tipului declarat al obiectelor
:
Virtual este folosit in declararea unei metode sau a unei proprietati. Acestea se vor numi
membri virtuali. Implementarea unui membru virtual poate fi schimbata prin suprascrierea
membrului intr-o clasa derivata.
Override se foloseste pentru a modifica o metoda sau o proprietate si furnizeaza o noua
implementare a unui membru mostenit dintr-o clasa de baza. Metoda de baza suprascrisa si
metoda de suprascriere trebuie sa aiba aceeasi signatura ( tip si numar de parametri ).
Implicit, metodele nu sunt virtuale. Nu se pot suprascrie metodele care nu sunt virtuale.
Exemplu:
class Baza
{
public virtual void Afiseaza()
{
Console.WriteLine("Apelul functiei Afiseaza din clasa de baza\n");
}
}
class Derivata : Baza
{
public override void Afiseaza()
{
Console.WriteLine("Apelul functiei Afiseaza din clasa derivata");
}
}
Derivata obiect2 = new Derivata();
//instantiem pe un obiect din clasa derivata
Baza obiect1 = obiect2;
//afiseaza functia din clasa de Baza
obiect1.Afiseaza();
obiect2.Afiseaza();
Polimorfismul ajuta la reducerea complexitatii pentru ca permite unei interfete sa fie folosita
de fiecare data pentru specificarea unei clase generice de actiuni. Programatorul nu va efectua
manual selectia. Selectia actiunii specifice (metoda) va fi facuta de compilator.
Programele orientate spre obiect sunt organizate in jurul datelor (ceea ce este afectat de executia
programului). Programele contin datele cat si metodele asociate crearii, prelucrarii si distrugerii
datelor.
In cadrul unui obiect, codul si datele pot fi private sau public. Cand sunt private, ele sunt
vizibile si accesibile doar in interiorul obiectului. In cazul public, celelalte parti ale programului
le pot utiliza.
Forma unui obiect este definata de clasa. Datele care constituie o clasa sunt denumite
variabile membri. Codul care opereaza asupra datelor este numit metoda .
Metoda implementeaza o actiune, poate admite parametri si returna valori de tip predefinit, de
tip obiect sau tipul void (nimic). Un parametru sau argument este o valoare transmisa unei
metode, cu valabilitate in corpul functiei.
Exemplu :
public class Student
{
//declararea variabilelor membru
public double nota;
private int varsta;
Datorita modificatorului de acces public, clasa Student va fi vizibila peste tot si cu acces
nelimitat. In cazul in care nu adaugam un modificator in declarea clasei, ea ar primit implicit tot acest
modificator de acces.
modificator Explicatii
access
public access nelimitat
internal acces permis doar in clasa sau spatiul de nume
in care e cuprinsa
protected acces in clasa curenta sau in cele derivate
private implicit.Doar pentru clasele interioare
protected folosit pentru clasele interioare semnificand
internal accesul in clasa care-l contine sau in tipurile
derivate din clasa care-l contine
Definitia unei clase creeaza un nou tip de data (clasa este un tip de date referinta). Continuand
cu exemplu, vom creea un obiect de tipul Student.
Dupa executia instructiunii, Popescu va fi o instanta a clasei Student. La crearea unei instante,
se va crea un obiect care contine o copie proprie a fiecarei variabile membru din cadrul clasei.
Pentru a referi este variabile, se va utiliza operatorul “.” .
Exemplu:
Constructorul
Constructorul este o metoda care face parte dintr-o clasa. Constructorul contine instructiuni
care se executa la crearea unui nou obiect al clasei. Daca o clasa nu are definit un constructor,
atunci se va atribui automat constructorul fara parametri al clasei de baza.
//constructor implicit
public Student()
{
nota = 0;
varsta = 0;
Destructorul
Corpul destructorului este format din instructiuni care se executa la distrugerea unui obiect al
clasei. Destructorul nu este apelat in mod explicit, pentru ca, in mod normal, la procesul de
distrugere este invocat Garbage Collector .
Operatorul new
Operatorul new este folosit pentru crearea obiectelor si invocarea constructorilor (reamintim
ca si la tipurile de date se apela constructorul implicit: int variabila = new int ();).
Operatorul this
Acest operator se refera la instanta curenta pentru care a fost apelata o metoda, o proprietate.
Operatorul mai este folosit in situatia cand numele unui parametru sau al unei variabile locale
coincide cu numele unei variabile membru. In acest caz, numele local “va ascunde” variabila
membru. Prin intermediul lui this, se va putea referi la acea variabila membru. (exemplu mai sus,
la declararea constructorului cu parametri).
Pentru o aplicare cat mai buna a principiilor POO, sunt importante operatiile de identificare a
entitatilor, a datelor si operatiilor, a relatiilor dintre entitati si o posibila ierarhie a claselor.
Mostenirea este unul din principiile fundamentale ale programarii. Cum functioneaza? Definim o
clasa generala, clasa de baza, ce are caracteristici comune pentru o multime de elemente
asemanatoare. Apoi, clasa de baza va fi mostenita de alte clase, clase derivate, care isi vor
adauga pe langa caracteristicile mostenite, numai caracteristici care le identifica in mod unic. O
clasa derivata este o versiune specializata a unei clase de baza. O clasa derivata va mosteni toate
variabilele, metodele, proprietatile si indexarile definite in clasa de baza, la care va adauga
elemente proprii, unice.
Este nevoie de conceptul de mostenire pentru a evita repetitia definirii unor clase care au in
comun mai multe caracteristici. In programare, mostenirea se refera la clasificare, la o relatie
intre clase.
In C#, o clasa derivata poate mosteni de la o singura clasa de baza ( mostenire simpla ).
O clasa de baza poate fi derivata in mai multe clase derivate. O clasa derivata poate fi clasa de
baza pentru alta clasa derivata.
O clasa de baza impreuna cu toate clasele derivate, direct sau indirect, formeaza o ierarhie de
clase. Reamintim, toate clasele din C# mostenesc clasa Object.
Exemplu de mostenire :
//clasa de baza
class Forma
{
public double inaltime;
public double latime;
public void AfiseazaDimensiune()
{
Console.WriteLine("inaltime: {0}, latime: {1}",inaltime,latime);
}
}
//clasa Dreptunghi mosteneste
//clasa Forma
class Dreptunghi : Forma
{
public double Aria()
{
//clasa Dreptunghi poate
//referi membrii clasei Forma
//ca membri proprii
return inaltime * latime;
}
}
In metoda Main :
In ceea ce priveste sintaxa la mostenire, se observa ca numele clasei de baza urmeaza dupa
numele clasei derivate, separate de “:”.
Clasa de baza Forma poate fi utilizata independent, chiar daca din ea deriva alte clase.
Despre accesul la membri : in cazul in care in clasa de baza, declaram membri de tip private, (
private double inaltime; private double latime; ), clasa Dreptunghi nu i-ar mai fi putut accesa.
Membrii privati sunt accesibili doar pentru codul din interiorul clasei lor, deci, mostenirea NU va
anula restrictiile impuse.
Pentru a putea depasi aceasta problema si a nu fi nevoie sa declaram membri public ( vor fi
accesibili in tot restul codului ), una din solutii ar fi declararea membrilor ca protected. Acest
modificator de acces face ca membri protejati sa fie public in cadrul unei ierarhii de clase, dar
privati in afara ei.
O intrebare interesanta cand se discuta despre mostenire, apare cand clasele de baza si clasele
derivate dispun de constructori proprii : Ce constructor se utilizeaza pentru a construi un obiect
apartinand unei clase derivate? Raspuns : constructorul clasei de baza construieste portiunea
obiectului care apartine de clasa de baza, constructorul clasei de baza construieste portiunea care
apare de clasa derivata.
//clasa de baza
class Forma
{
//membri variabila
protected double _inaltime;
protected double _latime;
//constructor pentru clasa Forma
public Forma(double inaltime, double latime)
{
_inaltime = inaltime;
_latime = latime;
}
}
//clasa Dreptunghi mosteneste
//clasa Forma
class Dreptunghi : Forma
{
//membru variabila privat
string stil;
O alta situatie legata de mostenire este atunci cand intr-o clasa derivata definim un membru al
carui nume coincide cu numele unui membru din clasa de baza.
class A
{
public int a;
class B : A
{
int a;
}
In acest caz, la compilare va aparea mesajul :
‘Inheritance.B.a’ hides inherited member ‘Inheritance.A.a’. Use the new keyword if hiding was
intended. Daca se doreste ca membrul clasei de baza sa nu fie vizibil in clasa derivata, se
foloseste new.
Chiar daca variabila membru i din B ascunde i din A, folosind base se permite accesul la
variabila definita in clasa de baza. Este valabil si pentru metode.
//descopera a
//din clasa A
base.a;
Modificatorul abstract
In unele cazuri, este necesar crearea unei clase de baza care stabileste un sablon general
comun pentru toate clasele derivate, fiecare completand detaliile caracteristice. O clasa care este
definita cu abstract, nu poate fi instantiata, poate contine metode abstract, iar o clasa derivata
dintr-o clasa abstract trebuie sa o implementeze in totalitate.
//aceasta clasa
//nu va putea fi mostenita
abstract class M
{
public abstract void Metoda();
}
Modificatorul sealed
//aceasta clasa
//nu va putea fi mostenita
sealed class M
{
Interfete
Scriam in articolul despre mostenire (inheritance) ca mostenirea unei clase foarte importanta in
C#. Dar, un altfel de mostenire, cea a interfetelor, are o putere mult mai mare.
O interfata defineste un set de metode, proprietati, evenimente, indexatori care vor fi
implementate de o clasa. Atentie, interfetele NU implementeaza, ele sunt abstracte si doar isi
descriu membrii. Interfata precizeaza CE trebuie facut, dar NU si CUM trebuie facut.
interface nume {
tip nume-metoda (param);
tip nume-metoda(param);
}
Exemplu : selectam in Visual Studio numele interfetei si la click dreapta vom implementa
interfata :
Codul, avand continutul metodelor modificat :
#endregion
}
Exemplu:
public interface I1
{
void Metoda1();
void Metoda2();
}
public interface I2
{
void Metoda3();
void Metoda4();
}
class A : I1, I2
{
#region I1 Members
#endregion
#region I2 Members
#endregion
}
In cazul in care o clasa implementeaza mai multe interfete, numele acestora vor fi separate
prin “,”. Este intalnita situatia cand o clasa va mosteni o clasa de baza si va implementa una sau
mai multe interfete – numele clasei de baza trebuie sa fie primul in lista separata prin virgule.
Mostenirea interfetelor
O interfata poate mosteni o alta interfata. Sintaxa este comuna cu cea folosita la mostenirea
claselelor. Daca o clasa mostenteste o interfata care mosteneste o alta interfata, clasa va trebui sa
contina implementari pentru toti membri definiti pe lantul de mostenire al interfetei.
Exemplu :
#region B Members
#endregion
#region A Members
#endregion
}
Si in cadrul mostenirii interfetelor, poate aparea situatia cand un membru al interfetei derivate
are aceeasi signatura cu membrul cu acelasi nume din interfata de baza. Situatia va genera un
mesaj de atentionare si va fi rezolvata prin folosirea operatorului new, daca se doreste ca
membrul din interfata de baza sa fie ascuns. (la fel ca la mostenirea claselor).
Exemplu :
Pastram interfetele definite mai sus. Acum, selectam in Visual Studio numele interfetei si la click
dreapta vom implementa interfata in mod explicit
Doar la implementarea metodei2 vom aduce modificari, pentru a fi vizibila in afara clasei.
class Test : B
{
#region B Members
void B.Metoda3()
{
Console.WriteLine("Metoda3.Implementare explicita");
}
#endregion
#region A Members
void A.Metoda1()
{
Console.WriteLine("Metoda1.Implementare explicita");
}
#endregion
}
//metodele implementate explicit, nu vor fi vizibile
Test t = new Test();
t.Metoda2();
In .Net, printre cele mai folosite interfete se numara IDisposable, (folosita pentru eliberarea
spatiului de memorie), IComparable (folosita pentru sortare), IConvertible (pentru convertirea la
tipuri de baza), etc.
O interfata este o constructie pur abstracta. Pentru un programator, nu doar in C#, lucrul cu
interfetele trebuie stapanit foarte bine.
Despre delegates
In programare, exista situatii cand trebuie sa executam o anumita actiune, dar fara sa stim in
avans ce metoda sau ce obiect vom apela pentru executarea actiunii. Exemplu : la apasare, un
buton va sti ca trebuie sa notifice un anumit obiect , dar nu stie exact care. Solutia simpla consta
in conectarea butonului la un delegat si apoi acesta sa indice catre o anumita metoda.
Un delegat este un obiect care poate referi o metoda. Chiar daca nu este un obiect, o metoda
ocupa un loc in memorie, iar aici, la aceasta adresa, va fi transmis controlul la invocarea metodei.
Un delegat reprezinta modul prin care se realizeaza comunicarea intre obiecte. Un delegat este
un tip referinta si este este echivalentul unui pointer la functie din C++. Diferenta este ca
delegates sunt type-safe si ca sunt orientati pe obiect.
lista-parametri – lista de parametri necesari metodelor care vor fi apelate prin intermediul
delegatului.
Poate fi declarat in afara unei clase, sau in interior. In functie de cum se vrea vizibilitatea lui, ii
putem aplica modificatorii de acces public, private, protected,etc.
Dupa ce a fost declarat, un delegat poate apela doar metode cu acelasi tip returnat si aceeasi
lista de parametri.
Exemplu :
class DelegateTest
{
//returneaza numarul de caractere al unui sir de caractere
public int StringLength(string str)
{
Console.WriteLine("Va returna numarul de caractere");
return str.Length;
}
//construim delegat
StringLengthDelegate strLength = new
StringLengthDelegate(test.StringLength);
//sirul care va fi transmis functiilor
string str;
//apelul unei metode prin intermediul delegarii convertim de la numar la
string
//pentru ca functia returneaza int
str = strLength("Test").ToString() ;
Console.WriteLine(str);
//construim delegat
StringReverseDelegate strReverse = new
StringReverseDelegate(test.StringReverse);
//apelul unei metode prin intermediul delegarii
str = strReverse("Test");
Console.WriteLine(str);
Pe scurt: avem doua metode statice in clasa DelegateTest ale caror signaturi coincid cu
signaturile delegatilor. In Main, construim referinte de tipul StringLengthDelegate si
StringReverseDelegate, pe care le atribuim metodelor. Se mai observa ca invocarea delegatilor
determina apelul metodelor.Determinarea metodei apelate se rezolva la momentul executiei, nu
la compilare.
Multicasting
Exemplu :
Datorita faptului ca la compilare, nu se cunosc metodele care urmeaza a fi executate, lucrul cu
delegate este intalnit in arhitecturile care permit adaugarea componentelor pe parcurs.
In articolul urmator, se va observa una din utilitatile delegatilor, anume implementarea
evenimentelor.
Despre evenimente
Cele mai multe evenimente sunt actiuni ale utilizatorilor (click, schimbarea unei liste,
introducere de text, sfarsitul unui calcul etc).
In .Net, obiectele cunoscute ca si event senders, declanseaza evenimente atunci cand au loc
actiuni. Event receivers se ocupa de aceste evenimente prin rularea unei metode numite event
handler. Pentru ca event sender-ul nu stie care metoda se va ocupa de eveniment, trebuie creat un
delegate care sa se comporte ca un pointer catre event handler.
Pentru a raspunde la un eveniment, e nevoie de doi pasi: trebuie sa construim o metoda,
metoda care trebuie sa se potriveasca signaturii delegatului. Doi, e nevoie sa adauga un event
handler care sa indice care metoda trebuie sa primeasca evenimentele.
Declararea unui eveniment se face prin folosirea cuvantului cheie event.
Exemplu:
//declaram un delegate
//pentru tratarea evenimentului
delegate void DelegatTratareEveniment();
Deoarece evenimentele au delegati multicast, tipul rezultatului va fi void. Pentru acest
exemplu, nu exista parametri, dar evenimentele pot accepta parametri.
class Eveniment
{
//declaram evenimentul
public event DelegatTratareEveniment activat;
//metoda apelata la lansarea evenimentului
public void Actiune()
{
if (activat != null)
//lansam evenimentul
activat();
}
}
Metoda Actiune va fi apelata de program pentru a semnala un eveniment si apeleaza rutina de
tratare a evenimentului prin intermediul delegatului activat, daca si numai daca acesta nu este
null (verificam faptul ca delegatul nu este null pentru ca este posibil ca metoda Actiune sa fie
apelata inainte de inregistrarea rutinei de tratare).
In clasa Program a proiectului, construim rutina de tratare numita handler, care in acest
exemplu simplu doar va afisa un mesaj.
//rutina de tratare
static void handler()
{
Console.WriteLine("Eveniment produs");
}
In metoda Main, construim o instanta a clasei Eveniment, iar metoda handler este inregistrata ca
rutina de tratare a evenimentului.
Lansam evenimentul:
//lansam evenimentul
ev.Actiune();
Multicast
Exemplu:
Scriem acelasi cod pentru declararea unui delegat si pentru declararea clasei Eveniment.
Adaugam inca doua clase, a caror rutine de tratare nu sunt statice, deci va trebuie sa creeam
instante pentru clasele respective.
class A
{
public void AHandler()
{
Console.WriteLine("Eveniment primit de un obiect A");
}
}
class B
{
public void BHandler()
{
Console.WriteLine("Eveniment primit de un obiect B");
}
}
//crearea instantelor
A a = new A();
B b = new B();
//lansam evenimentul
ev.Actiune();
//eliminarea unei rutine de tratare
ev.activat -= new DelegatTratareEveniment(a.AHandler);
Ca observatie valabila pentru evenimente in acest exemplu : evenimentele sunt destinate
instantelor si nu claselor in general.
In .Net, mai toate evenimentele sunt implementate folosind delegati multicast, care au doi
parametri (unul de tip object – care reprezinta obiectul care provoaca evenimentul, iar celelalt de
tip EventArgs care contine data utilizabile in tratarea evenimentului).
Putem construi evenimente in interiorul claselor, iar la declansarea lor ne putem da seama unde
s-a ajuns cu procesarea codului.
Despre threads
De multe ori, aplicatiile au nevoie de mult timp pentru a rezolva o sarcina (descarcarea unui
fisier, printare, generarea unui raport etc), timp in care programul nu poate raspunde unei alte
actiuni a utilizatorului. Pentru a face ca o aplicatie sa indeplineasca o sarcina si sa poata primi si
altele in acelasi timp, sunt folosite firele de executie multiple (multiple threads).
Intr-un program liniar se executa o singura linie de cod, se asteapta pentru completare si apoi
se continua cu urmatoarea linie de cod. Programul nu poate raspunde actiunii unui utilizator in
acest timp si chiar in cazul mai multor procesoare, va fi folosit doar unul singur, limitand
performanta, datorita programarii single-thread.
Un fir de executie (thread) este un program secvential, care poate fi executat concurent cu alte
fire. Un thread este o unitate de executie intr-un proces. Un proces poate avea mai multe fire de
executie, el numindu-se multithread. Daca un calculator are mai multe procesoare sau un
procesor cu mai multe nuclee, el poate executa mai multe fire de executie simultan.
Diferenta dintre un proces si un thread este ca procesele sunt izolate total unul de celalalt, in
timp ce thread-urile impart aceeasi memorie (heap) cu alte thread-uri care ruleaza in aceeasi
aplicatie (un thread poate prelua informatii noi, in timp ce un alt thread le prelucreaza pe cele
existente).
Folosirea firelor de executie este o solutie la imbunatatirea performantei. Se cere insa foarte
multa atentie la folosire. Scrierea unui cod multithread este complexa, iar problemele care pot
aparea pot fi foarte greu de rezolvat.
Trecerea de la starea gata de executare la starea in curs de executare are loc cand un procesor
il alege pentru executare. La terminare, trece in starea terminat.
Un proces în curs de executare poate fi suspendat (amanat sau blocat), dupa care poate reveni
în substarea gata de executare.
– amânat: inseamna întreruperea executarii procesului un anumit timp, în urma apelarii
procedurii predefinite sleep; aceasta procedura are un singur parametru de tip întreg.
- blocat: inseamna întreruperea executarii procesului pe baza unei relatii (comunicari) cu alte
procese; procesul poate reveni ulterior în starea gata de executare pe o baza similara.Esential in
folosirea firelor de executie multiple este evitarea conflictelor de resurse, cand vor trebuie
blocate anumite resurse, pentru a putea fi folosite de catre un singur fir de executie, pe rand.
Cel mai simplu mod de a crea un thread este sa instantiem un obiect Thread, al carui
constructor va cere ca parametru un delegate de tipul ThreadStart. Delegatul va indica ce metoda
va rula in thread.
using System.Threading;
class Numara
{
//numara pana la 10
public void Zece()
{
Console.WriteLine("Simple Thread");
for (int i = 0; i <= 10; i++)
{
Console.WriteLine(i);
}
}
}
in Main:
Codul de mai sus nu reflecta puterea firelor de executie multiple, pentru ca avem doar unul. O
sa adaugam inca un thread, o sa setam numele pentru o identificare mai usoara si vom folosi
proprietatea CurrentThread care va returna firul de executie curent.
//obiectul Thread
Thread thread1 = new Thread (new ThreadStart(numara.Zece));
Thread thread2 = new Thread(new ThreadStart(numara.Zece));
thread1.Start();
thread2.Start();
Exemplul complet, de la sfarsitul articolului, va contine cod care va arata cum thread-urile
apeleaza metode diferite.
Cand trebuie sa declaram mai multe fire de executie, putem construi un vector de fire de
executie.
Thread[] threads =
{
new Thread(new ThreadStart(numara.Zece)),
new Thread(new ThreadStart(numara.Zece))
};
Metoda Sleep
Se foloseste atunci cand vrem sa suspendam un fir de executie, pentru o anumita perioada.
Exemplu:
Thread.Sleep(5000);
Metoda Join
Presupunem ca suntem in situatia cand vrem ca un thread sa astepte un alt thread sa isi termine
executia, inainte ca thread-ul nostru curent sa continue.
Metoda Abort
Proprietatea Priority
Folosind aceasta proprietate, se poate determina ce timp de executie are un thread in
comparatie cu alte threaduri active din acelasi proces:
In lucrul cu multithreading-ul apare o problema majora, cea a sincronizarii: mai multe fire de
executii acceseaza acelasi obiect, simulan. Solutia vine de la “lock”: atunci cand primul thread
acceseaza obiectul, il va tine “ocupat” pana la incheiere.
Folosirea firelor de executie permite rularea proceselor simultan, dar acest lucru poate incetini
executia programului, daca nu sunt folosite cu atentie.
O aplicatie C# poate deveni multi-threading in doua moduri: fie explicit prin crearea si rularea
firelor de executie, fie prin folosirea unor caracteristici ale .Net care creeaza implicit thread-uri:
BackgroundWorker, thread pooling sau la construirea unui Web Service sau unei aplicatii
Asp.Net.
ArrayList
ArrayList este o colectie simpla care poate stoca orice tip de obiect.
Clasa ArrayList este continuta in spatiul de nume System.Collections. Aceasta clasa ne permite
sa construim un vector care sa creasca in marime, adaugand mereu elemente noi.
//declare
ArrayList lista = new ArrayList();
//adaugam obiecte
lista.Add("String");
lista.Add(3);
lista.Add(5.65);
Construim o lista de tip string si vom folosi metoda Sort pentru a o sorta alfabetic si Reverse
pentru sortare inversa
Console.WriteLine("----");
//sortare in ordine inversa
listaString.Reverse();
foreach (object o in listaString)
{
Console.WriteLine(o.ToString());
}
//sterge un obiect
//din lista
lista.Remove("abc");
Pentru localizarea unui anumit element, apelam metoda BinarySearch si transmitem valoarea
obiectului pentru a fi returnata pozitia obiectului in lista.
Console.WriteLine(listaString.BinarySearch("def"));
Putem folosi si metoda Contains care va returna o valoare de tip boolean ( daca lista contine sau
nu contine elementul cautat).
Console.WriteLine(listaString.Contains("def"));
ArrayList si Array se aseamana foarte mult. Ambele permit stocarea unui numar mare de
elemente.
ArrayList cu referinte
Vom crea o clasa Cont, cu 3 metode. Apoi vom construi obiecte pe baza ei si vom adauga
referinte in ArrayList catre obiectele respective.
class Cont
{
private decimal sold = 0;
//scade o suma din sold
public bool RetrageSuma(decimal suma)
{
//daca nu sunt suficienti bani
In Main:
//adaugam referinte
conturi.Add(student);
conturi.Add(elev);
Observatie : nu adaugam un Cont in lista, ci doar facem ca un element din lista sa indice catre
un Cont. Deci, nu se adauga obiectul insusi, ci o referinta.
Cum accesem elementele ? Daca incercam astfel, cum suntem obisnuiti, vom avea eroare la
compilare :
Cont c = conturi[0];
c.Sold();
Cannot implicitly convert type ‘object’ to ‘ContExemplu.Cont’. An explicit conversion exists (are
you missing a cast?)
Motivul este ca ArrayList este o lista de Objects. Vom rezolva problema convertind la tipul
Cont.
Foarte asemanatoare cu ArrayList este clasa List, in plus avand avantajul de typesafe. Vom
scrie despre aceasta clasa in articolul viitor.Exista si StringCollection, o colectie tipizata.
Impreuna cu alte colectii din .Net care sunt strongly typed, aceste clase tipizate sunt usor de
folosit in dezvoltare datorita faptului ca Visual Studio poate face automat validarea si nu mai e
nevoie sa folosim conversia.
ContExemplu.rar
ListaVectori.rar
Clase generice
Reamintesc problema din articolul despre ArrayList: cream o lista in care adaugam referinte
catre obiecte iar pentru a le accesa trebuia sa convertim explicit la tipul obiectului, altfel
primeam eroare la compilare, de tipul: “Cannot implicitly convert type ‘object’ to
‘ContExemplu.Cont’. An explicit conversion exists (are you missing a cast?)”.
C# este un limbaj type-safe, lucru care permite ca unele eventualele erori sa fie returnate la
compilare (acest lucru face ca programul sa fie stabil, in sensul ca mare parte dintre errorile
datorate tipurilor de date folosite sunt detectate la compilare), si nu la executie (run-time).
Am expus aceasta problema, pentru a intelege mai bine termenul de generic. Clasele sau
functiile generice sunt cele care primesc ca parametru tipul datelor manipulate si sunt foarte
folosite in implementarea colectiilor sau algoritmilor care actioneaza asupra unor tipuri variate
de date.
Notiunea de cod generic poate parea cunoscuta celor care programeaza in C++, doar ca
implementarea si regulile de utilizare difera in .Net.
Acum, putem construi o clasa pentru int si una pentru double, foarte simplu :
using System.Collections.Generic;
Folosind aceeasi clasa Cont din articolul ArrayList, construim o lista in care vom retine
referinte catre obiecte de tip Cont.
Lista conturi “va sti” exact ce tip de obiecte sa stocheze, astfel vom putea adauga un obiect de
tip Cont si vom putea accesa metodele lui, astfel:
conturi[0].Depune(40);
Console.WriteLine(conturi[0].Sold());
Daca incercam sa adauga un o varabila de un alt tip in afara de cel specificat in “<>”, va aparea
eroare de compilare:
Cu clasa List se pot face operatii asematoare cu cele din ArrayList (Add, Count, Remove,
BinarySearch etc).
Aceste proprietati furnizeaza informatii despre obiectul de tip List. Proprietatea Count, ca si in
cazul ArrayList, identifica numarul de elemente existente in lista. Proprietatea Capacity arata
cate elemente mai pot fi adaugate in lista fara ca aceasta sa fie redimensionata.
lista.TrimExcess();
//sunt egale
Console.WriteLine(lista.Capacity);
Console.WriteLine(lista.Count);
Metoda Clear
Metoda Clear va elimina toate elementele din List Conturi. Apelul acestei metode este similar
cu atribuirea liste catre null sau cu instantierea unui nou obiect (= new List…) in locul celui ale
carui componente le dorim sterse.
Presupunem ca avem un vector si vrem sa construim o lista care sa contina elementele acestuia.
Vom transmite vectorul ca parametru iar lista va copia elementele din el.
Exemplu:
Console.WriteLine(lista.Count);
In articolul urmator, vom prezenta o alta clasa, care lucreaza tot cu grupuri de obiecte,
Dictionary, avand rolul principal de stabili o relatie intre o cheie si o valoare.
Dictionary
O alta colectie generica utila in .Net este Dictionary. Ea functioneaza pe principiul perechilor
cheie/valoare.
Dictionarele stabilesc o relatie (“map“) intre o cheie si o valoare. De exemplu, avem numarul
de identificare al unui angajat, caruia ii putem atribui un obiect care reprezinta angajatul.
Clasa generica Dictionary face parte din spatiul de nume System.Collection.Generics. Pentru
folosirea ei, vom folosi:
using System.Collections.Generic;
Folosind in continuare clasa Cont, folosita in articolele despre ArrayList si List<T>, construim
un dictionar care va stoca perechi cheie/valoare de tipul string/Cont:
Va aparea eroare la compilare, daca vom incerca sa adaugam o valoare de alt tip decat Cont.
//eroare
MyClass cls = new MyClass();
dictionar.Add("String",cls);
dictionar["Student"].Depune(50);
Incercam sa accesem cu ajutorul unei chei un element. Daca nici un element nu are o valoare
pentru cheia respectiva, va aparea exceptia KeyNotFoundException : “The given key was not
present in the dictionary”.
//exceptie
dictionar["a"].Depune(50);
In cazul folosirii clasei Dictionary, se recomanda mai intai testarea cheilor pentru a vedea daca
exista si de a evita primirea exceptiei, folosind metoda ContainsKey.
if (dictionar.ContainsKey("aa"))
{
Console.WriteLine("exista");
}
else
{
Console.WriteLine("nu exista");
}
Clasa Dictionary contine o metoda, ContainsValue, care cauta in toata colectia dupa valoare
data ca parametru:
Metoda TryGetValue
TryGetValue va returna valoarea asociata unei chei. Folosirea acestei metode este eficienta
atunci cand se doreste obtinerea valorilor dintr-un dictionar unde programul acceseaza frecvent
chei care nu sunt in dictionar.
Metoda are doi parametri: cheie – a carei valori incercam sa o obtinem si valoare – contine
valoarea asociata cheii respective, in cazul in care este gasita. Daca nu este gasita, va returna
valoare implicita pentru tipul valoare.
Exemplu:
Ca sa testam daca metoda va returna corect valoarea cheii cautate, apelam metoda Depune,
pentru a modifica valoarea soldului.
contElev.Depune(40);
Construim un dictionar un pic mai simplu, pentru a arata cum putem folosi KeyValuePair intr-
un ciclu foreach.
Un lucru util in folosirea clasei Dictionary, este folosirea constructorului, atunci cand se
doreste construirea unei copii a unui dictionar existent. In urmatorul exemplu vom realiza o
copie a dictionarului catalog.
//construim o copie
//a unui dictionar existent
Dictionary<string, int> copie = new Dictionary<string, int>(catalog);
//afisam elementele din dictionar
foreach (KeyValuePair<string, int> pereche in copie)
{
Console.WriteLine("{0},{1}", pereche.Key, pereche.Value);
}
Pentru stergerea tuturor perechilor cheie/valoare din dictionar, se foloseste metoda Clear.
dictionar.Clear();
Daca se doreste eliminarea doar a unui element, folosim metoda Remove, care va avea ca
parametru o cheie.
In urma articolelor despre List si Dictionary, probabil va aparea intrebarea : Care trebuie
utilizata si cand? Recomandarea e ca Dictionary sa fie folosit atunci cand e nevoie de cautari
(“lookups”). In cazul unei cautari in clasa List, aceasta poate cauza probleme in cazul unui numar
foarte mare de elemente.
O colectie inrudita cu Dictionary este SortedDictionary, unde perechile sunt sortate dupa cheie.
HashTable (colectie de perechi cheie/valoare, organizata pe baza unui cod “hash” al cheii)
SortedList (perechi cheie/valoare care sunt sortate in functie de chei si care sunt accesate de
cheie si index),
ListDictionary (dictionar optimizat pentru colectii mici de obiecte, cu mai putin de 10 elemente),
HybridDictionary (dictionar care se bazeaza pe ListDictionary cand numarul elementelor este
mic, iar cand numarul elementelor va creste, va folosi HashTable),
Tipul Dictionary este util in situatiile cand e nevoie de stocarea obiectelor pe baza unui
identificator unic.
Colectiile generice dau foarte mult flexibilitate si sunt recomandate inainte celor non-generice.
In acest articol, vom lucra un exemplu in care vom folosi o colectie, un dictionar cu perechi de
chei de tip int si valori de tip string, dictionar care va fi construit pe baza a ceea ce introduce
utilizatorul de la tastatura. Dupa crearea lui, il vom salva intr-un fisier text pe hard. Apoi vom citi
colectia din fisierul text.
Vom alege un proiect de tip Console Application si vom explica notiuni care nu au fost
introduse pana acum.
Aceasta metoda din clasa DictionaryExemplu, va prelua caracterele introduse de utilizator, iar
daca va gasi numar, il va returna functiei Main. Pe acest numar il vom folosi pentru a sti cate
elemente va avea dictionarul pe care vrem sa il construim.
//parcurgem dictionarul
foreach (KeyValuePair<int, string> el in catalog)
{
//scriere textului in fisier
fisierIesire.Write("(" +el.Key+",");
fisierIesire.WriteLine(el.Value+")");
}
//inchiderea fisierului
fisierIesire.Close();
In metoda Main din clasa Program a proiectului, declaram si instantiem clasa
DictionarExemplu:
dictionarExemplu.CreeazaDictionar(numarElem);
dictionarExemplu.SalveazaDictionar();
Daca se doreste citirea dictionarului din fisierul text in care a fost salvat:
dictionarExemplu.CitesteDictionar();
Acesta a fost un exemplu scurt, practic, despre construirea unei colectii – dictionar (puteam
alege, foarte simplu, din cele prezentate in articolele precedente, List sau ArrayList) si salvarea
ei sub forma unui fisier text, pe hard, intr-o locatie specificata de noi. De asemenea, am construit
si o functie care sa citeasca dintr-un fisier text, continutul colectiei noastre.
Pentru a descarca in format rar codul integral folosit in articolul curent – click aici!
System.IO pe scurt
De multe ori, aplicatiile au nevoie sa stocheze date pe hard-disk (salvarea intre mai multe
sesiuni ale datelor aplicatiei, data logging, troubleshooting, comunicarea cu alte aplicatii,
compresarea, decompresarea datelor, etc). Pentru aceasta, aplicatiile vor folosi clasele din
System.IO:
using System.IO;
Aceasta clasa contine metode statice pentru crearea, mutarea directoarelor, afisarea
subdirectoarelor. Clasa nu poate fi mostenita.
Console.WriteLine("Directoare");
//afisam in consola fiecare director din vector
foreach (DirectoryInfo subDir in subDirs)
{
Console.WriteLine(subDir.Name);
}
Aceasta clasa ajuta la furnizarea multor informatii cu privire la directoare: nume, cale, marime,
extensii, data crearii, data ultimei accesari.
Metodele uzuale sunt Create (creeaza un director), Delete (sterge atat directorul cat si
continutul sau), MoveTo (muta directorul si continutul sau la o alta cale), CreateSubirectory,
GetFiles (returneaza lista de fisiere continute in directorul respectiv).
Descriere detaliata si membrii clasei DirectoryInfo, pe siteul Microsoft.
O alta clasa folosita pentru lucrul cu directoarele din sistemul de fisiere, este Directory.
Diferenta consta in faptul ca aceasta contine doar metode statice si se poate efectua doar o
singura operatie asupra unui director. Nu mai este necesar construirea unui obiect care sa
reprezinte directorul. Se intelege usor ca pentru situatiile cand e nevoie de efectuarea mai multor
operatii pe un director, se recomanda folosirea clasei DirectoryInfo.
Exemplu:
In directorul recent creat, vom crea fisiere pe care le vom muta, copia, sterge.
Atunci cand vrem sa creem fisiere cu ajutorul programarii in C#, putem folosi clasele File sau
FileInfo. Diferenta intre ele, consta in faptul ca File are metode statice care efectueaza o singura
operatie asupra unui fisier, iar metodele din FileInfo pot fi apelate pentru mai multe operatii
asupra unui fisier. Diferenta este aceeasi ca si in cazul Directory si DirectoryInfo.
File.Create(@"C:\DirectorTest\exemplu.txt");
File.WriteAllText(@"C:\DirectorTest\test1.txt", "text/copiere");
File.Copy(@"C:\DirectorTest\test1.txt",@"C:\DirectorTest\test2.txt");
File.WriteAllText(@"C:\DirectorTest\test1.txt", "text/mutare");
File.Move(@"C:\DirectorTest\test1.txt",@"C:\DirectorTest\test2.txt");
Clasa FileSystemInfo
Clasa FileSystemInfo reprezinta baza pentru DirectoryInfo si FileInfo. Nu se poate creea o
instanta a acesteia, pentru ca este o clasa abstracta. O lista cu metodele si proprietatile ei, aici.
Aceste clase scriu si citesc orice tip de date de baza intr-un format binar.
Inainte de folosirea claselor, vom crea un obiect de tip FileStream. Aceasta clasa creeaza sau
deschide un fisier.
//variabile
string sir = "text";
char car = 'a';
int numar = 10;
//scriem valorile in format binar
bw.Write(sir);
bw.Write(car);
bw.Write(numar);
Pentru citirea fisierului binar, creem din nou un obiect FileStream si apoi, un obiect
BinaryReader, clasa complementara a BinaryWriter.
Functia Read poate citi tipurile de date pe care functia Write le scrie. Diferenta consta in faptul
ca BinaryWriter are o singura metoda Write pentru scriere, in timp ce BinaryReader are o
metoda Read pentru fiecare tip de date (normal, pentru ca fiecare metoda va returna un tip de
data diferit). Dupa terminarea citirii, eliberam memoria prin apelul functiei Close.
//retine in variabile
string sirCitit = br.ReadString();
char carCitit = br.ReadChar();
int numarCitit = br.ReadInt16();
//afiseaza in consola
Console.WriteLine(sirCitit);
Console.WriteLine(carCitit);
Console.WriteLine(numarCitit);
//inchide si elibereaza resursele
br.Close();
Clasele TextReader si TextWriter sunt clase de baza. Clasele StreamReader si StringReader
deriveaza din TextReader. In mod similar, StreamWriter si StringWriter deriveaza din clasa
abstracta TextWriter.
StreamWritersi StreamReader sunt folosite in scrierea/citirea unui flux(stream) intr-o anumita
codificare.
Clasa FileSystemWatcher
Aceasta clasa contine metode, proprietati, care semnaleaza evenimente atunci cand au loc
modificari asupra fisierelor sau directoarelor dintr-un anumit director.
Exemplu:
Construim un obiect de tipul FileSystemWatcher, care va anunta daca un fisier dintr-un anumit
director a fost, de exemplu, redenumit.
Vom fi anuntati la redenumirea oricarui fisier sau director din directortest.
Metoda indicata de delegate pentru evenimentul de redenumire va afisa fostul nume al
fisierului/directorului si numele curent.
Obiectul watchermai poate folosi proprietatea Filter, pentru restrangerea ariei de fisiere
urmarite pentru modificari, proprieatea IncludeSubdirectories, pentru a urmari subdirectoare.
Puteti citi mai multe despre spatiul de nume System.IO, pe msdn.
Dezvoltatorii au nevoie adesea sa acceseze fisierele si directoarele pentru a obtine informatii si
a efectua modificari. Spatiul de nume System.IO furnizeaza toate clasele necesare.
Interfata IEnumerator
Obiectul enumerator este folosit pentru parcurgerea elementelor din colectie (il putem privi ca
un indicator care arata catre elementele dintr-o lista).
si metodele :
bool MoveNext() – va directiona indicatorul catre urmatorul element din lista. Va returna true,
daca mai exista in colectie un alt element, false, daca nu.
Prin construirea unui enumerator, cu ajutorul metodei GetEnumerator a unei colectii, si prin
apelarea repetata a metodei MoveNext si preluarea valorii din proprietatea Currentfolosind un
enumerator, putem naviga prin elementele unei colectii din element in element. Daca vrem sa
construim o colectie de clasa enumerabila, trebuie sa implementam interfata IEnumerable in
colectia de clase si sa furnizam implementarea interfetei IEnumerator care sa fie returnata de
metoda GetEnumerator a colectiei de clase.
In exemplul urmator, vom folosi interfetele IEnumerable si IEnumerator pentru a folosi un
foreach pentru obiecte ale clasei Cont, la fel cum il folosim pentru tipurile de baza.
Construim clasa Cont care va contine 3 variabile membru, numarul unic al contului, numele
persoanei care detine contul si soldul contului si vom creea constructorul clasei (articolul despre
introducere in programarea pe obiecte). Mai adaugam si functia toString, pe care o vom
suprascrie pentru a afisa in consola pentru fiecare obiect al clasei, toti cei trei membri (articolul
despre polimorfism).
//variabile membru
private int numarCurent;
private string numeTitular;
private double sold;
//constructor
public Cont(int numarCurent, string numeTitular, double sold)
{
this.numarCurent = numarCurent;
this.numeTitular = numeTitular;
this.sold = sold;
}
//suprascriem functia
//pentru a afisa toate variabilele membru
public override string ToString()
{
return "Nr.Crt.:" + this.numarCurent.ToString() + "\nNume:" +
numeTitular + "\nSold:" + sold.ToString();
}
Vom construi clasa Conturi, care va contine un vector de obiecte Cont. Clasa va contine si o
metoda de adaugare in lista a obiectelor si va implementa cele doua interfete (articolul despre
interfete).
//clasa Conturi
//implementeaza interfetele
class Conturi : IEnumerable,IEnumerator
{
//construim un vector
ArrayList listaConturi = new ArrayList();
//construim o colectie
ContList.AddEmployee(c1);
ContList.AddEmployee(c2);
ContList.AddEmployee(c3);
Initial, obiectul enumerator este situat inaintea primului element al colectiei. Vom apela
metoda Reset(), pentru a fi siguri de acest lucru:
ContEnumerator.Reset();
Parcurgem colectia:
Atentie! Folosirea corecta a interfetei IEnumerator presupune mai intai apelul metodei
MoveNext(), apoi accesarea proprietatii Current.
Obiectul enumerator nu are acces exclusiv asupra colectiei, parcurgerea cu ajutorul acestuia nu
este thread-safe, pentru ca in acelasi timp alte fire de executie pot aduce modificari colectiei. O
solutie ar fi blocarea colectiei in timpul parcurgerii.
In articolul urmator, vom discuta despre cateva clase din spatiul de nume System.IO.
Colectii in C#
Colectiile reprezinta un mod de stoacare a obiectelor intr-o maniera structurata. Acestea sunt
foarte utilizate in programare. Framework-ul .Net pune la dispozitia dezvoltatorilor foarte multe
clase pentru lucrul cu colectiile. Toate aceste clase sunt continute in spatiul de nume
System.Collections.
.Net defineste o colectie ca fiind un obiect care implementeaza una sau mai multe interfete :
System.Collections.ICollection, System.Collections.IListSystem, Collections.IDictionary. Astfel,
vom avea o clasificare a colectiilor din framework:
Colectii ordonate
Aceste colectii implementeaza doar interfata ICollection si se deosebesc prin faptul ca ordinea
in care elementele sunt inserate determina si ordinea in care elementele sunt regasite in colectie.
Exemplu: Stack, Queue.
Colectii indexate
Acest tip de colectie se distinge prin faptul ca accesul la elemente se face prin index, (incepand
cu 0), similar unui vector. Exemplu: ArrayList.
Stack
BitArray – este un vector care contine valoare de tip boolean, unde true este 1. false este 0.
//parcurgem tabloul
foreach (bool b in vectorBool)
{
Console.WriteLine(b);
}
Metoda And realizeaza operatia logica “AND” (articolul despre operatori in .Net):
//SAU
rezultat = vectorBool.Or(rezultat);
//SAU EXCLUSIV
rezultat = vectorBool.Xor(rezultat);
//Negatie
rezultat = vectorBool.Not();
SortedList
Aceasta clasa reprezinta o colectie de perechi cheie/valoare, care sunt sortate dupa cheie.
Elementele sunt accesate cu ajutorul cheii si al indecsilor. O colectie de tip SortedList este o
combinatie intre HashTable si un Array (articolul despre array). De ce? Pentru ca atunci cand
elementele acestui tip de colectie sunt accesate prin cheie folosind proprietatea Item, colectia se
comporta ca un HashTable. Cand elementele sunt accesate prin folosirea unui index cu ajutorul
metodelor GetByIndex sau SetByIndex, colectia se comporta ca un Array.
Colectiile de acest tip vor sorta elementele dupa cheie. Daca vom accesa o valoare prin index,
acel index va fi indicat de pozitia cheii in colectie:
//va afisa 5
Console.WriteLine("Valoarea de la pozitia a doua: " +
lista.GetByIndex(1));
In lista, ne putem da seama usor, ca, sortate, elementele vor fi in ordinea: abc, def, ghi. Astfel,
valoarea returnata va fi 5.
Stack
Clasa Stack (Stiva) reprezinta o structura de date de tip last-in-first-out (LIFO), adica ultimul
element introdus in colectie va fi primul care va iesi din colectie.
Un exemplu cu aceasta structura vom face in articolul urmator, folosind varianta generica a
clasei.
Queue
Clasa Queue (Coada) este o structura de date de tipul first-in-first-out (FIFO). Adica, primul
element intrat in colectie este primul element care va iesi din colectie.
Un exemplu cu aceasta structura vom face in articolul urmator, folosind varianta generica a
clasei.
Folosim o colectie cand este nevoie sa “punem la un loc” o serie de elemente care sunt
asemanatoare, apoi ne vom referi la ele ca la un grup de date, avand posbilitatea de a le parcurge
si de a le sorta.
Pot aparea probleme de performanta in folosirea colectiilor, pentru ca, de regula, atunci cand
apelam la colectii, inseamna ca avem de a face cu un numar mare de elemente. Implicit,
performanta aplicatiei va fi afectata. Un lucru foarte important este alegerea corecta, potrivita, a
tipului de colectie de care avem nevoie. Analizam daca se vor introduce toate elementele in
colectie la initializarea aplicatiei, sau pe parcurs. Mai tinem cont si de locul unde vor fi introduse
noile elementele in colectie (in mijloc, la sfarsit), sau daca ele vor fi stocate intr-o anumita ordine
sau nu.
Clasele din acest spatiu de nume sunt intuitive, produc o performanta buna, sunt usor de
folosit.
System.Collection.Generic
Conceptul de generic este unul foarte puternic in limbajul C#. O introducere in aceasta
caracteristica a C# 2.0 am facut in acest articol, despre List.
In ceea ce priveste clasele din cele doua spatii de nume, o corespondenta ar fi:
Generic NonGeneric
Collection<T> CollectionBase
Comparer<T> Comparer
Dictionary<K,V> Hashtable
List<T> ArrayList
SortedDictionary<K,V> SortedList
Stack<T> Stack
Queue<T> Queue
ReadOnlyCollection<T> ReadOnlyCollectionBase
Stack <T>
Colectia de tip Stack (Stiva) este o structura de date de tip LIFO ce stocheaza elemente care pot
fi apelate sau sterse printr-un singur pas. Personal, un bun exemplu de a-mi imagina o colectie de
tip stiva sunt vasele de la bucatarie care trebuie spalate. :)
Clasa Stack foloseste metoda Push pentru adaugarea unui element si metoda Pop pentru
scoaterea unui element din colectie.
Construim o clasa simpla Persoana si sa adaugam obiecte de acest tip intr-o stiva.
class Persoana
{
public string nume;
public string prenume;
stiva.Push(new Persoana("Ionescu","Ion"));
stiva.Push(new Persoana("Georgescu","George"));
stiva.Push(new Persoana("Popescu", "Paul"));
Daca vom afisa elementele stivei, vom observa ca primul element afisat este ultimul element
inserat in colectie. (Popescu, Georgescu, Ionescu).
stiva.Pop();
Putem apela proprietatea Count pentru a verifica faptul ca stiva acum va detine cu un element
mai putin decat inaintea apelarii metodei Pop. Afisam elementele stivei ca in modul de mai sus si
observam ca elementul eliminat prin apelul metodei Pop a fost cel introdus ultimul in colectie. In
exemplul nostru, Popescu.
Exista o metoda care va returna obiectul aflat “deasupra”, ultimul introdus in stiva, metoda
Peek.
Console.WriteLine(stiva.Peek().nume);
Queue<T>
Clasa Queue (C0ada) este o structura de date de tip FIFO. Adica, primul element intrat in
colectie este primul element care va iesi din colectie. Un exemplu din lumea reala este “coada”,
multimea, ce se formeaza la un ghiseu la banca.
Continuam cu acceasi clasa Persoana si vom inseara obiecte de acest tip intr-o colectie Queue.
Declaram colectia:
Clasa Queue detine metoda Enqueue pentru inserarea unui element in colectie si metoda
Dequeue pentru eliminarea primului element inserat in lista.
Afisam:
Elementele sunt afisate in ordinea in care au fost introduse, primul element inserat fiind si
primul afisat.
coada.Dequeue();
Putem apela proprietatea Count pentru a verifica numarul de elemente al colectiei, care acum
va fi mai mic cu un element decat inaintea apelarii metodei Dequeue. Afisam elementele cozii ca
in modul de mai sus si observam ca elementul eliminat prin apelul functiei Dequeue a fost cel
introdus primul in colectie. In exemplul nostru, Ionescu.
Asemanator clasei Stack, clasa Queue are metoda Peek, care va returna in acest caz, primul
element din colectie (“de la inceputul cozii”).
Console.WriteLine(coada.Peek());
Pentru lista completa a membrilor clasei Queue, siteul Microsoft.
Ambele clase contine metoda Clear pentru eliminarea tuturor elementelor din colectie.
coada.Clear();
stiva.Clear();
C# este un limbaj type-safe. Acest lucru presupune sa se stie tipul obiectului inainte de a-i
atribui o valoare.
Amintim ca, atunci cand vrem sa folosim una din colectiile generice, trebuie sa declaram
astfel:
using System.Collections.Generic;
.Net are un suport foarte vast pentru colectii. Exista foarte multe clase, metode, proprietati,
pentru a interactiona cu structuri de date variate din framework.
Ca avantaje ale colectiilor strong type: folosirea indecsilor, enumerarea, redimensionarea
dinamica.
Am lasat la urma, poate si din cauza ca e cea mai folosita, List<T>. Este alternativa la
ArrayList pe care o folosesc de cand am descoperit clasele generice. Aceasta clasa
implementeaza o serie de interfete:
[SerializableAttribute]
public class List<T> : IList<T>, ICollection<T>,
IEnumerable<T>, IList, ICollection, IEnumerable
care permit accesul la diverse facilitati despre care voi discuta mai detaliat intr-un articol viitor.
La ce e buna folosirea acestei clase? Din punctul meu de vedere ea este un ArrayList
imbunatatit, in primul rand prin type safe. List<T> este pentru mine o colectie care stie sa se
redimensioneze automat pe masura ce ii adaog noi elemente si care “stie” ce tip de elemente i-au
fost introduse (<T>). Nu o sa vin in cazul acestei clase cu exemple de cod, pentru ca am dat
anterior, sau in alte articole de pe acest blog, destule exemple, ci doar o sa enumar cateva din
situatiile practice in care poate fi folositoare.
Exemple:
1. Avem o lista de obiecte, sa zicem persoane, si trebuie sa pastram intr-o lista numai persoanele
cu varsta de peste 18 ani. Nu stim de la inceput cate persoane au peste 30 de ani. Alternativele
sunt: ori numaream rezultatele (intr-o variabila cont), tracand o data peste lista numai pentru a
numara, si creem un array de persoane de dimensiunea cont insa aceasta abordare presupune
parcurgerea de doua ori a listei, o data pentur numarare si inca o data pentru adaugarea
persoanelor cu varsta de peste 18 ani in array; alta varianta este folosirea unui ArrayList, care se
redimensioneaza dinamic pe masura ce sunt adaugate personae, insa care are dezavantajul ca la
fiecare accesare a unui obiect al listei, obiectul accesat trebuie convertit (cast) in Person; a treia
varianta pe care eu o vad este tocmai varianta care foloseste o lista generica, List<Person>, care
realizeaza o lista care se mareste dinamic si care permite accesul direct, fara cast, la obiectele
stocate in ea.
2. Citim un fisier text, si trebuie sa pastram intr-un tablou toate liniile de text care contin text,
altceva decat spatii goale. Nu stim cate linii vom gasi, asa ca cea mai potrivita abordare e crearea
unui obiect de tip List<string> unde sa pastram liniile de text gasite.
3. Avem un set de date dintr-un tabel dintr-o baza de date, sa zicem sub forma unui DataTabel, si
trebuie sa pastram numai anumite date (randuri), care indeplinesc anumite criterii, eventual sub
forma unor obiecte construite din datele de pe randul respectiv.
4. Citim un fisier XML unde avem definita limba folosita de aplicatia noastra. Numarul de
cuvinte definite este necunoscut, asa ca o lista dinamica de string este solutia cea mai potrivita.
5. Citim un fisier HTML si vrem sa extragem toate linkurile continute. Din fiecare link
construim un obiect de tip Uri pe care le pastram intr-o lista de tip List<Uri>.
In urmatorul articol, vom descrie System.Diagnostics, un spatiu de nume care contine o serie de
clase foarte interesante.
System.Diagnostics
.Net Framework contine spatiul de nume System.Diagnostics, care permite interactiunea cu event
log din Windows, monitorizeaza proceselor sistem din retea, monitorizeaza proceselor locale,
monitorizarea performantei sistemului.
Exista 3 tipuri de event log foarte des intalnite: system (stocheaza evenimente care nu sunt
legate de securitatea sistemului), securitate (stocheaza evenimente care legate de sesiunile
utilizatorilor, accesul la fisiere si registri), application (stocheaza evenimente de la toate
aplicatiile care nu au deja creat un event log specific).
Un mesaj detaliat al erorilor este recomandat doar in masura in care nu se afiseaza parole,
detalii despre utilizatori, conexiuni, etc.
O alta parte importanta pe care o trateaza spatiul de nume System.Diagnostics este
performanta. Aceasta este masurata cu ajutorul unor contoare, astfel, administratorul sistemului
sau dezvoltatorul aplicatiei poate monitoriza orice aspect al performantei aplicatiei.
Un alt exemplu: afisam indicatorul pentru afisarea memoriei Ram disponibila:
Console.WriteLine(indicatorRam.NextValue());
Clasa Process
Afisam toate procesele active la momentul respectiv, apeland functia statica GetProcesses
(procesele rulate de alti utilizatori nu sunt vizibile intotdeauna). Pentru fiecare proces, ii vom
afisa numele prin proprietatea ProcessName:
Pentru ca proprietatile unui proces sa ramana actuale si valide, apelam metoda Refresh.
p.Refresh();
Clasa Process pune la dispozitie metoda statica Start, pentru a porni un nou process. Nu trebuie
decat sa ii specificam numele fisierului executabil:
notepad.Start();
Sistemul de operare Windows expune foarte multe informatii despre computer si despre el
insusi prin intermediul WMI. Asemenea informatii sunt utile in instalarea/configurarea aplicatiei.
Performanta – se refera la viteza executiei unui program. Ea este influentata direct de calitatea
codului scris pentru aplicatie. Astfel, se pot face modificari majore sau minore asupra codului
pentru a mari viteza de executie. O recomandare este stabilirea unor puncte de performanta si
monitorizarea ei in cursul dezvoltarii aplicatiei.
Proces – in termeni generali, se poate defini ca o aplicatie care ruleaza. Un thread (fir de
executie) este unitatea de baza pe care sistemul de operare o aloca procesului.
Despre toti membrii spatiului de nume System.Diagnostics, puteti afla pe siteul msdn.com.
using System.Diagnostics;
//.......
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
//blocul de cod a carui timp de executie vrem sa il masuram
stopWatch.Stop();
Console.WriteLine("Executia a durat: " + stopWatch.ElapsedMilliseconds +
"milisecunde")
//.......
Un alt exemplu de folosire a acestei clase este penru optimizarea (sau el putin cunoasterea)
problemelor de conectare la o baza de date sau la un webservice. E important sa stii, cand scrii o
aplicatie, daca iti ia 1 secunda sa citesti ceva din baza de date sau daca iti ia o milisecunda. La fel
si pentru webservice.
Informatiile obtinute despre timpul de executie al anumitor portiuni – blocuri – de cod sunt, in
cazul meu cel putin, informatii care ajung in logurile aplicatiei. Daca scriu loguri, atunci scriu si
informatii despre timpul de executie.
HASH
O functie hash este o procedura bine definita sau o functie matematica ce converteste o cantitate
de date de dimensiuni varibile intr-o secventa de date de dimensiuni mici, de obicei de
dimensiune fixa, care depinde de tipul algoritmului folosit si nu de cantitatea (lungimea) datelor
de intrare.
O functie hash are (ar trebui sa aiba) urmatoarea proprietate: este imposibil de gasit, prin calcule,
un alt bloc de date care sa aiba aceeasi valoare hash. Cu alte cuvinte, daca pentru un bloc se date
dat calculam valoarea hash, este imposibila gasirea prin intermediul unui calcul a unui alt bloc a
carui valoare hash sa fie aceeasi. Functiile hash sunt folosite in semnaturi digitale sau pentru
verificarea integritatii datelor.
Valoarea hash are o lungime fixa indiferent de lungimea datelor pentru care a fist calculata.
Valoarea hash a doua blocuri de date ar trebui sa fie identica daca cele doua blocuri de date sunt
identice. Modificari minore in blocul de date duce la aparitia de modificari nepredictibile si
importante in valoarea hash calculata.
O functie Hash nu este inversabila. Este imposibila reconstituirea satelor care au generau o
anumita valoare hash.
.Net, in namespace-ul System.Security.Cryptography contine o serie de clase care
implementeaza divesi algoritmi hash. HashAlgorithm este o clasa abstracta, folosita ca baza
pentru diversi algoritmi de hash. In exemplul pe care vreau sa il prezint aici o sa folosesc 4 clase,
derivate din HashAlgorithm care implementeaza 4 lgoritmi de criptare: SHA1, SHA256,
SHA384, SHA512. Aceste clase sunt: SHA1Managed, SHA256Managed, SHA384Managed si
SHA512Managed.
Pentru inceput am construit un enum, in care am introdus cei 4 algoritmi pe care il voi folosi, si
cu ajutorul caruia userul va putea alege functia de hash dorita:
HashAlgorithm hash;
case HashAlgoritm.SHA256:
hash = new SHA256Managed();
break;
case HashAlgoritm.SHA384:
hash = new SHA384Managed();
break;
case HashAlgoritm.SHA512:
hash = new SHA512Managed();
break;
default:
hash = new SHA1Managed();
break;
}
Criptarea, asa cum se observa din cosul de mai sus, se aplica pe array de biti, motiv pentru care
textul este “convertit” in byte[]. Deasemeni, rezultatul criptatii este tot un array de biti, care,
pentru a fi reprezentat ca string se foloseste Convert.ToBase64String
System.Text
O expresie regulata reprezinta un set de caractere care este comparat cu un string pentru a
determina daca string-ul respectiv indeplineste cerintele unui anumit format. O expresie regulata
mai poate fi utila in extragerea/inlocuirea unor portiuni de text. Exemplu: stringuri care au
numere, siruri de caractere doar cu litere mici, siruri cu format hexadecimal.
Exemplu: vom creea doua siruri de caractere (pe primul il vom considera expresie regulata) si
vom determina daca primul sir de caractere se va potrivi cu al doilea sir de caractere.
Console.WriteLine("Expresia regulata:");
string expresieRegulata = Console.ReadLine();
Console.WriteLine("String pentru comparare:");
string stringC = Console.ReadLine();
Apelam metoda IsMatch din clasa Regex pentru a verifica daca cele doua siruri de caractere se
potrivesc. Adaugam declaratia de utilizarea a spatiului de nume
System.Text.RegularExpressions(link).
if (Regex.IsMatch(stringC, expresieRegulata))
Console.WriteLine("Potrivire!");
else
Console.WriteLine("Nepotrivire!");
Functiei IsMatch ii mai putem adauga un parametru reprezentand o enumerare a optiunilor cu
privire la modul in care se face potrivirea – RegexOptions(link).
Consideram expresia regulata ^\d{5}$ . Aceasta inseamna ca stringul va trebui sa aiba fix 5
caractere ({5}) si acestea trebuie sa fie doar cifre (\d). Inceputul sirului de caractere este marcat
de ^ si $ reprezinta sfarsitul sirului.
Expresiile regulate pot creea foarte multa confuzie, mai ales la citirea lor.:P Sunt foarte greu de
utilizat daca nu sunt cunoscute bine, insa reprezinta o metoda eficienta in validarea informatiilor
introduse de utilizator.
Pentru mai multe informatii despre clasele care lucreaza cu expresii regulate, vizitati msdn.
Fiecare sir de caractere dintr-un text este codificat folosind unul din standardele de codificare.
De cele mai multe ori, acest lucru este facut automat de .Net Framework. Sunt situatii, cand este
nevoie de a controla procesele de codificare si decodificare – interoperabilitatea cu sistemele
UNIX care folosesc alte tehnici de codificare, citirea si scrierea fisierelor in alte limbi.
Un exemplu de folosire a clasei Encoding este convertirea caracterelor in bytes, apoi codificarea
acestora in Korean.
// codificarea Korean
Encoding codificare = Encoding.GetEncoding("Korean");
// Convert ASCII bytes to Korean encoding
//convertim bytes ASCII in Korean
byte[] codat;
codat = codificare.GetBytes("Test!");
// afiseaza codurile bytes-urilor
for (int i = 0; i < codat.Length; i++)
Console.WriteLine("Byte {0}: {1}", i, codat[i]);
Cand e nevoie de unirea, concatenarea sirurilor de caractere (stringuri) folosim clasa
StringBuilder. Spre deosebire de tipul string, tipul stringBuilder poate fi schimbat fara a fi
copiat. Aceasta clasa creeaza siruri de caractere dinamice (“mutable”) – sirurile de caractere
“clasice” sunt immutable.
Exemplu:
Metoda Append adauga continutul argumentelor sale, fiecare argument avand apelata metoda
toString.
O metoda asemanatoare este AppendLine, cu diferenta ca aceasta va adauga o linie noua la
sfarsit. Deci, urmatorul sir de caractere care va fi adaugat stringbuilder-ului va fi pe o linie noua.
stringBuilder.AppendLine();
Clasa StringBuilder are mai multi constructori. Astfel, putem construi o instanta a acesteia
careia sa ii stabilim un string initial sau/si capacitatea stringbuilder-ului.
Capacitatea maxima este egala cu valoarea maxima a unui tip valoare int32 (int32.MaxValue).
In cazul in care numarul de caractere depaseste aceasta valoare, vom primi exceptie de tipul
ArgumentOutOfRangeException.
O varianta pentru clasa StringBuilder sunt vectorii de caractere, mult mai simpli. Pe acestia ii
vom folosi atunci cand stim dimensiunea, marimea absoluta a stringului si avem operatii care nu
includ iteratii.
Pentru toti membrii clasei StringBuilder, aflati mai multe detalii pe siteul Microsoft.
Spatiul de nume System.Text contine clase care permit lucrul cu ASCII,Unicode, UTF-7, UTF-
8, standarde de codificare.
Detalii mai multe despre spatiul de nume System.Text pot fi gasite pe msdn.
Sortare pe Array
De multe ori in practica stocam diverse date in array-uri. Pe aceste date, pastrate in array-uri
avem nevoie sa executam diverse operatii. Printre operatiile cel mai des folosite – cel putin de
mine – sunt operatii de sortate. Fie ca avem liste de persoane, liste de produse, liste de stringuri,
la un moment dat va trebui sa le asezam (afisam) intr-o anumita ordine.
In urma cu ceva timp am scris despre asta – .Net IComparer – se defineste un CustomComparer
care implementeaza interfata IComparer, bla, bla… Nu vreau sa reiau ceea ce am scris in
articolul precedent aici. Acum vreau sa dau o alta metoda de a face respectiva sortare, care
foloseste tot un iComparer, insa scrierea e mai scurta.
Presupunand ca avem un array de obiecte de tip Person[] oameni = new Person[]{ …. }; putem
sa il sortam scriind un comparer inline la modul urmator:
care va sorta lista (array-ul) respectiv dupa varsta persoanelor. E mai simplu, mi se pare mie, ca
si scriere decat in articolul anterior?
Codul complet este urmatorul:
Iar in metoda Main creem un array de persoane, pe care il afisam nesortat, il sortam si il afisam
inca odata pentru a vedea efetul sortari:
class Program
{
static void Main(string[] args)
{
Person[] oameni = new Person[]{
new Person("Ion", "Popescu", 22),
new Person("Geo", "Ionescu", 15),
new Person("Pop", "Vasilescu", 32),
new Person("Ady", "Georgescu", 28),
new Person("Ron", "Niculescu", 18),
new Person("Pan", "Petrescu", 22)
};
Console.WriteLine("\n\nDupa sortare:\n");
Array.Sort(oameni, delegate(Person p1, Person p2)
{
return p1.Age.CompareTo(p2.Age);
});
Console.ReadKey();
}
}
Ion Popescu - 22
Geo Ionescu - 15
Pop Vasilescu - 32
Ady Georgescu - 28
Ron Niculescu - 18
Pan Petrescu - 22
Dupa sortare:
Geo Ionescu - 15
Ron Niculescu - 18
Pan Petrescu - 22
Ion Popescu - 22
Ady Georgescu - 28
Pop Vasilescu - 32
Date&Time
In orice limbaj de programare si in orice program, la un moment dat este necesara folosirea datei
sau timpului si diverselor operatii cu acestea. In .Net exista o structura specializata care se ocupa
de data si timp, DateTime. Lucrul cu structura DateTime este simplu, intuitiv.
Exista 12 constructori in structura DateTime, si cateva metode (proprietati) statice care permit
crearea sau obtinerea unei instante a acestei structuri. Toate posibilitatile de obtinere a unui
obiect de tip DateTime prin folosirea contructorului sunt prezentate pe msdn aici. In afara de
aceasta cale, mai exista si alte posibilitati de a obtine un obiect de tip DateTime:
1. DateTime d = DateTime.Now;
2. DateTime d = DateTime.Today;
3. DateTime d = DateTime.UtcNow;
4. DateTime d = DateTime.Parse(dateAsString);
…si altele.
Astazi voi scrie cateva exemple de cod care folosesc anumite operatii cu data – pe principiul ca
un exemplu e mai bun decat 2 pagini de teorie!Asadar, programul pe care l-am scris
demonstreaza folosirea anumitor operatii simple cu date. Voi posta in continuare codul c# si apoi
voi face cateva remarci pe marginea codului, desi codul este destul de bine comentat. Codul este
impartit in mai multe metode, pentru a usura intelegerea lui (sper ca aceasta fragmentare sa nu
ingreuneze citirea!).
using System;
namespace zeltera.DateTimeDemo
{
static class DateTimeDemo
{
static Random rnd = new Random();
Console.WriteLine("\n---------\n");
Console.ReadKey();
}
/// <summary>
/// Afiseaza data de astazi
/// </summary>
static void PrintDate()
{
Console.WriteLine("Astazi: " + DateTime.Now.ToString());
}
/// <summary>
/// Afiseaza data de ieri
/// </summary>
static void PrintYesterday()
{
Console.WriteLine("Data de ieri: " + DateTime.Now.AddDays(-
1).ToShortDateString());
}
/// <summary>
/// Afiseaza data de maine
/// </summary>
static void PrintTomorrow()
{
Console.WriteLine("Data de ieri: " +
DateTime.Now.AddDays(1).ToShortDateString());
}
/// <summary>
/// Afiseaza numele zilei pentru o data specifica
/// </summary>
/// <param name="d"></param>
static void PrintDayName(DateTime d)
{
Console.WriteLine(d.DayOfWeek.ToString());
}
---------
1. PrintDate – afiseaza data de astazi. Instanta DateTime este obtinuta folosing proprietatea
statica Now
2. PrintYesterday – afiseaza data de ieri, pentru a demontra folosirea metodei AddDays
3. PrintTomorrow – afiseaza data de maine
4. PrintDayName – afiseaza numele zilei (in engleza) al datei transmise ca argument
5. GetRandomDate – genereaza o data aleatoare
6. diferenta dintre doua date (exprimata in zile) – calculeaza diferenta dintre doua date si
afiseaza rezultatul ca numar de zile. Aici se poate vedea si folosirea compararii a doua
date: TimeSpan ts = d1 > d2 ? d1 – d2 : d2 – d1;
Cam atat, deocamdata despre lucrul cu data (cu timpul se lucreaza in mod asemanator). Intr-unul
din articolele urmatoare voi vorbi despre foramtarea pentru afisare a unei instante a DateTime.
Microsoft a introdus, in versiunea 3.5 a .Net framework posibilitatea de a extinde o clasa fara a
creea o noua clasa, derivata din clasa pe care vrem sa o extindem. Pana in versiunea 2.0, pentur a
introduce (adauga) noi metode unei clase singura posibilitate era crearea unei noi clase, care sa
mosteneasca clasa pe care vrem sa o extindem. Daca clasa pe care o vroiam completata era
declasata sealed, derivarea ei nu este posibila. Extinderea rezolva problema claselor declarate
sealed – orice clasa accepta extinderi.
Mai concret, ce e aia? Sa luam un exemplu simplu: clasa String – are o serie de metode care se
aplica pe sirurile de caractere. Printre metodele pe care le pot folosi nu se gaseste insa, de
exemplu, o metoda care sa imi inverseze (reverse) un string. Am mai multe posibilitati: scriu o
metoda in locul unde am nevoie care sa faca asta, scriu codul de inversare inline (e mic… 2 linii,
doar), imi creez o clasa de tip CommonHelperFunctions unde pun toate metodele ajutatoare etc.
Sau… extind clasa String adaugandu-i o metoda noua: Reverse();
Pentru a exemplifica mai bine, am ales sa extind clasa String adaugandu-i urmatoarele metode:
Reverse, IsDate, IsInteger, IsDouble, CountNonEmptyChars, CountWords, AlternateCase.
Numele metodelor pe care vreau sa le introduc spun (in engleza, ce-i drept) cam ce face
respectiva metoda. Pentru a implementa ceea ce am propus mai sus se procedeaza in felul
urmator: se creeaza o clasa publica, statica, in care se definesc metodele repsective ca metode
statice si care accepta ca prim parametru ceva de genul urmator: this string str.
/// <summary>
/// Reverse the string
/// </summary>
/// <param name="str">string to reverse</param>
/// <returns>reversed string</returns>
public static String Reverse(this String str)
{
if (str == null)
return null;
StringBuilder rev = new StringBuilder();
for (int i = str.Length - 1; i >= 0; i--)
rev.Append(str[i]);
return rev.ToString();
}
///
<summary>
///
Check is a string represent a valid date
///
</summary>
///
<param name="str">string to check</param>
/// <returns>true if the string represent a valid date, else
false</returns>
public static bool IsDate(this String str)
{
try
{
DateTime.Parse(str);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Check if the string is a valid number (integer)
/// </summary>
/// <param name="str">string to check</param>
/// <returns>true if the string represent a integer, else
false</returns>
public static bool IsInteger(this String str)
{
try
{
int.Parse(str);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Check if the string is a valid number (double)
/// </summary>
/// <param name="str">string to check</param>
/// <returns>true if the string represent a double, else
false</returns>
public static bool IsDouble(this String str)
{
try
{
double.Parse(str);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Counts all the not empty chars on the given string
/// </summary>
/// <param name="str">string to count</param>
/// <returns>the number of non empty chars found</returns>
public static int CountNonEmptyChars(this string str)
{
int counter = 0;
for (int i = 0; i < str.Length; i++)
{
if (Char.IsWhiteSpace(str[i]))
continue;
counter++;
}
return counter;
}
/// <summary>
/// Count the words on a given string
/// </summary>
/// <param name="str">a phrase (string)</param>
/// <returns>number of words</returns>
public static int CountWords(this string str)
{
return str.Split(separators.ToCharArray(),
StringSplitOptions.RemoveEmptyEntries).Length;
}
///
<summary>
///
Transforms a string by alternating the letters case
///
</summary>
///
<param name="str">string to transform</param>
/// <param name="startsWithUpper">build the transformed string
starting with a uppercase char</param>
/// <returns> the transformed string</returns>
public static string AlternateCase(this string str, bool
startsWithUpper)
{
if (str == null)
return null;
StringBuilder alt = new StringBuilder();
bool upc = startsWithUpper;
for (int i = 0; i < str.Length; i++)
{
if (upc)
alt.Append(Char.ToUpper(str[i]));
else
alt.Append(Char.ToLower(str[i]));
upc = !upc;
}
return alt.ToString();
}
}
Acum, de fiecare data cand vrem ca aceste metode sa fie disponibile pentur a fi folosite cu clasa
String, trnuie doar sa folosim o instructiune using care sa importe numele de spatiu unde am
definit clasa StringExtensions.
Odata inclus numele de spatiu in proiectul curent, pentur orice obiect de tip string vom avea
disponibile toate cele 7 metode definite. In plus, aceste metode vor fi vizibile si in intelisense-ul
visual studio (cam asa):
class Program
{
static void Main(string[] args)
{
string testString = "The quick brown fox jumps over the lazy dog";
Console.WriteLine("Reverse:");
Console.WriteLine(testString.Reverse());
Console.WriteLine("Alternate case:");
Console.WriteLine(testString.AlternateCase(true));
Console.ReadKey();
}
}
Reverse:
god yzal eht revo spmuj xof nworb kciuq ehT
Caractere nevide: 35
Numar de cuvinte: 9
Alternate case:
ThE QuIcK BrOwN FoX JuMpS OvEr tHe lAzY DoG
FAQ:
2. Q: Cum pot totusi folosi extensiile daca nu am Visual Studio mai nou de
2005?
A: Instaleaza unul. Microsoft ofera versiuni express ale ultimului Visual
Studio (gratis).
Clasa Process
Sunt situatii in care din aplicatia pe care o scriem avem nevoie sa pornim o alta aplicatie. Daca
lucram in .Net asta se face cu ajutorul clasei Process aflata in namespace-ul System.Diagnostics.
Astazi o sa fac o scurta prezentare a acestei clase, urmand ca in viitor sa revin cu mai multe
detalii si mai multe exemple.
using System.Diagnostics;
//.... cod
//.... cod
Codul de mai sus, asa cum ai ghicit, deschide o sesiune de notepad. notepad.exe se poate inlocui
cu orice alt executabil (cale absoluta, relativa, numele executabilului daca el este definit prin nu
stiu ce variabile de system).
Insa nu e singura posibilitate. Nume aplicatiei poate fi inlocuit cu… un url. Codul devine:
//…. cod
static void Main(string[] args)
{
Process p = new Process();
p.StartInfo = new ProcessStartInfo(“http://google.com/”);
p.Start();
}
//…. cod
care, evident, va deschide browserul default al sistemului si va incarca pagina specificata (google
in cazul asta).
Odata pornit, un proces poate fi controlat in diverse feluri. Un mod de a controla un process este
sa il opresti. Asta se face cu ajutorul metodei Kill() asociata obiectului:
using System.Diagnostics;
//.... cod
//.... cod
Asa cum se observa, obiectul de tip Process are o proprietate p.StartInfo de tip ProcessStartInfo
unde se definesc practic parametrii procesului care va fi pornit.
Cred ca destul de utila este pornirea unui proces care reprezinta o aplicatie de tip consola.
Aceasta difera un pic de procesele de tip fereastra, prin faptul ca din consola se primesc date
(output-ul consolei) sau, eventual, se transmit date. Pentru a executa si primi output-ul unei
aplicatii de tip consola se procedeaza asa:
using System.Diagnostics;
//.... cod
p.StartInfo = psi;
p.Start();
while (!p.StandardOutput.EndOfStream)
{
Console.Write(Char.ConvertFromUtf32(p.StandardOutput.Read()));
//Console.WriteLine(p.StandardOutput.ReadLine()); //citeste linie
cu linie
//Console.WriteLine(p.StandardOutput.ReadToEnd()); //citeste tot
out-put-ul ca bloc - nu trebuie executat in block while.
}
Console.WriteLine();
Console.WriteLine("Exit code: {0}", p.ExitCode);
Console.ReadKey();
}
//.... cod
Marea majoritate a programelor pe care le folosim accepta la executie o serie de parametri. Asta
arata cam asa: numeProgram.exe [lista de parametri], unde [lista de parametri] reprezinta o
lista optionala de parametri transmisi programului numeProgram.exe.
Cei care au folosit sisteme de operare MS_DOS sau unix/linux stiu cel mai bine cat de util este
ca un program sa poata fi executat cu o lista de parametri si sa nu trebuiasca sa interactionam cu
el pe parcursul executiei.
Sa presupunem ca am un program care muta fisierele din directorul Images in directorul Pictures.
Cand programul intalneste un fisier care este ReadOnly sau Hidden ar trebui sa ceara
confirmarea (asa cum Windows Explorer face) pentru a muta fisierul. Sunt cazuri cand vreau sa
nu fiu intrebat. Vreau sa pot sa ii spun de la inceput: nu ma intreba, daca exista ceva ReadOnly,
muta fara sa intrebi, sau treci mai departe. Cum as vrea sa pot scrie asta? Cam asa: muta.exe -ro
yes -h no. Tradus, asta ar suna cam asa: daca fisierul care trebuyie mutat e ReadOnly (-ro), muta-
l. Daca fisierul e hidden (-h), nu il muta.
Un exemplu mai bun decat cel prezentat anterior este batranul notepad.exe. Cand vrem sa vedem
un fisier text, pornim Windows Explorer, gasim fisierul care ne intereseaza si cu un simplu dublu
click fisierul respectiv este deschis in notepad. Cum functioneaza asta? Simplu: windowsul
executa, in fundal, o comanda de genul: notepad.exe fullPath_fisier.txt, unde fullPath_fisier.txt
este parametru trimis aplicatiei notepad.
O alta operatie pe care o putem face prin intermediul programului notepad e sa tiparim un fisier
text. Pentru a face asta, ne folosim de un alt argument: notepad.exe /P fullPath_fisier.txt.
Scopul articolului de astazi e sa construiesc o aplicatie simpla, consola, care sa accepte o lista de
parametri. Voi construi o aplicatie consola pentru a simplifica exemplele oferite, insa orice
executabil (consola, fereastra) poate primi argumente.
Asa cum probabil stiti, executia unui program incepe in metoda Main. Aceasta metoda arata cam
asa:
Dupa cum se poate vedea, metoda Main are un argument: string[] args, care reprezinta exact
ceea ce utilizatorul va trimite ca parametru/i programului nostru. Array-ul va fi creat prin ruperea
string-ului trimis ca parametru acolo unde apar spatiile albe (space).
Pentru a verifica daca am primit sau nu parametri, este suficient sa facem urmatoarea verificare:
if(args.Length > 0)
{
//...avem ceva parametri
}
Hai sa creem un program simplu, un fel de hello world, care sa primeasca ceva parametri si sa si
faca ceva cu ei.
if(args.Length > 0)
{
//...avem ceva parametri
for(int i=0; i<args.Length; i++)
{
if(args[i] == "-fn" && i + 1 < args.Length) //verificam daca
avem cava dupa -fn
{
fName = args[i+1];
}
if(args[i] == "-ln" && i + 1 < args.Length) //verificam daca
avem cava dupa -ln
{
lName = args[i+1];
}
}
}
else
{
Console.WriteLine("Lipsesc parametrii");
return;
}
Console.WriteLine("Salut {0} {1}", fName, lName);
}
Daca userul nu specifica altceva in linia de comanda, atunci aceste valori ar trebui folosite.
Foarte multe aplicatii accepta parametri transmisi in linia de comanda, insa sunt si foarte mult
programatori, sau companii, care nu insclud optiunea de a transmite acest tip de parametri. Poate
data viitoare cand scrii o aplicatie te gandesti: ce ar fi daca in loc sa pot deschide un fisier word
cu dubluclick din windows explorer ar trebui sa deschid Word, sa apas Open, sa caut locul unde
e fisierul pe care vreau sa il deschis etc.
Am scris acest articol mai mult pentru a aminti programatorilor de aceasta posibilitate si mai
putin pentru a da exemple bune de folosire.
DEBUG
Una dintre cele mai importante facilitati pe care o ofera Visual Studio este cea de Debug. Ce
inseamna asta? A face Debug inseamna a analiza ce se intampla, la executie, in interiorul
aplicatiei asupra careia facem Debug. Aceasta operatie are ca scop gasirea si repararea erorilor
(bugs) dintr-o aplicatie. Aceasta operatie se face cu ajutorul uor instrumente speciale, numite
debuggers. Cu ajutorul acestor unelte se poate vedea la runtime ce se intampla in timpul
executiei aplicatiei asupra careia se face operatia de debug. In cazul Visual Studio uneltele sunt
incluse in pachetul Visual Studio, userul netrebuind sa instaleze separat nimic.
Exista, din cate stiu eu, doua metode de a porni o operatie de debug: prin apasarea butonului
Start Debuging din toolbar-ul Standard (sau comanda din meniul Debug) sau, metoda a doua,
prin atasarea de o aplicatie care ruleaza, folosind comanda Attach to Process, din acelasi meniu.
Vezi in imaginile urmatoare cele doua optiuni.
O captura de ecran pentru butonul debug:
O sa explic cum se folosesc fiecare dintre aceste optiuni. O sa scriu un program scurt, caruia o sa
ii fac Debug. Acest program, simplu, care contine, deocamdata, numai metoda Main, este
urmatorul:
class Program
{
static void Main(string[] args)
{
Console.Write("Name: ");
string str = Console.ReadLine();
for (int i = 0; i < 5; i++)
{
Console.WriteLine(i + ". Hello " + str);
}
}
}
Tot ce face acest program e sa citeasca de la tastatura un string si sa afiseze de 5 ori mesajul
Hello plus stringul citit de la tastatura, iar in fata fiecarui rand afisat apare si numarul itinetatiei.
La executie vedem ceva de genul:
Ady
0. Hello Ady
1. Hello Ady
2. Hello Ady
3. Hello Ady
4. Hello Ady
In imaginea anterioara se observa cum linia unde a setat primul breakPoint este evidentiata
diferit, cu galben. Asta inseamna ca programul este oprit in puctul respectiv. Pentru a continua, e
nevoie sa ii spunem Debugger-ului sa mearga mai departe – apasam tasta F5 pentru a continua
executia programului, tasta F10 pentru a continua executia prin executarea urmatoareil linii de
program (numai o linie) sau F11 pentru a continua executia prin salt in interiorul functiei care se
executa in linia in care am oprit – numai in cazul in care suntem intr-o linie in care se executa
cod definit intr-o alta metoda – nu e cazul actual.
Eu apas acum F5. Programul isi continua executia si eu trebuie sa introduc un nume. Scriu in
fereastra consola un nume: “Ady” si apas enter. Programul isi continua executia pana la
urmatorul breakPoint unde debugger-ul preia controlul. In fereastra codului, in momentul in care
sunt in Debug Mode am acces la memoria aplicatiei, in sensul ca pot vedea starea obiectelor care
imi compun aplicatia. Daca pozitionez cursorul mouse-ului deasupra numelui unui obiect, Visual
Studio imi va afisa valoarea respectivei valori asa cum este ea in momentul respectiv in
memorie. Pozitionand cursorul mouse-ului deasupra variabilel str, voi vedea valoarea ei,
respectiv numele introdus de mine in fereastra Consola:
Daca versiunea de Visual Studio este Profesional, valoarea variabilelor se poate medifica in
fereastra debug, la runtime. Insa despre asta intr-un alt articol. Continui executia programului, de
data asta linie cu linie, apasand tasta F10. Observ ca la fiecare apasare a tastei respective executia
avanseaza, iar linia care se executa in momentul curent este evidentiata. Acum urmaresc valorile
variabilei i in interioru ciclului for:
A doua metoda de a intra in modul debug este prin atasarea la un proces care ruleaza. Pornim
aplicatia (apasam Ctrl+ F5, adica start without debugging, sau executam din folderul
\HelloWorld\HelloWorld\bin\Debug\HelloWorld.exe aplicatia. In momentul in care aplicatia a
ajuns in puctul in care ne cere sa introducem numele, revenim la visual studio si din meniul
Debug apelam Attach to process:
In acest moment avem o fereastra care contine numele proceselor care sunt executate in
calculator:
Selectam procesul care are numele identic cu numele aplicatiei noastre si apasam butonul Attach.
Din acest moment procesul de debug continua ca si in primul caz, descris mai sus. Programul
opreste la ficare BreakPoint, poate fi executat linie cu linie, se pot modifica valorile variabilelor
(Pro) etc.
In momentul in care Visual studio este in mod Debug, click cu butonul din dreapta deschide un
meniu care are cateva comenzi importante, dintre care vreau sa evidentiez doua: Run To Cursor
si Set next Statement:
Ce sunt cele doua comenzi? Prima, Run To Cursor este utila in cazul in care vrem sa continuam
executia programului pana intr-un anumit punct, punct in care sa fie iarasi intrerupta executia.
Putem sa punem acolo un breakPoint nou, sau putem sa pozitionam cursorul in editor in locul
respectiv si sa apelam comanda Run To Cursor. A doua comanda este Set next Statement. Cu
ajutorul acesti comenzi se continua executia programului de la un punct dorit de programator –
inainte sau dupa punctul curent. La ce foloseste asta? Pentru a evita o eroare (care trebuie
corectata ulterior), pentru a relua executia unui bloc etc.
Acesta este un prim articol despre operatia de debug. Vor mai fi si altle.