Lubię pisać w C#, zwłaszcza na dedykowanym dla niego edytorze w pakiecie Visual Studio. Ale czasami szlag mnie trafia, kiedy jego największa zaleta czasami daje o sobie znać w najgorszy możliwy sposób – a mianowicie zarządzanie pamięcią.
W C++ mamy jasno określoną sytuację – char zajmuje jeden bajt; jak go zapiszemy do pliku, to dalej zajmuje jeden bajt, a jak go odczytamy – dalej zajmuje to samo. Sprawa w C# okazuje się nie być tak banalna jeżeli chodzi o typ danych przechowujących znaki czytane.
Oczywiście całe cyrki zaczynają się gdy chcemy zapisać tekst do pliku binarnego. Zakładam, że chcemy zapisać “Józek” – jak widać posiada polski znak, więc wypadało by go zapisać z odpowiednim kodowaniem. Ale jakiego kodowania używa pakiet .NET i w ogóle cały C#?
Po krótkim grzebaniu w MSDN dowiadujemy się, że Unicode – to wydawało się dosyć oczywiste od początku. Bakcyl polega na fakcie, że nie wiadomo którego z kilku standardów. Ale i tego udało mi się dowiedzieć. Wprawdzie nie znalazłem tego na MSDN – są na nim wszystkie informacje o wszystkich standardach, ale jakoś informacja o tym którego oni używają gdzieś im umknęła – a znalazłem ją dopiero w dymku z podpowiedziami do obieku System.Text.Encoding.Unicode – a mianowicie jest to UTF-16.
Dobra, wiemy już jak to jest zapisane w pamięci – po 2 bajty na każdy znak (standard przynajmniej na to wskazuje). Jak się dogrzebiemy do opisu obiektu “char”, to też mamy potwierdzenie tej teorii, ponieważ zajmuje on w pamięci 2 bajty. A więc zróbmy eksperyment i zapiszmy naszego stringa raz bezpośrednio, a raz jako łańcuch char’ów.
-
-
string strText = "Józek"; // 5 znaków * 2 bajty = 10 bajtów
-
-
BinWr.Write(strText); // zapisanych = 7 bajtów
-
-
BinWr.Write(strText.ToCharArray()); // zapisanych = 6 bajtów
No ok – niby byliśmy mądrzy – już pisaliśmy kod czytania plików oparty na adresowaniu pamięci, a tu niespodzianka – string nie zajmuje tyle co powinien? WTF?!
Oczywiście najpierw z kamienną miną szukałem odpowiedzi na MSDN. Znalazłem tam jedynie, że metoda Write(string Text) klasy BinaryWriter najpierw zapisuje liczbę znaków, a potem tekst. Szczegół, że to w ogóle nie tłumaczy dlaczego napis zajmuje 7 a nie 14 bajtów.
Dopiero głębsza analiza pliku dała jakieś odpowiedzi. Okazuje się, że BinaryWriter (i Reader) używają do określenia ilości znaków tzw. int’a zmiennej długości. Oznacza to że jeżeli długość tekstu nie przekroczy 127 znaków – to używa jednego bajtu w pamięci. Jeżeli przekroczy, to dwóch. A jak piszemy jakąś przemowe pojedziemy ponad 32 tysiące, to 3 itd.
No dobra – to tłumaczy jeden bajt. Pozostało jeszcze do wyjaśnienia te tajemnicze 6 bajtów. Okazuje się (a nie jest to absolutnie nigdzie napisane), że BinaryWriter i BinaryReader używają do kodowania tekstu standardu UTF-8, czyli zmiennej długości znaku. Innymi słowy, dopóki znak mieści się w podstawowym A-Z zwykłego ASCII, to zajmuje 1 bajt – jeżeli jest to coś innego to 2, 3 lub w razie potrzeby 4. Dlatego nasz tekst zajmuje 6 bajtów = 4 zwykłe znaki + 2 bajty dla specjalnego.
Wszystko fajnie – niby to oszczędza pamięć, ale dla człowieka który pisze dosyć zaawansowany kod czytający dane z różnego miejsca pliku (a jak w dodatku robi to w C++, to zabawa robi się jeszcze przedniejsza), to już nie jest fajnie. Oczekujemy że dane będą miały stałą długość. Na szczęście można to rozwiązać w dosyć prosty sposób:
-
public static void WriteString(string strText, BinaryWriter BinWr)
-
{
-
BinWr.Write((Int32)strText.Length);
-
BinWr.Write(strText.ToCharArray());
-
}
-
-
// gdzieś w kodzie
-
WriteString("Józek", BinWr);
I wtedy nasz tekst powinien zająć nasze upragnione, pamięciożerne 14 bajtów.