.NET String

Među prvim pojmovima sa kojima se susreće programer početnik su osnovni tipovi podataka: celi brojevi, brojevi u pokretnom zarezu i nizovi karaktera, tj. stringovi. Kako već imamo jasne predstave o ovim tipovima podataka, uglavnom ih prihvatamo zdravo za gotovo. Uobičajeno je da se u literaturi i na kursevima koji obrađuju uvod u programiranje posvećuje vreme opisu kako računari barataju brojevima. Stringovi često ostaju nepravedno zapostavljeni, dešava se da čak i programeri sa iskustvom ne poznaju potpuno kako je u njihovom omiljenom programskom jeziku implementiran ovaj osnovni tip podataka. Ovo je izraženo u .NET framework-u kojim ću se baviti u ovom postu, koristiću c# za primere.

Šta su stringovi?

System.String ili skraćeno string je .NET klasa koja predstavlja niz Unicode karaktera koji se koristi da se predstavi tekst. Pod terminom niz mislim na više redom poređanih elemenata, ne nužno klasu System.Array.
Svakom elementu stringa možemo da pristupimo na osnovu njegovog indeksa, npr. ako je string definisan sa:

string s = “abcd”;
Console.WriteLine(s[1]);
// Outputs "b"

izraz s[1] ima vrednost ‘b’ koja je tipa System.Char.

Encoding

Korišćenje Unicode standarda nam omogućuje da u naše .NET stringove možemo da smeštamo naša slova i ćirilicu bez problema sa kodnim rasporedima. Na primer, ovo parče koda:

“š”.ToUpper();

radi baš ono što očekujemo. Ovo nas košta u memoriji, tako da stringovi troše 20 + [n / 2] * 4 bajtova, gde je n broj karaktera u stringu, a [n / 2] ceo deo rezultata deljenja.
Neki programski jezici podrazumevaju da stringovi sadrže ASCII karaktere, kao što je PHP

Uzmite u obzir da zauzeće memorije zavisi od implementacije i da .NET koristi string interning. Interning podrazumeva da postoji jedna kopija jedinstvenog literala (konstantne vrednosti stringa), vrednosti se čuvaju u takozvanom string intern pool-u, tako da stringovi koji sadrže istu vrednost referenciraju istu adresu u pool-u. Da bi ovaj metod funkcionisao neophodno je da stringovi budu implementirani kao nepromenljivi (immutable) objekti, ovom osobinom stringova i posledicama se bavimo u nastavku.

Immutability

U implementaciji CLR-a value types se čuvaju na steku, što podrazumeva kopiranje vrednosti pri prosleđivanju parametara koji su value types, uzimajući u obzir da stringovi dosta variraju po dužini, a mogu biti i dosta veliki, value type nije mogao biti rešenje, tako da je .NET string reference type.
Sa druge strane, često želimo da se stringovi ponašaju kao value type:

  • da stringu dodeljujemo vrednost operatorom =, a ne referencu;
  • želimo operator == upoređuju stringove po vrednosti, a ne referenci;
  • želimo da budu nepromenljivi.

Operatori dodeljivanja (=) i upoređivanja (==) su preopterećeni (overloaded) da bi radili onako kako želimo, a želimo da rade po vrednosti. Obratite pažnju da ukoliko upoređujemo objekat i string možemo nenamerno da pređemo na poređenje po referenci. Kako je u implementaciji stringova korišćen interning moguće je da će i poređenje po referenci vratiti true za iste literale, međutim nije garantovano da će za iste vrednosti stringa biti u pitanju i ista referenca, da ilustrujem primerima:


string string1 = "Test";

string1 += " String";

Console.WriteLine(string1);
// Outputs "Test String"

string string2 = "Test String";

// compares values, comparison is true
Console.WriteLine(
   "string1 == string2 {0} by value", string1 == string2
);

// compares references, comparison is false
Console.WriteLine(
   "string1 == string2 {0} by reference", string.ReferenceEquals(string1, string2)
);

Object object3 = "Test String";

// unintended reference comparasion, comparison is true
Console.WriteLine(
   "string2 == object3 {0} by reference", string2 == object3
);

string string4 = string.Copy(string2);

// compares values, comparison is true
Console.WriteLine(
   "string1 == string4 {0} by value", string1 == string4
);

// unintended reference comparasion, comparison is false
Console.WriteLine(
   "object3 == string4 {0} by reference", object3 == string4
);

Zavisno od podešavanja kompajler bi trebalo da da upozorenje “Possible unintended reference comparison”, u slučajevima kada upoređuje objekat i string. Statička metoda string. Copy alocira memoriju i kopira vrednost stringa koji je prosleđen kao parametar na novu lokaciju, tako da je moguće naterati kompajler da napravi novu kopiju iste vrednosti stringa, da smo koristili operator dodele (=), koji i koristimo u uobičajenim situacijama string4 bi dobio istu referencu kao string2 i object3. Metod ReferenceEquals, kao što mi i samo ime kaže, poredi objekte po referenci, i koristi nam da vidimo kakve bi rezultate dobijali pri poređenji stringova da operator jednakosti (==) nije preopterećen.

