Efektywne manipulowanie stringiem w C#

Łańcuchy w C# są niezmienne (immutable) — każda operacja na string tworzy nowy obiekt, zamiast modyfikować istniejący (pełne omówienie niezmienności w artykule Łańcuchy string w C#). Przy pojedynczej operacji to bez znaczenia. Przy tysiącach operacji w pętli — to realny, mierzalny koszt wydajnościowy. Ten artykuł pokazuje, dlaczego tak jest i jakie masz opcje poza oczywistym StringBuilder.
Dlaczego sklejanie w pętli to O(n²), a nie O(n)
Gdy sklejasz łańcuch operatorem + lub += w pętli, każda iteracja tworzy nowy obiekt string i kopiuje do niego całą dotychczasową zawartość plus nowy fragment:
// ❌ Kosztowne: każda iteracja kopiuje CAŁY dotychczasowy tekst
string wynik = "";
for (int i = 0; i < 1000; i++)
{
wynik += i + ", "; // nowy obiekt string, kopia wszystkiego dotychczas + nowy fragment
}Przy n iteracjach kopiujesz średnio połowę rosnącego tekstu za każdym razem — sumarycznie to 1 + 2 + 3 + … + n skopiowanych znaków, czyli w przybliżeniu n²/2. Dla 1000 iteracji to różnica między ~1000 operacjami (liniowo) a ~500 000 operacjami kopiowania (kwadratowo). Przy 10 000 iteracji różnica robi się już dotkliwa — kwadratowy wzrost oznacza, że dziesięciokrotnie więcej danych to sto razy więcej pracy.
StringBuilder — rozwiązanie O(n)
StringBuilder (z System.Text) modyfikuje bufor znaków w miejscu, zamiast tworzyć nowy obiekt przy każdej zmianie — dopisanie tekstu jest średnio O(1), a cała pętla n dopisań to O(n), nie O(n²):
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i).Append(", ");
}
string wynik = sb.ToString();Pełny przegląd API StringBuilder (Append, Insert, Remove, Replace, pojemność, method chaining) znajdziesz w dedykowanym artykule: StringBuilder w C#. Tutaj skupiamy się na tym, kiedy w ogóle po niego sięgać — i jakie ma alternatywy.
Gdy znasz liczbę elementów z góry: string.Join / string.Concat
Jeśli łączysz gotową kolekcję elementów (a nie budujesz tekst przyrostowo w logice warunkowej), string.Join bywa czytelniejszy i równie wydajny co StringBuilder — wewnętrznie oblicza całkowitą potrzebną długość raz, przed alokacją:
var liczby = Enumerable.Range(0, 1000).Select(i => i.ToString());
// Czytelniejsze niż ręczna pętla ze StringBuilder, podobnie wydajne
string wynik = string.Join(", ", liczby);string.Concat(a, b, c, d) ma podobną przewagę dla stałej, znanej z góry liczby fragmentów — kompilator C# tłumaczy zresztą zwykłe sklejanie kilku literałów operatorem + właśnie na wywołanie string.Concat, więc "a" + "b" + "c" poza pętlą nie ma problemu wydajnościowego opisanego wyżej — problem pojawia się dopiero, gdy liczba sklejeń rośnie w pętli.
ReadOnlySpan<char> — operacje bez żadnej alokacji
Metody takie jak Substring() zawsze alokują nowy obiekt string na stercie — nawet jeśli tylko “podglądasz” fragment tekstu i zaraz go odrzucasz (np. podczas parsowania). ReadOnlySpan<char> pozwala operować na fragmencie istniejącego łańcucha bez kopiowania danych:
string linia = "2026-07-13,Warszawa,22.5";
ReadOnlySpan<char> span = linia;
int pierwszyPrzecinek = span.IndexOf(',');
// Podgląd fragmentu bez alokacji nowego stringa
ReadOnlySpan<char> data = span[..pierwszyPrzecinek];
Console.WriteLine(data.ToString()); // "2026-07-13" — kopia powstaje dopiero tutaj, świadomie
To technika dla gorących ścieżek wykonania (hot paths) — parsowania dużych plików, logów, protokołów sieciowych — gdzie tysiące wywołań Substring() zapychałyby stertę zbędnymi alokacjami. Dla zwykłego kodu biznesowego różnica jest zwykle niemierzalna, a czytelność zwykłego Substring() wygrywa.
String interning — gdy ten sam tekst pojawia się wielokrotnie
CLR automatycznie “internuje” literały tekstowe znane w czasie kompilacji — dwa identyczne literały "abc" w kodzie wskazują w pamięci na ten sam obiekt, nie na dwie kopie:
string a = "test";
string b = "test";
Console.WriteLine(ReferenceEquals(a, b)); // true — ten sam obiekt w pamięciStringi zbudowane dynamicznie (np. przez StringBuilder.ToString() albo konkatenację zmiennych) nie są automatycznie internowane — każdy taki string to osobny obiekt, nawet jeśli treść jest identyczna z innym. Jeśli masz aplikację, która wielokrotnie tworzy te same, powtarzające się teksty w pamięci (np. parsując duży plik z powtarzającymi się wartościami), możesz ręcznie je zinternować metodą string.Intern(s), żeby zredukować zużycie pamięci kosztem odrobiny narzutu na wyszukanie w puli. To rzadko potrzebna, wąsko wyspecjalizowana optymalizacja — warto o niej wiedzieć, ale nie stosować profilaktycznie.
Pełny przegląd metod StringBuilder — deklaracja, Append, method chaining i pułapkę porównywania przez Equals() — znajdziesz w StringBuilder w C# — kiedy używać i jak działa.
Podsumowanie — co wybrać
- Kilka sklejeń poza pętlą — zwykła interpolacja
$"..."lub+. Kompilator i tak zoptymalizuje to dostring.Concat. - Budowanie tekstu w pętli, warunkowo, przyrostowo —
StringBuilder. - Łączenie gotowej kolekcji elementów znaną liczbą razy —
string.Join. - Parsowanie dużych ilości tekstu w gorącej ścieżce —
ReadOnlySpan<char>, żeby uniknąć zbędnych alokacjiSubstring(). - Powtarzające się, identyczne stringi budowane dynamicznie w pamięciożernej aplikacji — rozważ
string.Intern, ale dopiero gdy profiler faktycznie wskaże to jako problem.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →
2 comments