StringBuilder i przetwarzanie tekstu

Raport, który powstaje w pętli, i dane wczytane jako jeden wiersz z separatorem — dwa zadania, przy których zwykłe sklejanie napisów przestaje wystarczać.

C# StringBuilder Split Join 50 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego sklejanie napisów w długiej pętli jest kosztowne
  • Zbudujesz tekst przy użyciu StringBuilder
  • Podzielisz wiersz danych metodą Split z odpowiednimi opcjami
  • Odrzucisz uszkodzone dane, zanim sięgniesz po indeks tablicy
  • Złożysz wyrównany raport z nagłówkiem, sumą i podsumowaniem błędów
TEORIA

Dlaczego sklejanie napisów bywa kosztowne

Z lekcji 08 wiesz, że napis jest niemutowalny: każda operacja tworzy nowy napis, a stary zostaje bez zmian. Przy kilku sklejeniach to nie ma znaczenia. Przy tysiącach — ma.

wersja wolna
string raport = "";

for (int i = 1; i <= 50000; i++)
{
    raport += i + ";";     // za kazdym razem powstaje NOWY napis
}

W obiegu numer 40 000 program kopiuje napis o długości około 200 tysięcy znaków tylko po to, żeby dopisać kilka znaków na końcu. Łącznie kopiuje miliardy znaków.

StringBuilder to bufor, który można rozbudowywać w miejscu. Dopiero na końcu zamieniamy go na napis.

wersja szybka
using System.Text;

StringBuilder budowniczy = new StringBuilder();

for (int i = 1; i <= 50000; i++)
{
    budowniczy.Append(i).Append(';');
}

string raport = budowniczy.ToString();
SytuacjaCzego użyć
Sklejenie 2–5 kawałkówinterpolacja $"..." albo zwykłe +
Łączenie elementów tablicy albo listystring.Join
Budowanie tekstu w pętli o wielu obiegachStringBuilder
Sklejanie stałych, np. "a" + "b"obojętne — kompilator zrobi to raz, przy budowaniu

Nie przesadzaj

StringBuilder w pętli o pięciu obiegach nic nie przyspieszy, a kod będzie dłuższy. Sięgaj po niego, gdy liczba dopisań jest duża albo nieznana z góry.

TEORIA

Co potrafi StringBuilder

Program.cs
using System.Text;

StringBuilder sb = new StringBuilder();

sb.Append("Uczen");            // dopisuje
sb.Append(':').Append(' ');    // wywolania mozna laczyc lancuchem
sb.Append(42);                 // liczby tez - bez ToString
sb.AppendLine();               // dopisuje znak konca wiersza
sb.AppendLine("druga linia");

sb.Insert(0, ">> ");           // wstawia na podanej pozycji
sb.Replace("Uczen", "Zawodnik"); // zamienia WSZYSTKIE wystapienia

Console.WriteLine(sb.Length);  // dlugosc buforu
Console.WriteLine(sb.ToString());
wynik (Length wypisze 32)
>> Zawodnik: 42
druga linia

Bufor zachowuje się jak tablica znaków, którą można też czytać i modyfikować po indeksie: sb[0]. Metoda Clear() opróżnia go bez tworzenia nowego obiektu — przydaje się, gdy budujesz wiele raportów po kolei.

ToString wywołaj raz

sb.ToString() tworzy nowy napis, kopiując cały bufor. Wywołanie go w pętli niweczy cały zysk. Buduj do końca, a konwertuj na sam koniec.

TEORIA

Dzielenie i składanie tekstu

Dane z pliku albo od użytkownika najczęściej przychodzą jako jeden wiersz z separatorem. Rozbijamy go metodą Split, a składamy metodą string.Join.

Program.cs
string wiersz = " Ala ;; Ola ; Jan ";

string[] czesci = wiersz.Split(';', StringSplitOptions.RemoveEmptyEntries
                                  | StringSplitOptions.TrimEntries);

