A string típus tervezési hibás?
Időnként előkerülő téma az interneten, hogy a .NET keretrendszerben a string típus tervezési hibás nem is egy, hanem több szempontból is. Ezen cikkben ennek járunk egy picit utána.
Bevallom őszintén, a cikk címe nem titkoltan hatásvadász és a clickbait vonalon mozog, ezért elnézést kérek. De ha már úgyis itt vagyunk, akkor vessük bele magunkat a dolgokba és tárjuk fel a vélt vagy valós sérelmeket.
A string egy osztály és nem struktúra
A .NET keretrendszer világában vannak az érték és a referencia típusok. Az érték típusok a stack-en tárolódnak, míg a referencia típusok a heap-en. A beépített összes alaptípus (int, byte, char, float, double, stb…) a string kivételével érték típusként van megvalósítva. Korábban visszahallottam több helyről, hogy a string emiatt egy tervezési hiba.
Azért nem az, mert a stack mérete limitált a heap-hez képest. A stack általában metódusok paramétereinek érték átadására szolgál, illetve itt tárolódnak az aktuálisan végrehajtott metódus lokális változói, amelyek a metódusból kilépve felszabadulnak. Ebből adódóan mérete MiB nagyságrendű, míg manapság egy átlag PC memóriája is több GiB nagyságrend. Éppen ezért nem lenne szerencsés szövegeket tárolni a stack-en. Egy string változó elméletben 4GiB mennyiségű szöveget tud tárolni (2 byte karakterenként * int.MaxValue).
Ekkora mennyiséget nem lenne szerencsés a stack-en tárolni, mivel egyszerűen nem maradna hely kb semmi másnak. Itt megjegyzem, hogy csak egyetlen egy szövegről beszéltünk. Több esetén még drámaibb lenne a helyzet.
Lényegében ezért tárolódnak a szövegek a heap-en és ezért van referencia típusként megvalósítva. Ennek ellenére megértem, hogy frusztráló tud lenni, hogy van üres és null értéke is, ami az esetek 99% százalékában azonos jelentéssel bír a programokban. Ezen a helyzeten javít a nullable reference types, de ha a projekt régi C# verziót használ, akkor zavaró lehet, de ettől még nem tervezési hiba.
Sőt! Egy hatalmas előnye is van, hogy referencia a string. Ha allokálnánk két szöveget azonos tartalommal, akkor a 2. alkalommal a szöveg nem kell, hogy allokálódjon, elég, ha a referenciát a már allokált szövege állítjuk. Mivel ez egy immutable típus, vagyis a módosítás mindig egy új allokációt eredményez, ez nem jelent semmi problémát, cserébe viszont spórolni lehet a memóriával. Ez egyfajta de-duplikáció
A string.Empty valójában egy field és nem konstans
Szintén egy fájó pont lehet, mivel ha van egy hasonló kódrészletünk, akkor az bizony nem fog fordulni:
void Valami(string parameter = string.Empty)
{
//kód
}
A fenti metódus deklaráció előnye, hogy szebben olvasható lenne, mint idézőjelekkel, csak mégsem fordul, mivel az Empty értéke fordítási időben nem meghatározható. Ez elnézve tényleg hibának tűnik, de ássuk bele magunkat, hogy miért is van ez. Bevallom őszintén ehhez nekem is bele kellett néznem a .NET forráskódjába, hogy rájöjjek. A string típus forrásánál meg is kapjuk a választ. A releváns részt bemásoltam:
// The Empty constant holds the empty string value. It is initialized by the EE during startup.
// It is treated as intrinsic by the JIT as so the static constructor would never run.
// Leaving it uninitialized would confuse debuggers.
//
//We need to call the String constructor so that the compiler doesn't mark this as a literal.
//Marking this as a literal would mean that it doesn't show up as a field which we can access
//from native.
public static readonly String Empty;
A kommentek alapján látható, hogy az Empty miért nem konstans: Ha az lenne, akkor a fordító valószínűleg kioptimalizálná, illetve a korábban említett de-duplikáció se működne valószínűleg rendesen. Nyomósabb ok azonban, hogy akkor natív kódból (COM+ és CLR-be behívó egyéb dolgok) nem lenne elérhető.
A string hossza nem azonos a benne tárolt karakterekkel
Ez elsőre szintén egy meglepő furcsaság lehet, de aki járatos az informatikában és az UTF kódok világban, annak ez az információ mondhatni magától érthető.
A char C# és .NET esetén egy UTF-16 karaktert jelöl. Viszont az UTF-16 egy változó hosszúságú karakterkódolás. Ez azt jelenti, hogy az ábrázolandó szimbólum egy vagy két karakterből állhat, attól függően, hogy a kódtáblában hol helyezkedik el. Ez hétköznapi szövegek esetén nem okozott korábban problémát, de az emoji-k terjedésével valós problémává vált, mivel ezek a kódtábla olyan részén helyezkednek el, hogy egy karakter nem elég az ábrázolásukra. Nézzünk erre egy példát:
string teszt = "🐇";
Console.WriteLine(teszt.Length);
A fenti pici kódrészlet kimenete 2-t fog kiírni, pedig 1 karakterből áll. Ez a működés, vagy viselkedés okozhat problémákat bizonyos esetekben, de nem tervezési hiányosság.
Végszó
A string típus rendelkezik furcsaságokkal, de a keretrendszert okos emberek alkották. Minden furcsának tűnő döntés mögött nyomós érvek állnak, hogy miért pont úgy lett kialakítva, megvalósítva.