Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Dlaczego stringi są niezmienne i co z tego wynika?

 <suchar>Niestety zajmiemy się mniej namacalną implementacją stringów</suchar>  ;)

Typ string (System.String) służy do reprezentacji łańcuchów tekstowych w postaci sekwencji wartości typu char (System.Char) określających znaki Unicode (zakodowane w UTF-16). Zazwyczaj jeden element char koduje jeden znak...

Przy pracy z ciągami tekstowymi należy pamiętać, że stringi w .NET są niezmienne (immutable)! Oznacza to po prostu, że raz utworzony łańcuch tekstowy nie może być modyfikowany (bez użycia refleksji lub kodu unsafe), a metody pozornie modyfikujące string, tak naprawdę zwracają nowy obiekt mający żądaną wartość.

Niezmienność stringów ma wiele zalet (o tym za chwilę), może jednak być źródłem problemów jeśli programista zapomni, że każda "zmiana" łańcucha tekstowego to tak naprawdę tworzenie nowej instancji klasy String. Chociaż CLR traktuje stringi w szczególny sposób, nadal są one typem referencyjnym, dla których to pamięć przydzielana jest na zarządzanej stercie.

Działanie tej pętli spowoduje utworzenie 10 tysięcy zmiennych typu string, z których wszystkie prócz ostatniej to śmieci, które będzie musiał zebrać Garbage Collector:

string s = string.Empty;

for (int i = 0; i < 10000; i++)
{
    s += "x";
}

Na poniższym obrazie widać fragment okna "Histogram by Size for Allocated Objects" aplikacji CLR Profiler. Widać jak kolejne obroty pętli powodowały alokacje pamięci na stercie dla coraz większych stringów:

By uniknąć tworzenia wielu niepotrzebnych obiektów należy użyć klasy StringBuilder, która umożliwia modyfikację tekstu bez każdorazowego tworzenia nowych obiektów:

StringBuilder sb = new StringBuilder();

for (int i = 0; i < 10000; i++)
{
    sb.Append("x");
}

string x = sb.ToString();

Ta prosta zmiana ma ogromny wpływ na ilość przydzielanej i zwalnianej pamięci. Spójrz na poniższe porównanie fragmentów okien "Summary" profilera:

 

Dlaczego projektanci .NET (tak samo jak np. twórcy Javy) zdecydowali się zaimplementować niezmienne łańcuch tekstowe?

Z przyczyn optymalizacyjnych (głównie ze względu na szybkość porównywania) łańcuchy tekstowe mogą być zapisywane w specjalnej tabeli (intern pool) utrzymywanej przez CLR. Chodzi o to by uniknąć tworzenia wielu zmiennych określających ten sam ciąg znaków. Poniżej przedstawiony jest kawałek kodu dowodzący tego, że zmienne posiadające ten sam łańcuch tekstowy mogą wskazywać na ten sam* obiekt:

string a = "xx";
string b = "xx";
string c = "x";
string d = String.Intern(c + c);

Console.WriteLine((object)a == (object)b); // True
Console.WriteLine((object)a == (object)d); // True

Gdyby łańcuchy tekstowe były modyfikowalne, zmieniając wartość zmiennej a, zmieniłoby się też wartość b oraz d.

Niezmienność stringów ma też pozytywne znaczenia w aplikacjach wielowątkowych - zmiana tekstu to utworzenie nowej zmiennej więc nie ma konieczności zakładania blokady (lock) w celu uniknięcia konfliktów związanych z jednoczesnym dostępem wielu wątków do jednego łańcucha tekstowego. Ma to tym większe znaczenie, że często dokonuje się autoryzacji pewnych operacji w zależności od tekstu (np. można blokować szczególne funkcjonalności usługi bazując na stringowym adresie klienta).

Ważnym powodem niezmienności jest powszechne użycie stringów jako kluczy w tablicach haszujących. Jaki sens miało by wyliczenie położenia elementu w tablicy, gdyby można było zmodyfikować wartość użytego klucza? Poniższy listing dowodzi, że "zmiana" wartości zmiennej użytej jako klucz nie ma wpływu na działanie tablicy:

string key = "abc";
Hashtable ht = new Hashtable();
ht.Add(key, 123);

key = "xbc";

Console.WriteLine(key); // xbc
Console.WriteLine(ht["abc"]); // 123

O tym co stałoby się w przypadku modyfikowalnych łańcuchów tekstowych można się przekonać za pomocą kodu używającego bloku unsafe w celu rzeczywistej zmiany wartości stringa użytego jako klucz:

