Manipulacja tekstem w C# — Replace, Split, Substring inne metody string

Manipulacja tekstem w C# — Replace, Split, Substring inne metody string

Każda aplikacja przetwarza tekst. Formularze, raporty, importy CSV, logi, API — wszędzie napotykasz dane w postaci stringów, które trzeba oczyścić, wyciąć, zamieniać i formatować. Jeśli nie opanujesz manipulacji tekstem w C#, będziesz co chwilę pisać te same niezgrabne pętle i tracić czas na debugowanie błędów, których nie rozumiesz.

W tym tutorialu poznasz 10 kluczowych metod stringowych w C# — od ToUpper() przez Replace(), Substring(), Contains(), Split() aż po PadLeft(). Nie będzie tu definicji z dokumentacji. Będzie konkretny kod, realistyczne scenariusze i — co ważniejsze — wyjaśnienie dlaczego coś działa tak, a nie inaczej.

📌 Czego nauczysz się w tym artykule?
✅ Jak bezpiecznie normalizować tekst (ToUpper, ToLower, StringComparison)
✅ Jak zamieniać i wycinać fragmenty stringów (Replace, Remove, Insert, Substring)
✅ Jak parsować dane CSV i unikać ukrytych pułapek (Split + StringSplitOptions)
✅ Jak sprawdzać zawartość stringa (Contains, StartsWith, EndsWith)
✅ Jak formatować wyrównane kolumny w konsoli (PadLeft, PadRight)
✅ Jak zrefaktoryzować powtarzający się kod do reużywalnej metody
✅ Dlaczego string jest niemutowalny i co to oznacza dla Twojego kodu

Zanim zaczniesz: najważniejszy model mentalny pracy ze stringami

Zanim przejdziemy do konkretnych metod, musisz wiedzieć jedną rzecz. To zdanie warto zapamiętać:

String w C# jest niemutowalny (immutable).
Żadna metoda stringowa nie zmienia oryginalnego tekstu. Każda operacja zwraca nowy string.
Jeśli nie przypiszesz wyniku do zmiennej — efekt znika.

Analogia: kartka papieru

Wyobraź sobie kartkę z napisem “jan”. Kiedy wywołujesz ToUpper(), nie wymazujesz liter na tej kartce — tworzysz nową kartkę z napisem “JAN”. Oryginalna kartka leży nienaruszono, jeśli nie przypisałeś wyniku.

string imie = "jan";

// ❌ BŁĄD: wynik jest ignorowany — imie nadal = "jan"
imie.ToUpper();
Console.WriteLine(imie); // "jan"

// ✅ POPRAWNIE: zawsze przypisuj wynik
imie = imie.ToUpper();
Console.WriteLine(imie); // "JAN"

⚠️ Najczęstszy błąd początkujących
Wywołanie metody stringowej bez przypisania wyniku to jeden z najczęstszych błędów.
Kod się kompiluje, program działa — ale tekst się nie zmienia. I to jest pułapka.
Złota reguła: zmienna = zmienna.Metoda(…)

ToUpper() i ToLower() — normalizacja tekstu

Kiedy porównujesz dane od użytkownika, nigdy nie wiesz, jaką wielkość liter wybierze. Dlatego przed porównaniem zawsze normalizujesz tekst do jednego formatu.

Podstawowe użycie

string input = "Jan Kowalski";

// Konwersja na wielkie litery
Console.WriteLine(input.ToUpper()); // "JAN KOWALSKI"

// Konwersja na małe litery
Console.WriteLine(input.ToLower()); // "jan kowalski"

// Bezpieczniejsza alternatywa dla aplikacji wielojęzycznych
Console.WriteLine(input.ToUpperInvariant());

Kiedy użyć ToUpperInvariant() zamiast ToUpper()?

Metoda ToUpper() używa ustawień kulturowych systemu. W języku tureckim litera “i” zamieniona na wielką daje “İ” (z kropką nad literą), a nie “I”. Jeśli Twoja aplikacja działa globalnie lub porównujesz kody systemowe — zawsze używaj InvariantCulture.

// ❌ Potencjalnie niebezpieczne przy locale=tr-TR
bool match = input.ToLower() == "admin";

// ✅ Bezpieczne niezależnie od ustawień systemu
bool match = string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase);

💡 Pro tip
Do porównań stringów w C# używaj string.Equals(a, b, StringComparison.OrdinalIgnoreCase) zamiast a.ToLower() == b.ToLower().
Jest czytelniej, bezpieczniej i szybciej.

