A szöveg programozási szempontból összetett típusnak számít. Az implementáció megvalósítása programozási nyelvenként eltérő. C esetén szöveg típus nem létezik, ott karakterek tömbjeiről beszélünk. A memóriában egy karakterlánc végét a string végjel karakter jelzi, ami a \0 karakterrel fejezhető ki.
Pascal esetén a szövegek kezelése némileg fejlettebb. Ott az első byte tárolja a szöveg hosszát, majd ezt követheti 255 byte információ, (egy byte maximális értéke 255) a szövegről.
Mindkét megoldásnak vannak hibái. A C megoldás hibája, hogy bárhol a szövegbe szúrható egy \0 jel, ami után nincs kiírás. De ugyanilyen módszerrel a teljes memória is kiíratható, vagy legalábbis jó nagy része, ha a kiírásra kerülő szöveg végéről eltávolítjuk a szöveg vége jelet. Ebben az esetben a kiírás a memóriában található legközelebbi \0 karakterig folytatódik, ami biztonságilag nem éppen a legjobb.
A Pascal implementáció hibája, hogy a szöveg csak 255 karakterből állhat ezzel a megoldással, viszont DOS időkben ez bőven jó volt.
A C# megoldása tekinthető a kettő kombinációjának: a szövegek karakterek tömbjeként vannak tárolva, viszont tudjuk, hogy minden C# tömb rendelkezik egy hossz tulajdonsággal, ami 32 bites egész (int) szám. Így elméletben egy szöveg 231-1 karakter hosszú lehet. Itt direkt nem byte-ot említettem, mert a C# UTF-16 karakter kódolást használ, ami azt jelenti, hogy egy karakter 2 byte helyet foglal a memóriában.
Az UTF kód
Az UTF kódolásra a hordozhatóság és a kompatibilitás miatt esett a választás. Anno az UTF létrejötte előtt az ASCII kódolást használta és használja a mai napig sok programozási nyelv.
Az ASCII kód egy karakterkódolási szabvány, amit 1963-ban dolgoztak ki annak érdekében, hogy a különböző számítógépek azonos kódokkal jelöljenek betűket. Eredetileg egy 7 bites kód, ami az angol ABC betűit és néhány írásjelét tartalmazza. Az első 32 karakter nem nyomtatható vezérlőkarakter, amelyek olyan gombokat reprezentálnak, mint a TAB és a DEL. Eredetileg a vezérlőkarakterek a kor modemei számára kerültek bele a szabványba.
A szabványt később felbővítették 8 bitesre, ami plusz 127 karakterrel egészítette ki. A plusz 127 kód jelentése kódtáblákkal módosítható, így megoldható a különböző kultúrák írásjeleinek támogatása. Később ezeket is szabványosították, azonban ez nem oldotta meg az ASCII legnagyobb problémáját.
A gond abból fakadt, hogy az eltérő kódlapok más és más karaktereket rendeltek egy adott kódhoz. Ezért, ha a szöveg eredeti kódlapja nem volt ismert, az igencsak meg tudta nehezíteni a szöveg elolvasását. Az internet térhódításával a problémát ideiglenesen úgy próbálták megoldani, hogy nem használtak ékezetes betűket a kommunikáció során. Végérvényesen a problémát az UTF kódolások elterjedése oldotta meg a 2000-es évek elején.
A Unicode kódolásokat az eltérő kódlapos ASCII kód hordozhatósági problémájának megszüntetése érdekében hozták létre. A szabvány első változata 1991-ben került publikálásra, azóta folyamatos fejlesztés alatt áll. A kódlapok problémáját az UTF úgy oldotta meg, hogy eredetileg 16 bitet alkalmazott egy karakter kódolására. Ez a lehetséges karakterek számát 256-ról 65536-ra növelte. Ma ez a kódolás UTF-16 néven ismert.
Azonban, mivel ennél jóval több írásjel használt a földön, a kódolást tovább kellett fejleszteni. Ennek köszönhetően mára az UTF kódolás 32 bites, ami elvben több, mint 4 milliárd írásjel kódolására alkalmas. A legfrissebb szabvány szerint ebből nagyjából csak 100 ezer írásjelet határoz meg. Így nem valószínű, hogy a kódolásban használt biteket egyhamar megnövelik.
Kompatibilitási okok miatt az UTF első 127 karaktere megegyezik az ASCII kódtáblázattal, azonban UTF esetén a vezérlő jelek nem használtak.
A kódolásnak három típusa terjedt el, ezek: UTF-16, UTF-32 és UTF-8. A kötőjel utáni szám a bitek számát jelenti. Unicode és UTF alatt külön bitszám jelzés nélkül a 32 bites változat értendő.
A 8 bites UTF változat a legelterjedtebb. Ez a kódolás változó bithosszúsággal jelöli a karaktereket. 8 bitet alkalmaz egy karakter kódolására, ha a karakter az ASCII kódtáblázatban is megtalálható, 16 bitet, ha a 16 bit elegendő az ábrázoláshoz és 32 bitet, ha csak 32 biten ábrázolható a karakter.
Immutable string
Mivel a szövegek karakterek tömbjeként tárolódnak, fontos beszélnünk arról, hogy ha módosítani szeretnénk egy szöveget, mi fog történni. Nézzünk egy egyszerű példát, amiben bővítjük a szövegünket:
var szoveg = "Ez egy szep";
szoveg += " nap";
szoveg += "!";
Első körben létrejön a szoveg változó a memóriában. Az első bővítés hatására lekérdezésre kerül a szoveg változó hossza, amihez hozzáadja a bővítő szöveg hosszát. Ezen információ alapján foglalásra kerül egy memória terület, amiben elfér az új szöveg. Ezek után a két rész bemásolódik az új szövegbe, majd az eredeti szoveg-re mutató referencia átírásra kerül az új szövegre mutatóra.Ugyanez ismétlődik a második bővítésnél. A menet közben feleslegessé vált szöveg példányok eltakarítását a GC fogja elvégezni.
Érezhető, hogy ez nagyon nem optimális megoldás nagy méretű, vagy sokszor elvégzett ilyen típusú műveletek esetén. Éppen ezért lehetőleg kerülni kellene a szövegek ilyen fajta összefűzését. Ennél jóval optimálisabb megoldás, ha a string típus Format metódusát használjuk, ami ugyanazokat a formátumokat kezeli, mint a Console osztály WriteLine és Write metódusai.
Az olyan objektumokat, amelyek értéke nem módosítható és minden esetben egy új objektum létrehozását eredményezik, Immutable, magyarul nem módosítható objektumoknak nevezzük. Az Immutable objektumok elsősorban funkcionális programozási nyelveknél használatosak.
Ezért sokkal jobb teljesítmény érhető el a StringBuilder segítségével, ha nagy szövegeket kell összefűzni és felépíteni. Ez belsőleg a List osztályhoz hasonló struktúrával tárolja az adatokat, ami nagyságrendekkel nagyobb teljesítményt jelent. A StringBuilder osztály a System.Text névtérben található. Használatát az alábbi példaprogram mutatja be:
using System;
using System.Diagnostics;
using System.Text;
namespace PeldaString
{
class Program
{
static void Main(string[] args)
{
var szoveg = "";
var stringBuilder = new StringBuilder();
var random = new Random();
Console.WriteLine("100 000 db random karater összefűzése szövegbe");
Stopwatch watch = Stopwatch.StartNew(); //algoritmusok sebességének mérésére használható osztály
for (int i=0; i<100000; i++)
{
szoveg += (char)random.Next(32, 255);
}
watch.Stop();
var stringIdo = watch.Elapsed.TotalMilliseconds;
watch = Stopwatch.StartNew();
for (int i = 0; i < 1000000; i++)
{
stringBuilder.Append((char)random.Next(32, 255));
}
watch.Stop();
var stringBuilderIdo = watch.Elapsed.TotalMilliseconds;
Console.WriteLine("Eddig tartott String-el: {0} ms", stringIdo);
Console.WriteLine("Eddig tartott StringBuilder-el: {0} ms", stringBuilderIdo);
Console.ReadKey();
}
}
}
A program egy lehetséges kimenete:
100 000 db random karater összefuzése szövegbe
Eddig tartott String-el: 6429,7839 ms
Eddig tartott StringBuilder-el: 31,9724 ms
A StringBuilder által tárolt “szöveget” tényleges string objektummá a ToString metódussal tudjuk konvertálni. Ahogy a példában látható, az Append metódussal fűzünk hozzá. Ezen felül létezik az AppendLine metódus is, ami a szöveg beillesztése után sortörés jelet is beilleszt, illetve az AppendFormat metódus is, ami formázott szöveget illeszt be.
A példa kimenete alapján a StringBuilder által produkált futási idő legalább egy nagyságrenddel kisebb lesz.
Az immutability mítosza
A string típus esetén az immutability kérdése becsapós, mivel unsafe kontextusban nem immutable, mégpedig azért, mert unsafe kódot általában azért írunk, hogy egy algoritmust optimalizáljunk. Az optimalizáció ellensége pedig a folyamatos memória másolás.
internal class Program
{
private static string TitleCase(string input)
{
if (string.IsNullOrWhiteSpace(input))
return string.Empty;
unsafe
{
fixed (char* p = input)
{
*p = char.ToUpper(*p);
}
}
return input;
}
private static void Main(string[] args)
{
const string A = "foo";
const string B = "foo";
var a = A;
var b = B;
TitleCase(a);
Console.WriteLine(a);
Console.WriteLine(b);
}
}
A program kimenete:
Foo
Foo
A fenti kódrészlet jól szemlélteti, hogy a memóriaterület közvetlen átírása nem várt következményekkel járhat. A kódrészletben a fixed szerkezet megakadályozza, hogy a szemétgyűjtő áthelyezze a memóriát, miközben közvetlenül módosítjuk. A kódban b változó értéke annak ellenére módosul, hogy mi az a változót írjuk. Ez a háttérben történő deduplikáció miatt történik. Mivel a kiindulási szövegek azonosak, ezért b csak egy referencia lesz az a változóra.
UTF-8 String literal
C# 11 óta lehetőségünk van szöveget közvetlenül UTF-8 formában létrehozni. Ez különösen hasznos HTTP és hálózati programozás esetén.
ReadOnlySpan<byte> utf = "Ez utf-8 kódolású"u8;
Ahogy a név mutatja, szöveg literálról van szó, aminek az eredménye egy ReadOnlySpan<byte> típusban tárolódik. A szöveg végi u8 módosító jelzi, hogy UTF-8 kódolás kerül alkalmazásra. A ReadOnlySpan<byte> számos megkötést hoz magával, ezért ha byte[] tömbbel szeretnénk tovább dolgozni, akkor a ToArray() hívással tudjuk tömbbé konvertálni a kifejezés eredményét:
byte[] utf = "Ez utf-8 kódolású"u8.ToArray();