unsafe
{
    string key = "abc";
    Hashtable ht = new Hashtable();
    ht.Add(key, 123);

    fixed (char* p = key)
    {
        p[0] = 'x';
    }

    Console.WriteLine(key); // xbc
    Console.WriteLine(ht["abc"]); // Nie znajdzie!
}

Niezmienność wiąże się też z tym, że string wewnętrznie przechowywany jest jako tablica, czyli struktura danych reprezentującą ciągłą przestrzeń adresową (dla której ze względów wydajnościowych nie dopuszcza się np. operacji wstawiania elementu).

* To czy literał łańcuchowy jest automatycznie dodawany do puli może być zależne od użycia narzędzia Ngen.exe lub ustawień CompilationRelaxations...

Niepewne działanie TextBox.MaxLength (użycie Fiddlera do modyfikacji żądania wysłanego przez IE)

Ustawienie właściwości MaxLength w kontrolce asp:TextBox pozwala na ograniczenie ilości znaków jakie użytkownik może wprowadzić w pole tekstowe. Ustawienie to działa jednak jedynie gdy właściwość TextMode kontrolki ustawiona jest na SingleLine lub Password (nie zadziała w przypadku opcji MultiLine). Dzieje się tak ponieważ w dwóch pierwszych przypadkach TextBox renderowany jest jako element HTML input type="text", który może posiadać atrybut maxlength informujący przeglądarkę o konieczności ograniczenia ilości znaków. Gdy ustawi się tryb wielowierszowy, TextBox renderowany jest jako element textarea (który nie obsługuje atrybutu maxlenght).

Korzystając z właściwości MaxLenght musisz pamiętać o pewnym problemie! Otóż ograniczenie długości tekstu działa jedynie w przeglądarce. Po stronie serwera długość tekstu nie jest sprawdzana. Jeśli nie zastosujesz zatem odpowiedniej walidacji, złośliwy użytkownik będzie miał możliwość wpisania dowolnie długiego tekstu. Wystarczy tylko odpowiednio spreparować POST.

Do modyfikacji żądania wysyłanego na serwer posłużymy się darmowym narzędziem Fiddler. Jest to proxy służące do monitorowania komunikacji klient-serwer, posiadające również możliwość modyfikacji wysyłanych przez przeglądarkę żądań.

Do testów (sprawdzałem na .NET Framework 3.5, IE7 i Fiddler 2.2.4.2 beta) użyjemy prostej strony zawierającej TextBox i Button:

<asp:TextBox ID="TextBox1" runat="server" MaxLength="5"></asp:TextBox>
<asp:Button ID="Button1" runat="server" Text="Button" />

Jak widzisz maksymalna długość tekstu ograniczona jest (pozornie) do 5 znaków. Uruchom przygotowaną stronę i wypełnij pole tekstowe np. pięcioma literami "a".

Teraz uruchom Fiddlera (menu Narzędzia | Fiddler2) i ustaw automatyczne przerwanie (breakpoint) na requestach przeglądarki naciskając klawisz F11. W pasku stanu powinna pojawić się czerwona ikona z symbolem pauzy:

Po ustawieniu przerwań powróć na stronę testową i naciśnij przycisk by spowodować postback. POST towarzyszący naciśnięciu przycisku zostanie wyłapany przez Fiddlera (jeśli Fiddler nie przechwytuje ruchu przeczytaj uwagę na końcu tekstu). W oknie widocznym po lewej stronie kliknij w ostatni request (oznaczony czerwoną ikoną). Teraz możesz dowolnie zmodyfikować dane wysyłane na serwer.

W prawej części okna wybierz zakładkę "Inspectors". Następnie wybierz przycisk "WebForms" (przy pomocy tej opcji najłatwiej modyfikować pola formularzy). Czy widzisz w sekcji "Body" pole "TextBox1" z prowadzonym przez Ciebie pięcioliterowym tekstem? Dopisz do niego dowolne litery i naciśnij zielony przycisk "Run to Completion" by wznowić normalną komunikację przeglądarki z serwerem WWW.

Co stało się po przeładowaniu strony? Pole tekstowe posiada teraz tekst dłuższy od dopuszczalnego! Znaczy to, że infrastruktura ASP.NET nie sprawdza długości pola - po prostu aktualizuje właściwość Text:

* Istnieje prosty trik umożliwiający Fiddlerowi przechwytywanie ruchu na stronie z adresem localhost. Dodaj kropkę po słowie "localhost" i przeładuj stronę tak by jej adres wyglądał np. tak: http://localhost.:7913/Test