Replace(), Remove() i Insert() — chirurgiczne operacje na stringu

Te trzy metody pozwalają modyfikować konkretne fragmenty tekstu. Każda z nich robi co innego — i każda ma swoją pułapkę.

Replace() — zamień wszystkie wystąpienia

string tekst = "Witaj, świecie!";

// Zamień wszystkie 'i' na 'I'
string wynik = tekst.Replace("i", "I");
// "WItaj, świecIe!"

// Method chaining — każdy Replace() zwraca nowy string
string telefon = "+48-123-456-789";
telefon = telefon.Replace("-", "").Replace("+48", "");
// "123456789"

Remove() — wytnij fragment od indeksu

string tekst = "Witaj, świecie!";

// Remove(startIndex, count) — usuń 9 znaków od indeksu 6
string wynik = tekst.Remove(6, 9);
// "Witaj, !"

// ⚠️ Uwaga: indeksy zaczynają się od ZERA
// ArgumentOutOfRangeException jeśli startIndex + count > Length

Insert() — wstaw tekst w konkretnym miejscu

string tekst = "Witaj, świecie!";

// Insert(index, value) — wstaw na pozycji 7
string wynik = tekst.Insert(7, "piękny ");
// "Witaj, piękny świecie!"

⚠️ Pułapka: ArgumentOutOfRangeException
Metody Remove() i Insert() rzucają wyjątek, jeśli podasz indeks poza zakresem stringa.
Zawsze waliduj dane wejściowe zanim wywołasz te metody na danych od użytkownika.
Pamiętaj: indeksy w C# zaczynają się od ZERA, nie od 1.

Substring() — wytnij dokładnie to, czego potrzebujesz

Substring to Twoje narzędzie do wycinania fragmentów tekstu. Możesz podać indeks startowy i opcjonalnie długość — albo zabrać wszystko do końca.

Podstawowa składnia i przykłady

string email = "jan.kowalski@firma.pl";
//              indeksy zaczynają się od 0!

// Weź 3 znaki od indeksu 0
string imie = email.Substring(0, 3);    // "jan"

// Weź wszystko od indeksu 13 do końca
string domena = email.Substring(13);    // "firma.pl"

// C# 8+ — nowoczesna alternatywa: range syntax
string domenaNew = email[13..];          // "firma.pl" — to samo
string imieNew   = email[..3];           // "jan" — to samo co Substring(0, 3)

Dynamiczne indeksy — użyj IndexOf()

Gdy indeks nie jest stały (np. nie wiesz, gdzie jest znak “@” w emailu), połącz Substring() z IndexOf():

string email = "jan.kowalski@firma.pl";

// Znajdź pozycję znaku '@'
int atIndex = email.IndexOf('@');       // 12

// Wytnij nazwę użytkownika (wszystko przed '@')
string user   = email.Substring(0, atIndex); // "jan.kowalski"

// Wytnij domenę (wszystko po '@')
string domain = email.Substring(atIndex + 1); // "firma.pl"
💡 Pro tip
IndexOf() zwraca -1 jeśli szukany znak nie istnieje w stringu.
Zawsze sprawdzaj: if (atIndex >= 0) przed wywołaniem Substring(), inaczej ryzykujesz ArgumentOutOfRangeException.

Contains() — sprawdź, czy tekst w ogóle tam jest

Contains() odpowiada na proste pytanie: czy dany fragment tekstu w ogóle istnieje w stringu? Zwraca true lub false.

string log = "ERROR: Nie można połączyć z bazą danych";

// Sprawdź czy log zawiera słowo "ERROR"
if (log.Contains("ERROR"))
{
    Console.WriteLine("Znaleziono błąd krytyczny!");
}

// Case-insensitive (bez rozróżniania wielkości liter) — C# 5+
bool haserror = log.Contains("error", StringComparison.OrdinalIgnoreCase); // true

Powiązane metody: StartsWith() i EndsWith()

string url = "https://firma.pl/kontakt";

// Sprawdź prefix
bool isHttps = url.StartsWith("https");      // true

// Sprawdź suffix
bool isPl = url.EndsWith(".pl/kontakt");  // true

// Case-insensitive
url.StartsWith("HTTPS", StringComparison.OrdinalIgnoreCase); // true
⚠️ Anty-wzorzec: nie używaj Replace() do walidacji
Kuszące, ale złe: email.Replace(“@”, “”) != email (żeby sprawdzić, czy jest małpa)
Poprawnie: email.Contains(“@”)
Replace() jako walidator jest wolniejszy, nieczytelny i łatwy do pomylenia.

