Explicam la un moment dat că string este un tip de dată imuabil. Aceasta înseamnă că odată ce atribuiți o valoare unei variabile string, nu o mai puteți modifica în mod direct. Aceasta înseamnă și că orice operație cu șiruri de caracter care utilizează funcții gen Trim(), Replace(), ToUpper(), etc, va crea de fapt un șir nou în memorie, unde valoarea rezultată va fi stocată, iar valoarea veche, inițială, va fi ștearsă. Acest comportament este unul foarte complex, implicând pointeri și referințe, și are multe avantaje, dar în unele cazuri poate cauza probleme de performanță.
Cel mai groaznic exemplu de performanță proastă la care mă pot gândi este concatenarea șirurilor în interiorul unei bucle. NICIODATĂ să nu faceți asta! Nu am aflat încă despre memoria dinamică sau colectorul de gunoi, așa că nu pot explica pe deplin motivele pentru care acest lucru are ca rezultat o performanță atât de proastă, dar totuși, să încercăm să înțelegem motivele din spatele acestui lucru. Pentru a le înțelege, mai întâi trebuie să înțelegem ce se întâmplă când folosim operatorii + sau += pe șiruri de caractere. Să luăm în considerare următorul cod:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
using
System;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
string prenume = "Ion ";
string nume = "Popescu";
string numeComplet = prenume + nume;
numeComplet = "Ioana";
Console.ReadLine();
}
}
}
|
Ce se va întâmpla în memorie? Când declarăm variabilele prenume și nume, acestea vor fi stocate într-o memorie specială numită Heap. Când le concatenăm, atribuim valoarea rezultată unei a treia variabile. Deci, acum avem trei valori în memorie și trei variabile care indică spre ele, iar acesta este rezultatul așteptat. Cu toate acestea, atunci când modificăm valoarea variabilei numeComplet, alocam de fapt o nouă zonă de memorie, stocăm noul șir în ea și ștergem valoarea șirului care a fost stocat în locația anterioară. Acest proces poate dura timp, mai ales când este repetat de multe, multe ori, ca într-o buclă.
În C#, nu trebuie să ne facem griji cu privire la ștergerea manuală a valorilor variabilelor de care nu mai avem nevoie, ca în alte limbaje, cum ar fi C sau C++. Există o componentă specială numită Garbage Collector care curăță automat orice resurse nefolosite, dar aceasta are un preț: ori de câte ori efectuează curățarea, durează destul de mult și în general încetinește viteza de execuție. Deci, nu numai că forțăm GC să curețe memoria tot timpul, ci și facem ca programul să transfere caractere dintr-un loc în altul în memorie (când se execută concatenarea șirurilor), operație care este lentă, mai ales dacă șirurile sunt lungi.
Să demonstrăm acest lucru. Să concatenăm toate numerele de la 0 la 200.000 într-un șir. Modul obișnuit de a face acest lucru ar fi așa:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
using
System;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now);
string collector = "Numere: ";
for (int index = 1; index <= 200000; index++)
collector += index;
Console.WriteLine(collector.Substring(0, 1024));
Console.WriteLine(DateTime.Now);
Console.ReadLine();
}
}
}
|
Afișăm ora curentă în momentul în care începem concatenarea (deși încă nu am aflat despre obiectul DateTime), apoi executăm îmbinarea șirului în interiorul buclei și, în final, afișăm din nou ora curentă, pentru a putea compara timpul scurs.
După cum puteți vedea, pe un procesor Intel Quad Core i5 4590, care rulează la 3,3 GHz, acest lucru a durat aproape două minute. Unii dintre voi ar putea spune: „da, dar totuși, sunt 200.000 de operații de efectuat! Asta trebuie să ia ceva timp!” și ați greși. Calculatoarele sunt FOARTE bune la efectuarea de operațiuni repetate, extrem de rapide, în special pe CPU-urile moderne din zilele noastre.
Dar, cel mai important, în 2017, a-i face pe utilizatori să aștepte 2 minute pentru o operațiune este aproape inacceptabil, iar mulți o vor închide înainte ca aceasta să aibă șansa de a se finaliza.
Problema cu procesarea în buclele consumatoare de timp este legată de modul în care funcționează șirurile în memorie. Fiecare iterație creează un nou obiect în Heap și indică referința la acesta, așa cum am explicat. Acest proces necesită un anumit timp fizic.
La fiecare pas se întâmplă mai multe lucruri:
1. O zonă de memorie este alocată pentru înregistrarea următorului număr de rezultat al concatenării. Această memorie este utilizată doar temporar în timpul concatenării și se numește tampon (buffer).
2. Vechiul șir este mutat în noul buffer. Dacă șirul este lung (să zicem 500 KB, 5 MB sau 50 MB), acest proces poate fi destul de lent!
3. Următorul număr este concatenat la buffer.
4. Buffer-ul este convertit într-un șir.
5. Șirul vechi și buffer-ul temporar devin neutilizate. Mai târziu, sunt distruse de Garbage Collector. Aceasta poate fi, de asemenea, o operațiune lentă.
O modalitate mult mai elegantă și mai adecvată de a concatena șiruri într-o buclă este utilizarea clasei StringBuilder. Știu, încă nu am vorbit despre clase, dar nu vă preocupați cu asta. Să vedem doar cum funcționează. În primul rând, StringBuilder este o clasă care servește la construirea și modificarea șirurilor de caractere. Depășește problemele de performanță care apar la concatenarea șirurilor de tip string. Clasa este construită sub forma unei matrice de caractere și ceea ce trebuie să știm despre ea este că informațiile din ea pot fi modificate liber. Modificările care sunt necesare în variabilele de tip StringBuilder, sunt efectuate în aceeași zonă de memorie (buffer), ceea ce economisește timp și resurse. Modificarea conținutului nu creează un obiect nou, ci pur și simplu îl schimbă pe cel actual. Să rescriem codul de mai sus în care am concatenat șiruri într-o buclă. Observați că tipul StringBuilder este declarat într-o bibliotecă externă numită System.Text, așa că va trebui să adăugați o altă directivă using. Dacă vă amintiți, anterior operația a durat 2 minute. Să măsurăm cât timp va dura aceeași operație dacă folosim StringBuilder:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
using
System;
using
System.Text;
namespace
BunaLume
{
class
Program
{
static void Main(string[] args)
{
Console.WriteLine(DateTime.Now);
StringBuilder sb = new StringBuilder();
sb.Append("Numere: ");
for (int index = 1; index <= 200000; index++)
sb.Append(index);
Console.WriteLine(sb.ToString().Substring(0, 1024));
Console.WriteLine(DateTime.Now);
Console.ReadLine();
}
}
}
|
După rularea codului, obținem acest rezultat:
Nu știu despre voi, dar 200.000 de operații în mai puțin de o secundă, acum, asta numesc o creștere a performanței! Timpul necesar este de fapt de ordinul milisecundelor!
Modul în care folosim StringBuilder este prin crearea unei noi instanțe a acestuia și apoi să folosim metoda Append() pentru a concatena șiruri de caractere la acesta. Veți înțelege mai bine acest proces când veți învăța capitolul următor. Deocamdată, amintiți-vă că StringBuilder este o modalitate MULT mai eficientă de concatenare a șirurilor.