Cuprins >> Șiruri De Caractere Și Procesarea De Text > String Builder
Articol de importanță medie

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.

21/04/2017 08:53:00 AM
Numbers: 123456789101112131415161718192021222324252627282930313233343536373839404142
434445464748495051525354555657585960616263646566676869707172737475767778798081828384
858687888990919293949596979899100101102103104105106107108109110111112113114115116117
118119120121122123124125126127128129130131132133134135136137138139140141142143144145
146147148149150151152153154155156157158159160161162163164165166167168169170171172173
174175176177178179180181182183184185186187188189190191192193194195196197198199200201
202203204205206207208209210211212213214215216217218219220221222223224225226227228229
230231232233234235236237238239240241242243244245246247248249250251252253254255256257
258259260261262263264265266267268269270271272273274275276277278279280281282283284285
286287288289290291292293294295296297298299300301302303304305306307308309310311312313
314315316317318319320321322323324325326327328329330331332333334335336337338339340341
342343344345346347348349350351352353354355356357358359360361362363364365366367368369
3703713723733743
 
21/04/2017 08:54:55 AM
 

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:

21/04/2017 08:59:59 AM
Numbers: 123456789101112131415161718192021222324252627282930313233343536373839404142
434445464748495051525354555657585960616263646566676869707172737475767778798081828384
858687888990919293949596979899100101102103104105106107108109110111112113114115116117
118119120121122123124125126127128129130131132133134135136137138139140141142143144145
146147148149150151152153154155156157158159160161162163164165166167168169170171172173
174175176177178179180181182183184185186187188189190191192193194195196197198199200201
202203204205206207208209210211212213214215216217218219220221222223224225226227228229
230231232233234235236237238239240241242243244245246247248249250251252253254255256257
258259260261262263264265266267268269270271272273274275276277278279280281282283284285
286287288289290291292293294295296297298299300301302303304305306307308309310311312313
314315316317318319320321322323324325326327328329330331332333334335336337338339340341
342343344345346347348349350351352353354355356357358359360361362363364365366367368369
3703713723733743
 
21/04/2017 08:59:59 AM
 

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.