Kako stringovi mogu imati vrednost null, korisno je znati da string ima statičku metodu Equal, tako da možemo da upoređujemo i nulabilne stringove bez dodatne logike za obradu izuzetaka:

//True
string.Equals(null, "abc")

Nepromenljivost, tj. immutability praktično znači da se pri svakoj izmeni vrednosti stringa kreira novi objekat.  Što za posledicu ima da stringove ne menjaju metode kojima ih prosleđujemo kao parametre (osim ako to nije naznačeno ključnim rečima out ili ref). Osim što je immutability očekivano ponašanje, omogućuje nam da koristimo stringove kao ključeve za Hashtable/Dictionary, pojednostavljuje multithreading, složićete se da je sve ovo jako zgodno…
Posledica ove osobine je da se pri svakoj izmeni vrednosti kreira novi objekat, što nemilice troši resurse. Evo primera:

string s = string.Empty;
for (int i = 0; i < 10000; i++)
   s += i.ToString();

Ovo parče koda, na mom računaru, izvršava se oko 150 milisekundi, međutim kada povećam broj iteracija na 50 000 (5 puta) dobijam presporih 7.3 sekunde, što znači da algoritam ima tzv. eksponencijalnu efikasnost, tj. vreme izvršenja eksponencijalno zavisi od broja iteracija.
Rešenje je jednostavno, dovoljno je da umesto stringova koristimo StringBuilder i dobičemo mnogo bolje rezultate, kroz 10 000 iteracija protrči za 2 milisekunde. Što je još bitnije, vreme izvršenja linearno zavisi od broja iteracija, što znači da će 50 000 iteracija izvršiti za 10 milisekundi, složićete se da je ovo odlično u odnosu na više od 7 sekundi koje smo imali koristeći klasu String…


Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
   sb.Append(i);

stopWatch.Stop();
TimeSpan ts = stopWatch.Elapsed;
Console.WriteLine(string.Format("RunTime: {0:00}.{1:000}",  ts.Seconds, ts.Milliseconds));

StringBuilder je rešenje za spajanje stringova u petljama, naročito ukoliko ne znate unapred koliko će iteracija biti. Ukoliko spajate nekoliko stringova, koristite ekspresivnije string.Format ili string.Concat, na primer:

string Name = "John";
string LastName = "Doe";

string FullName = string.Format("{0} {1}", Name, LastName);
FullName = string.Concat(Name, " ", LastName);

Ukoliko koristite literale, kompajler će uraditi optimizaciju za vas, prepoznaće da su u pitanju konstante i spojiće ih u vreme kompaliranja. Znači iz ugla performansi potpuno je svejedno da li pišete:

string FullName = "John" + " " + "Doe";

ili

string FullName = "John Doe";

String.Join

S vremena na vreme naiđem na kod koji izgleda ovako:

string[] boje = { "plava", "crvena", "zelena" };
string sveBoje = string.Empty;

foreach (string boja in boje)
   sveBoje += boja + ",";

Console.WriteLine(sveBoje.TrimEnd(','));

Ne baš čitljiv i sažet kod, koji uz to može da ima i veoma loše performanse ako niz ima puno elemenata.

Sada naravno znamo da možemo da dobijemo na performansama koristeći StringBuilder:

string[] boje = { "plava", "crvena", "zelena" };

StringBuilder sveBoje = new StringBuilder();

foreach (string boja in boje)
   sveBoje.AppendFormat("{0},", boja);

Console.WriteLine(sveBoje.ToString().TrimEnd(','));

Performanse su bolje, ali i dalje ne izgleda najjelegantnije, naročito bode oči ovaj višak zareza koji moramo da trimujemo posle petlje. Da li može bolje? Statička metoda string.Join radi baš to što smo hteli i brža je od koda koji koristi StringBuilder:

string[] boje = { "plava", "crvena", "zelena" };

Console.WriteLine(string.Join(",", boje));

Da napomenem da postoji i inverzna metoda string.Split.

Stringovi su nešto komplikovaniji nego što bi očekivali, u ovom postu sam se bavio osnovama koje nam daju sliku šta se dešava ispod haube. Postoji još tema koje se takođe tiču stringova i tekstualnih podataka, u ovom postu nisam se dotakao upoređivanja stringova, što bi nas odvelo u pravci internacionalizacije, ali ovo su posebne teme koje ću sačuvati za drugu priliku.

Ukoliko vas interesuje istorija, Joel Spolsky je pisao na temu različitih implementacija stringova pre .NET-a: Back to Basics

Pages:

Leave a Comment

NOTE - You can use these HTML tags and attributes:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>