Console.WriteLine(czesci.Length);              // 3
Console.WriteLine(string.Join(" | ", czesci)); // Ala | Ola | Jan
Opcja StringSplitOptionsDziałanie
NoneZachowanie domyślne — puste fragmenty i spacje zostają.
RemoveEmptyEntriesPomija puste fragmenty, np. z podwójnego separatora.
TrimEntriesObcina spacje z obu stron każdego fragmentu.

Kiedy wiersz ma stałą budowę — imie;punkty — po podziale sprawdzamy liczbę części przed sięgnięciem po indeks:

Program.cs
string[] pola = wiersz.Split(';');

if (pola.Length != 2)
{
    Console.WriteLine("Wiersz ma zla liczbe pol - pomijam.");
}
else if (!int.TryParse(pola[1], out int punkty))
{
    Console.WriteLine("Punkty nie sa liczba - pomijam.");
}
else
{
    Console.WriteLine($"{pola[0].Trim()} ma {punkty} pkt");
}

Split nie sprawdza za ciebie

pola[1] na wierszu bez średnika kończy się wyjątkiem IndexOutOfRangeException. Zawsze najpierw Length, potem indeks — ta sama zasada, co przy tablicach.

PRZYKŁAD

Przykład: raport z surowych danych

Program dostaje wiersze w formacie imie;punkty, odrzuca uszkodzone i buduje wyrównaną tabelkę.

Program.cs
using System.Text;

class Program
{
    static void Main()
    {
        string[] surowe =
        {
            "Ala;18",
            "  Ola ; 24 ",
            "uszkodzony wiersz",
            "Jan;abc",
            "Ewa;7"
        };

        StringBuilder raport = new StringBuilder();
        raport.AppendLine("IMIE                PUNKTY");
        raport.AppendLine(new string('-', 26));

        int poprawne = 0, bledne = 0, suma = 0;

        foreach (string wiersz in surowe)
        {
            string[] pola = wiersz.Split(';', StringSplitOptions.TrimEntries);

            if (pola.Length != 2 || !int.TryParse(pola[1], out int punkty))
            {
                bledne++;
                continue;
            }

            poprawne++;
            suma += punkty;
            raport.AppendLine($"{pola[0].PadRight(20)}{punkty,6}");
        }

        raport.AppendLine(new string('-', 26));
        raport.AppendLine($"{"RAZEM".PadRight(20)}{suma,6}");
        raport.AppendLine($"Poprawnych: {poprawne}, blednych: {bledne}");

        Console.Write(raport.ToString());
    }
}
wynik w konsoli
IMIE                PUNKTY
--------------------------
Ala                     18
Ola                     24
Ewa                      7
--------------------------
RAZEM                   49
Poprawnych: 3, blednych: 2

Co dzieje się po kolei

  • Split z opcją TrimEntries od razu usuwa spacje, więc " Ola " nie wymaga osobnego Trim.
  • Wiersz bez średnika daje jednoelementową tablicę — warunek pola.Length != 2 wyłapuje go przed dostępem do pola[1].
  • continue pomija resztę obiegu; licznik błędnych rośnie, a program się nie przerywa.
  • new string('-', 26) tworzy linię z 26 myślników bez pętli.
  • Raport powstaje w buforze i trafia na ekran jednym Console.Write — łatwo go potem zapisać do pliku zamiast wypisywać.
ELEMENTY WBUDOWANE

Zestawienie metod

MetodaZwracaDziałanie i ograniczenia
sb.Append(x)StringBuilderDopisuje na końcu bufora. Zwraca ten sam obiekt, więc wywołania można łączyć. Przyjmuje też liczby i znaki.
sb.AppendLine(x)StringBuilderJak Append, ale dokłada znak końca wiersza właściwy dla systemu.
sb.Insert(i, x)StringBuilderWstawia na pozycji i. Indeks większy niż Length daje wyjątek.
sb.Replace(a, b)StringBuilderZamienia wszystkie wystąpienia w buforze.
sb.Clear()StringBuilderOpróżnia bufor, zachowując obiekt.
sb.ToString()stringTworzy napis z zawartości bufora. Wywołuj raz, na końcu.
s.Split(znak, opcje)string[]Dzieli napis. Bez opcji zostawia puste fragmenty. Napisu źródłowego nie zmienia.
string.Join(sep, kolekcja)stringSkleja elementy separatorem. Działa dla tablic i list dowolnego typu.
s.PadRight(n) / PadLeft(n)stringDopełnia spacjami do długości n. Napis dłuższy niż n nie jest przycinany.
new string(znak, n)stringNapis z n powtórzeń znaku — wygodne przy liniach i słupkach.
s.Contains(x)boolCzy napis zawiera podany fragment. Wersja z StringComparison pozwala pominąć wielkość liter.
CZĘSTE BŁĘDY

Na co uważać

ZapisProblem
sb.ToString() w każdym obieguKopiuje cały bufor za każdym razem — zysk z StringBuilder znika.
sb.Append("a") + "b"Append zwraca bufor, nie napis. Sklejanie z napisem daje bezsensowny wynik — dopisuj kolejnym Append.
pola[1] bez sprawdzenia LengthIndexOutOfRangeException na pierwszym uszkodzonym wierszu.
Split(';') na danych z separatorem w treściImię „Kowalski; Jan” rozpadnie się na dwa pola. Przy zapisie odrzucaj albo zamieniaj znak separatora w danych.
Replace na string bez przypisaniaNapis jest niemutowalny — s.Replace(...) bez s = nie zmienia niczego. W StringBuilder zmienia bufor w miejscu.

Co jeszcze da się zrobić z tekstem

Skoro umiesz już rozbierać i składać tekst, warto zobaczyć klasyczne algorytmy tekstowe. Lekcja 30 pokazuje szyfr Cezara, szyfr podstawieniowy, XOR i sumy kontrolne — wszystkie na tym, co już znasz.

ZADANIA

Zadania

ZAD 1Dwa liczniki★☆☆

Zbuduj napis z liczbami od 1 do 20 oddzielonymi przecinkiem — raz przez +=, raz przez StringBuilder, raz przez string.Join. Porównaj długość kodu i czytelność.

ZAD 2Ramka★☆☆

Napisz fragment, który otacza podany tekst ramką z gwiazdek dopasowaną do jego długości. Użyj new string('*', n) i PadRight.

ZAD 3Czyszczenie listy★★☆

Z napisu "Ala;;Ola; ;Jan;" zrób tablicę trzech imion bez pustych wpisów i bez spacji, a potem złóż ją z powrotem separatorem ", ".

ZAD 4Odwracanie kolejności słów★★☆

Wczytaj zdanie i wypisz je ze słowami w odwrotnej kolejności, zachowując pojedyncze spacje. Podpowiedź: Split, pętla od końca i StringBuilder.

ZAD 5Statystyka tekstu★★☆

Dla wczytanego zdania podaj: liczbę znaków, liczbę słów, najdłuższe słowo i liczbę samogłosek. Wynik zbuduj w StringBuilder i wypisz jednym wywołaniem.

ZAD 6Prosty CSV★★★

Mając tablicę wierszy w formacie imie;klasa;punkty, zbuduj raport z nagłówkiem, wyrównanymi kolumnami, sumą i średnią punktów oraz listą wierszy odrzuconych wraz z numerem i powodem odrzucenia.

PODSUMOWANIE

Co trzeba zapamiętać

  • Napis jest niemutowalny — każde += tworzy nowy obiekt.
  • StringBuilder buduje tekst w buforze; ToString wywołujemy raz, na końcu.
  • Append zwraca ten sam bufor, więc wywołania można łączyć w łańcuch.
  • Split z opcjami RemoveEmptyEntries i TrimEntries oszczędza ręcznego czyszczenia.
  • Po podziale zawsze sprawdź liczbę pól, zanim sięgniesz po indeks.
  • Do sklejenia kilku kawałków wystarczy interpolacja; StringBuilder jest do pętli o wielu obiegach.

Dokumentacja: Microsoft Learn — temat tej lekcji.