Split() — podziel string na tablicę

Split to Twój główny parser do CSV, ścieżek, list rozdzielanych przecinkami i wszystkiego, co ma separator. Zwraca tablicę string[].

Podstawowe użycie

string csv = "Jan,Kowalski,Warszawa,35";

// Podziel po przecinku
string[] pola = csv.Split(',');

Console.WriteLine(pola[0]); // "Jan"
Console.WriteLine(pola[1]); // "Kowalski"
Console.WriteLine(pola[2]); // "Warszawa"
Console.WriteLine(pola[3]); // "35"

// Podział po wielu separatorach jednocześnie
string tekst = "jabłko;gruszka,śliwka banana";
string[] owoce = tekst.Split(';', ',', ' ');

StringSplitOptions — dla brudnych danych

string csv = "Jan, Kowalski,,Warszawa";

// ❌ Bez opcji — spacje i puste elementy trafiają do tablicy
string[] zle = csv.Split(',');
// ["Jan", " Kowalski", "", "Warszawa"]
//             ^^spacja    ^^pusty element!

// ✅ Z opcjami — usuń puste, przytnij spacje
string[] dobre = csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); // C# 8+
// ["Jan", "Kowalski", "Warszawa"]

⚠️ Dwie ukryte pułapki Split()
Pułapka 1: Spacja przy separatorze — “Jan, Kowalski” da pola[1] = ” Kowalski” (z wiodącą spacją).
Rozwiązanie: użyj StringSplitOptions.TrimEntries (C# 8+) lub wywołaj .Trim() na każdym elemencie.

Pułapka 2: Pusty string — “”.Split(‘,’) NIE zwraca pustej tablicy.
Zwraca tablicę z jednym pustym elementem: [“”]. Rozwiązanie: StringSplitOptions.RemoveEmptyEntries.

PadLeft() i PadRight() — formatowanie wyrównanych kolumn

Metody Pad uzupełniają string do zadanej długości, dodając znaki wypełnienia. Brzmią niepozornie, ale są nieocenione przy budowaniu raportów, tabel tekstowych i numeracji.

string numer = "42";

// PadLeft — uzupełnij zerami od lewej (numery faktur!)
Console.WriteLine(numer.PadLeft(6, '0'));  // "000042"

// PadRight — uzupełnij myślnikami od prawej
Console.WriteLine(numer.PadRight(6, '-')); // "42----"

// Wyrównana tabela w konsoli:
Console.WriteLine(
    "Jan".PadRight(12) +
    "Kowalski".PadRight(15) +
    "jan@firma.pl".PadRight(30) +
    "123456789".PadLeft(12)
);
// "Jan         Kowalski       jan@firma.pl                 123456789"

💡 Pro tip
Jeśli string jest już dłuższy niż żądana długość — Pad* zwraca oryginał bez skracania.
PadLeft(‘0’) to standardowy sposób na numery z wiodącymi zerami (faktury, identyfikatory).

Przykład produkcyjny — czyszczenie danych z importu CSV

Zobaczmy wszystkie metody razem w jednym, realistycznym scenariuszu: czyszczenie brudnego rekordu z zewnętrznego systemu.

Dane wejściowe — typowy brudny import

// Rekord z zewnętrznego systemu — pełen problemów:
string rekord = "   jan,KOWALSKI,jan.kowalski@FIRMA.pl,+48-123-456-789     ";
//               ^^spacje  ^^wielkie litery              ^^myślniki   ^^spacje

Krok po kroku — od brudnych danych do gotowego raportu

// Krok 1: Trim — usuń spacje z brzegów (zawsze pierwszy krok!)
string oczyszczony = rekord.Trim();

// Krok 2: Split — podziel na pola
string[] pola = oczyszczony.Split(',');

// Krok 3: Normalize — imię i nazwisko z wielkiej litery
string imie    = Capitalize(pola[0]); // "Jan"
string nazwisko = Capitalize(pola[1]); // "Kowalski"

// Krok 4: ToLower — emaile zawsze normalizujemy do małych liter
string email = pola[2].ToLower(); // "jan.kowalski@firma.pl"

// Krok 5: Replace chain — wyczyść numer telefonu
string telefon = pola[3]
    .Replace("-", "")    // usuń myślniki
    .Replace("+48", ""); // usuń prefiks
// "123456789"

// Krok 6: Contains + PadRight — formatuj raport
bool czyFirmowy = email.Contains("@firma.pl");
string etykieta = czyFirmowy ? "FIRMOWY" : "ZEWNĘTRZNY";

Console.WriteLine(
    imie.PadRight(12) +
    nazwisko.PadRight(15) +
    email.PadRight(30) +
    telefon.PadLeft(12) +
    $" [{etykieta}]"
);

Refaktoryzacja — wyciągnij powtarzającą się logikę

Normalizacja imienia i nazwiska to ta sama logika powtórzona dwa razy. Wyciągnij ją do metody — będzie reużywalna i testowalna:

// Metoda pomocnicza: pierwsza litera wielka, reszta mała
static string Capitalize(string input)
{
    // ⚠️ Zawsze zabezpieczaj się przed null i pustym stringiem
    if (string.IsNullOrEmpty(input)) return input;

    return char.ToUpper(input[0]) + input.Substring(1).ToLower();
    // C# 8+: char.ToUpper(input[0]) + input[1..].ToLower();
}

✅ Po refaktoryzacji: Capitalize() jest reużywalna dla imienia, nazwiska i każdego kolejnego pola.
✅ Jeden punkt zmiany: jeśli logika się zmieni, poprawiasz w jednym miejscu.
✅ Łatwa do testowania: możesz napisać unit test dla Capitalize() niezależnie od reszty kodu.

Checklista: manipulacja tekstem w C# — co musisz wiedzieć

Zanim przejdziesz dalej, upewnij się, że rozumiesz każdy z poniższych punktów:


✅  String jest niemutowalny — zawsze przypisuję wynik: zmienna = zmienna.Metoda()
✅  ToUpper/ToLower normalizuje tekst przed porównaniem
✅  Do porównań case-insensitive używam StringComparison.OrdinalIgnoreCase
✅  Replace(), Remove(), Insert() — każda zwraca NOWY string
✅  Indeksy w C# zaczynają się od ZERA
✅  Substring() + IndexOf() = dynamiczne wycinanie bez magicznych liczb
✅  Contains() to prawidłowe narzędzie do sprawdzania obecności — nie Replace()
✅  Split() z StringSplitOptions.TrimEntries i RemoveEmptyEntries dla brudnych danych
✅  PadLeft/PadRight nie skraca stringa — tylko uzupełnia do zadanej długości

Zobacz też — powiązane artykuły

👉 Tworzenie klas i obiektów w C# — kompletny przewodnik

👉 LINQ w C# — przetwarzanie kolekcji bez pętli – zobacz w kursie LINQ w C# -czytelny kod, wydajne zapytania

👉 Typy wartościowe vs referencyjne w C# — jak działa pamięć – zobacz w kursie C# Podstawy Programowania: Twój Pierwszy Krok w Świat Kodowania

Zadanie praktyczne — sprawdź się!

Teoria bez praktyki nic nie daje. Oto dwa zadania — jedno obowiązkowe, jedno dla ambitnych.

Zadanie 1 — parser emaila (15–30 minut)

Napisz program konsolowy, który:

  1. Pobiera od użytkownika adres email za pomocą Console.ReadLine()
  2. Sprawdza, czy zawiera znak “@” — jeśli nie, wypisuje błąd
  3. Rozdziela email na część przed i po “@” (użyj IndexOf + Substring lub Split)
  4. Normalizuje email do małych liter
  5. Wypisuje: Użytkownik: jan.kowalski  |  Domena: firma.pl

Zadanie 2 (dla ambitnych) — formater numeru telefonu

Napisz metodę static string FormatujTelefon(string numer), która:

  • Usuwa wszystkie znaki niebędące cyframi (myślniki, spacje, nawiasy, “+”)
  • Jeśli zostaje 9 cyfr — formatuje jako XXX-XXX-XXX
  • Jeśli zostaje 11 cyfr zaczynających się od “48” — usuwa prefiks i formatuje jak wyżej
  • W każdym innym przypadku zwraca oryginał z dopiskiem ” (nieznany format)”
  • Przetestuj na: +48-123-456-789, (123) 456 789, 48123456789, 123456789, abc

🔑 Złota reguła do zapamiętania na zawsze:
String jest niemutowalny. Zawsze przypisuj wynik: zmienna = zmienna.Metoda(…)

Masz pytanie? Coś niejasnego? Pytaj śmiało w komentarzach — odpowiadam na każdy.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *