Menu

StringBuilder w C#: Append, Insert, Replace i kiedy go używać

Dlaczego wielokrotne łączenie stringów w pętli jest w C# wolne i jak naprawia to StringBuilder: Append, AppendLine, AppendFormat, Insert, Remove, Replace, pojemność oraz kiedy lepszym wyborem jest zwykła konkatenacja albo string.Join.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Stringi w C# są niemutowalne: s += "x" nie wydłuża s, tylko tworzy nowy string ze starymi znakami i nowymi, a stary string staje się śmieciem. Raz to nic. W pętli wykonywanej tysiące razy każda iteracja kopiuje wszystko, co zbudowano do tej pory, więc łączna praca rośnie z kwadratem długości.

StringBuilder z przestrzeni System.Text to mutowalny bufor. Append zapisuje w wolnym miejscu na końcu, od czasu do czasu powiększając bufor, a ToString() tworzy jeden string na końcu.

Wynik:

Order #1042, total 59.97
Status: shipped
**********
51

Dlaczego konkatenacja w pętli jest wolna

Oto ten sam tekst zbudowany na oba sposoby. Czasy zależą od maszyny, więc poniższe liczby to tylko przykład; liczy się różnica, która rośnie razem z pętlą:

Przykładowy wynik:

string +=     482 ms, length 188890
StringBuilder 1 ms, length 188890

Przy 20 000 dopisań wersja z += kopiuje łącznie prawie 2 miliardy znaków. StringBuilder kopiuje każdy znak mniej więcej dwa razy, raz przy dopisaniu i jeszcze raz w ToString(), dlatego trzyma się w okolicach milisekundy.

Append, AppendLine i AppendFormat

Append ma przeciążenia dla każdego wbudowanego typu i zwraca ten sam builder, więc wywołania można łączyć w łańcuch. AppendLine dodaje znak nowej linii po argumencie. AppendFormat przyjmuje złożony string formatu, tak jak string.Format:

Wynik:

RECEIPT
-------
Espresso        2.40
Croissant       3.10
Orange juice    4.50
TOTAL          10.00

Dopisanie interpolowanego stringa (sb.Append($"...")) jest w porządku i dobrze się czyta. W .NET 6 i nowszych kompilator zapisuje nawet fragmenty bezpośrednio do buildera, bez tworzenia pośredniego stringa.

AppendLine używa Environment.NewLine: \n w Linuksie i macOS, \r\n w Windows. Gdy format pliku wymaga konkretnego końca linii, dopisz go sam.

Insert, Remove, Replace i indeksator

W przeciwieństwie do stringa StringBuilder można edytować w miejscu:

Wynik:

Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0

Ustawienie mniejszej wartości Length obcina tekst; to najtańszy sposób na usunięcie końcowego separatora. Clear() ustawia długość na 0 i zachowuje przydzielony bufor, więc jeden builder można wykorzystać ponownie w kolejnych iteracjach zewnętrznej pętli.

Indeksator jest szybki dla niedawno dopisanych znaków, ale przy losowym dostępie do bardzo dużego buildera może być wolny, bo zawartość jest przechowywana w połączonych fragmentach. Jeśli musisz czytać znaki, wywołaj raz ToString() i indeksuj string.

Problem końcowego separatora

Klasyczne zastosowanie to budowanie listy rozdzielonej przecinkami. Dopisywanie separatora po każdym elemencie zostawia na końcu jeden za dużo:

Wynik:

csharp, dotnet, linq
csharp, dotnet, linq

Gdy elementy są już w kolekcji, string.Join jest krótszy i co najmniej tak samo szybki. StringBuilder zostaw dla tekstu, którego kształt zależy od logiki: warunkowych sekcji, zagnieżdżonych pętli, mieszanego formatowania.

Pojemność

StringBuilder startuje z miejscem na 16 znaków. Gdy miejsce się kończy, dołącza nowy blok pamięci tak duży jak wszystko, co już przechowuje, maksymalnie 8000 znaków na blok, więc póki jest mały, jego pojemność się podwaja, a potem rośnie o 8000 znaków. Powiększanie nigdy nie kopiuje już zapisanego tekstu. Jeśli wiesz mniej więcej, jak długi będzie wynik, przekaż pojemność do konstruktora, a builder nie będzie musiał rosnąć:

Wynik:

12000
10000
True
True

Wstępne ustalenie rozmiaru to optymalizacja, a nie wymóg. Powiększanie przydziela nowy blok, ale nigdy nie przenosi istniejącego tekstu, więc wartość domyślna wystarcza, chyba że budujesz wiele dużych stringów w gorącej ścieżce.

Kiedy nie używać StringBuilder

StringBuilder nie jest szybszym zamiennikiem każdego +. Kompilator już teraz zamienia a + b + c + d w jednym wyrażeniu na jedno wywołanie string.Concat, które oblicza końcową długość i kopiuje każdy fragment raz, czyli wykonuje tę samą pracę co builder, tylko mniejszym kodem. Używaj zwykłych stringów, gdy:

  • Liczba fragmentów jest mała i stała: $"{first} {last} ({age})".
  • Fragmenty są w kolekcji i potrzebują separatora: string.Join(", ", items).
  • Fragmenty nie potrzebują separatora: string.Concat(parts).
  • Łączysz stringi kilka razy poza jakąkolwiek pętlą.

Używaj StringBuilder, gdy dopisujesz w pętli, dopisujesz nieznaną z góry liczbę razy albo edytujesz tekst (wstawiasz, zamieniasz, usuwasz) w trakcie budowania. Konwertowanie tam i z powrotem (sb.ToString() wewnątrz pętli) marnuje całą korzyść; wywołaj je raz, na końcu.

Drugą alternatywą są strumienie: żeby utworzyć duży plik, zapisuj linie do StreamWriter na bieżąco, zamiast budować całą zawartość w pamięci. Zobacz pliki.

Najczęściej zadawane pytania

Czym jest StringBuilder w C#?

System.Text.StringBuilder to mutowalny bufor znaków do budowania stringa w wielu krokach. Append dopisuje na koniec bufora zamiast za każdym razem tworzyć nowy string, a ToString() raz tworzy końcowy string. To standardowe narzędzie do budowania tekstu w pętli.

Kiedy używać StringBuilder zamiast łączenia stringów?

Gdy dopisujesz w pętli albo nieznaną liczbę razy, na przykład budując raport, plik CSV lub HTML z kolekcji. Przy kilku stałych fragmentach (a + b + c albo jeden interpolowany string) zwykła konkatenacja jest równie szybka i czytelniejsza, bo kompilator i tak łączy ją w jedno wywołanie string.Concat.

Jak usunąć ostatni znak ze StringBuilder?

Zmniejsz jego Length: if (sb.Length > 0) sb.Length--; usuwa ostatni znak bez kopiowania. sb.Remove(sb.Length - 1, 1) robi to samo. To typowy sposób na pozbycie się przecinka na końcu, choć string.Join w ogóle nie dodaje końcowego separatora.

Czy StringBuilder jest bezpieczny wątkowo?

Nie. Wywoływanie Append na tym samym StringBuilder z kilku wątków naraz może uszkodzić jego zawartość albo rzucić wyjątek. Daj każdemu wątkowi własny builder i połącz wyniki albo chroń współdzielony builder instrukcją lock.

Co dodaje AppendLine w C#?

Tekst, a po nim Environment.NewLine, czyli \r\n w Windows i \n w Linuksie i macOS. Jeśli wynik musi mieć konkretny znak końca linii, na przykład \n w formacie pliku albo \r\n w nagłówku HTTP, dopisz go jawnie przez Append("\n").

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