String — tekst, którego nie da się zmienić

Uczeń wpisuje nazwę z dodatkowymi spacjami i wielkimi literami. Program ma to uporządkować — a przy okazji nauczysz się reguły, o którą potyka się każdy początkujący.

C# string Trim Substring 55 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego metoda tekstowa nie zmienia napisu, na którym ją wywołano
  • Oczyścisz i przekształcisz napis metodami Trim, Replace i ToUpper
  • Odnajdziesz fragment metodami Contains, StartsWith i IndexOf
  • Wytniesz część napisu metodą Substring, nie wychodząc poza zakres
  • Porównasz napisy tak, żeby wielkość liter miała albo nie miała znaczenia — świadomie

Przygotowanie: lekcje 01–07. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Napisu nie da się zmienić

To zaskakuje każdego: w C# żadna metoda nie zmienia istniejącego napisu. Każda tworzy nowy i zwraca go jako wynik. Napis raz utworzony pozostaje taki na zawsze — mówimy, że string jest niemutowalny.

Program.cs
string tekst = "  Ala  ";

tekst.Trim();                     // wynik POWSTAJE i natychmiast ginie
Console.WriteLine($"[{tekst}]");  // [  Ala  ]  - bez zmian!

tekst = tekst.Trim();             // TAK: przypisujemy wynik z powrotem
Console.WriteLine($"[{tekst}]");  // [Ala]
wynik w konsoli
[  Ala  ]
[Ala]

Najczęstszy błąd w tej lekcji

Wywołanie tekst.Trim();, tekst.Replace(…); albo tekst.ToUpper(); bez przypisania nie robi nic widocznego. Kompilator nie protestuje — kod jest poprawny, tylko bezużyteczny. Jeśli napis „się nie zmienia”, w dziewięciu przypadkach na dziesięć brakuje właśnie tekst = .

Można też zapisać wynik do innej zmiennej — wtedy masz obie wersje naraz:

Program.cs
string oryginal = "  Ala  ";
string czysty = oryginal.Trim();

Console.WriteLine($"[{oryginal}]");  // [  Ala  ]  - nietkniety
Console.WriteLine($"[{czysty}]");    // [Ala]

Po co taka niewygoda

Niezmienność ma swoje zalety: napis można bezpiecznie przekazywać w wiele miejsc programu, mając pewność, że nikt go po drodze nie podmieni. Ceną jest koszt — sklejanie tysięcy napisów w pętli tworzy tysiące obiektów. Rozwiązanie tego problemu, klasa StringBuilder, czeka w lekcji 29.

TEORIA

Czyszczenie, zamiana, wielkość liter

Program.cs
string wpis = "   Ala Kowalska   ";

Console.WriteLine($"[{wpis.Trim()}]");        // [Ala Kowalska]
Console.WriteLine($"[{wpis.TrimStart()}]");   // [Ala Kowalska   ]
Console.WriteLine($"[{wpis.TrimEnd()}]");     // [   Ala Kowalska]

string zdanie = "kot i kot";
Console.WriteLine(zdanie.Replace("kot", "pies"));  // pies i pies
Console.WriteLine(zdanie.ToUpper());               // KOT I KOT
Console.WriteLine(zdanie.ToLower());               // kot i kot

Console.WriteLine(zdanie);                         // kot i kot - oryginal caly czas ten sam

Zwróć uwagę na dwie rzeczy. Trim usuwa białe znaki wyłącznie ze skrajów — spacja między imieniem a nazwiskiem zostaje. Replace zamienia wszystkie wystąpienia, nie tylko pierwsze.

ToUpper i litera „i”

W języku tureckim wielką literą od i jest İ, a nie I. Program uruchomiony na komputerze z tureckimi ustawieniami regionalnymi da inny wynik ToUpper niż twój. Gdy zamieniasz wielkość liter po to, żeby porównać techniczne kody — start, STOP, nazwy plików — używaj wersji ToUpperInvariant albo, jeszcze lepiej, porównania z StringComparison opisanego niżej.

TEORIA

Szukanie fragmentów i wycinanie

Program.cs
string plik = "raport-2026.txt";

Console.WriteLine(plik.Contains("2026"));      // True
Console.WriteLine(plik.StartsWith("raport"));  // True
Console.WriteLine(plik.EndsWith(".txt"));      // True
Console.WriteLine(plik.IndexOf('-'));          // 6   - pozycja pierwszego myslnika
Console.WriteLine(plik.IndexOf("2026"));       // 7
Console.WriteLine(plik.IndexOf("csv"));        // -1  - nie znaleziono

Console.WriteLine(plik.Substring(7));          // 2026.txt   - od indeksu do konca
Console.WriteLine(plik.Substring(7, 4));       // 2026       - 4 znaki od indeksu 7
Console.WriteLine(plik.Substring(0, 6));       // raport
MetodaZwracaUwaga
Contains(x)boolCzy fragment gdziekolwiek występuje. Rozróżnia wielkość liter.
StartsWith(x) / EndsWith(x)boolCzy napis zaczyna się / kończy podanym fragmentem.
IndexOf(x)intPozycja pierwszego wystąpienia albo −1, gdy nie ma. LastIndexOf szuka od końca.
Substring(i)stringWszystko od indeksu i do końca.
Substring(i, n)stringDokładnie n znaków od indeksu i. Wyjście poza napis daje wyjątek.

Minus jeden to nie pozycja

IndexOf przy braku fragmentu zwraca -1. Podanie tej wartości do Substring kończy się wyjątkiem ArgumentOutOfRangeException. Od lekcji 14 będziesz ten wynik sprawdzać instrukcją warunkową — na razie zapamiętaj, że taka pułapka istnieje.

TEORIA

Porównywanie napisów

Dwa napisy porównujemy operatorem == — i dla string porównuje on zawartość, a nie miejsce w pamięci. To wyjątek wśród typów złożonych, wygodny i celowy.

Program.cs
string a = "Ala";
string b = "Ala";

Console.WriteLine(a == b);         // True  - taka sama zawartosc
Console.WriteLine(a == "ala");     // False - wielkosc liter ma znaczenie

Gdy wielkość liter ma być nieistotna, nie kombinuj z ToLower — powiedz to wprost:

Program.cs
string wpis = "  START  ";

Console.WriteLine(string.Equals(wpis.Trim(), "start", StringComparison.OrdinalIgnoreCase));
// True

Console.WriteLine(string.IsNullOrEmpty(""));           // True
Console.WriteLine(string.IsNullOrWhiteSpace("   "));   // True
Console.WriteLine(string.IsNullOrWhiteSpace("Ala"));   // False
Tryb StringComparisonKiedy używać
Ordinalporównanie numerów znaków — szybkie i przewidywalne; do kodów, nazw plików, identyfikatorów
OrdinalIgnoreCaseto samo, ale bez rozróżniania wielkości liter — domyślny wybór dla komend i kodów
CurrentCulturereguły języka użytkownika — do porządkowania tekstu pokazywanego człowiekowi
CurrentCultureIgnoreCasejak wyżej, bez wielkości liter

Puste, białe i „nic”

"" to napis pusty — istnieje, ale ma zero znaków. " " to napis z samymi spacjami — nie jest pusty, choć wygląda jak nic. null oznacza brak napisu w ogóle i próba wywołania na nim metody kończy się wyjątkiem. Metoda string.IsNullOrWhiteSpace obsługuje wszystkie trzy przypadki naraz; do null wrócisz w lekcji 34.

TEORIA

Trzy sposoby na złożenie tekstu

Program.cs
string imie = "Ala";
int punkty = 18;

// 1. sklejanie operatorem +
Console.WriteLine("Uczen " + imie + " ma " + punkty + " pkt");

// 2. interpolacja - czytelniejsza, ta uzywamy najczesciej
Console.WriteLine($"Uczen {imie} ma {punkty} pkt");

// 3. napis doslowny @ - ukosniki bez podwajania
Console.WriteLine(@"C:\dane\raport.txt");
Console.WriteLine("C:\\dane\\raport.txt");   // to samo, zapis zwykly
wynik w konsoli
Uczen Ala ma 18 pkt
Uczen Ala ma 18 pkt
C:\dane\raport.txt
C:\dane\raport.txt

Przy sklejaniu operatorem + liczba zostaje automatycznie zamieniona na tekst. To wygoda, ale i pułapka znana z lekcji 09: "Wynik: " + 5 + 5 daje Wynik: 55, bo dodawanie idzie od lewej.

Znak i napis też można skleić — powstaje napis:

Program.cs
string kod = "A7B2";
char pierwszy = kod[0];

string opis = "Pierwszy znak: " + pierwszy;
Console.WriteLine(opis);                  // Pierwszy znak: A
Console.WriteLine(opis.Length);           // 16

Dzielenie napisu na części

Napis z separatorami — "Ala;Ola;Jan" — rozbija się metodą Split. Jej wynikiem jest tablica, a tablice poznasz dopiero w lekcji 23. Dlatego Split wraz z całym warsztatem przetwarzania danych czeka na ciebie w lekcji 29.

PRZYKŁAD

Przykład: porządkowanie wpisu

Program dostaje niechlujny wpis i doprowadza go do porządku. Przewidź każdy wiersz wyniku, zanim uruchomisz.

Program.cs
string wpis = "   raport-2026.TXT   ";

Console.WriteLine($"oryginal      : [{wpis}]");
Console.WriteLine($"dlugosc       : {wpis.Length}");
Console.WriteLine();

string czysty = wpis.Trim();
Console.WriteLine($"po Trim       : [{czysty}]  dlugosc: {czysty.Length}");

string maly = czysty.ToLowerInvariant();
Console.WriteLine($"malymi        : {maly}");

Console.WriteLine($"zaczyna sie od 'raport'? {maly.StartsWith("raport")}");
Console.WriteLine($"konczy sie na '.txt'?    {maly.EndsWith(".txt")}");
Console.WriteLine($"pozycja myslnika         {maly.IndexOf('-')}");
Console.WriteLine();

string rok = maly.Substring(7, 4);
string nazwa = maly.Substring(0, 6);
Console.WriteLine($"nazwa: {nazwa}, rok: {rok}");
Console.WriteLine($"nowa nazwa: {maly.Replace("2026", "2027")}");
Console.WriteLine();

Console.WriteLine($"oryginal nadal: [{wpis}]");
wynik w konsoli
oryginal      : [   raport-2026.TXT   ]
dlugosc       : 22

po Trim       : [raport-2026.TXT]  dlugosc: 15
malymi        : raport-2026.txt
zaczyna sie od 'raport'? True
konczy sie na '.txt'?    True
pozycja myslnika         6

nazwa: raport, rok: 2026
nowa nazwa: raport-2027.txt

oryginal nadal: [   raport-2026.TXT   ]

Co dzieje się po kolei

  • Nawiasy kwadratowe w wypisywaniu to zwykłe znaki — pokazują, gdzie kończy się napis, więc spacje przestają być niewidoczne. Warto używać ich zawsze przy testowaniu Trim.
  • Każda operacja zwraca nowy napis i przypisujemy go do nowej zmiennej. Ostatni wiersz dowodzi, że wpis przez cały czas był nietknięty.
  • ToLowerInvariant, a nie ToLower — nazwa pliku to kod techniczny, więc nie chcemy zależeć od ustawień regionalnych komputera.
  • Liczby 7, 4, 0 i 6 w Substring wpisano na sztywno, bo znamy budowę tego konkretnego napisu. W programie przyjmującym dowolne dane wyliczyłbyś je z IndexOf — po lekcji 14, gdy będziesz umiał sprawdzić wynik -1.
ELEMENTY WBUDOWANE

Zestawienie metod klasy string

Metoda / właściwośćZwracaDziałanie i ograniczenia
tekst.LengthintLiczba jednostek char. Właściwość — bez nawiasów.
tekst[i]charZnak o indeksie i. Poza zakresem — wyjątek.
tekst.Trim()stringNowy napis bez skrajnych białych znaków. Środka nie rusza. Są też TrimStart i TrimEnd.
tekst.ToUpper() / ToLower()stringZmieniona wielkość liter według ustawień regionalnych. Wersje …Invariant — niezależne od nich.
tekst.Replace(a, b)stringZamienia wszystkie wystąpienia. Podanie pustego b usuwa fragmenty.
tekst.Contains(x)boolCzy fragment występuje. Rozróżnia wielkość liter, chyba że podasz StringComparison.
tekst.StartsWith(x) / EndsWith(x)boolSprawdzenie początku i końca napisu.
tekst.IndexOf(x)intPozycja pierwszego wystąpienia albo -1. LastIndexOf — od końca.
tekst.Substring(i, n)stringFragment od indeksu i o długości n. Zły zakres — ArgumentOutOfRangeException.
string.Equals(a, b, tryb)boolPorównanie według wskazanego trybu; radzi sobie z null.
string.IsNullOrEmpty(t)booltrue dla null i napisu pustego.
string.IsNullOrWhiteSpace(t)boolJak wyżej, a dodatkowo dla samych białych znaków.
$"…{x}…"stringInterpolacja — wstawia wartość wyrażenia. Pełne formatowanie w lekcji 13.
@"…"stringNapis dosłowny: ukośniki bez podwajania, wolno łamać wiersze.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
tekst.Trim();Wynik ginie. Napisz tekst = tekst.Trim();.
tekst[0] bez sprawdzenia długościPusty napis nie ma indeksu 0 — wyjątek.
Substring(tekst.IndexOf("x"))Gdy fragmentu nie ma, IndexOf daje -1 i Substring zgłasza wyjątek.
a.ToLower() == b.ToLower()Działa w większości przypadków, ale zależy od ustawień regionalnych i tworzy dwa zbędne napisy. Użyj string.Equals(a, b, StringComparison.OrdinalIgnoreCase).
tekst == "" jako sprawdzenie „pusty”Napis ze spacjami przejdzie ten test. Użyj string.IsNullOrWhiteSpace.
"Suma: " + 2 + 3Da Suma: 23. Policz najpierw: "Suma: " + (2 + 3) albo użyj interpolacji.

Dalszy ciąg tematu

Metody klasy string wystarczają do prostych zadań. Gdy tekst budujesz w pętli o wielu obiegach albo rozbijasz całe pliki danych, sięgniesz po StringBuilder i opcje metody Split z lekcji 29.

ZADANIA

Zadania

ZAD 1Dowód niezmienności★☆☆

Zapisz w zmiennej napis " Robot ". Wywołaj na nim Trim bez przypisania i wypisz zmienną między nawiasami kwadratowymi. Potem przypisz wynik i wypisz ponownie. Opisz w komentarzu, co się zmieniło i dlaczego.

ZAD 2Rozbiór nazwy pliku★☆☆

Dla napisu "sprawozdanie-3b.docx" wypisz: długość, pozycję myślnika, pozycję kropki, sam rozszerzenie oraz nazwę bez rozszerzenia. Użyj IndexOf i Substring.

ZAD 3Komenda mimo bałaganu★★☆

Sprawdź, czy wpis " START " po przycięciu jest komendą start bez względu na wielkość liter. Wypisz wynik dla tego wpisu oraz dla "restart" i " ". Nie używaj ToLower.

ZAD 4Cztery rodzaje pustki★★☆

Dla czterech napisów: "", " ", "\t" i "Ala" wypisz w kolumnach ich Length, wynik IsNullOrEmpty i wynik IsNullOrWhiteSpace. Wyjaśnij różnicę między drugą a trzecią kolumną.

ZAD 5Maskowanie numeru★★☆

Z napisu "4417-2938-5561-0092" zbuduj wersję zamaskowaną "****-****-****-0092": pobierz cztery ostatnie znaki metodą Substring i doklej je do stałego przedrostka. Nie wpisuj końcówki na sztywno.

ZAD 6Wizytówka★★★

Mając trzy zmienne — imię, nazwisko i klasę — zbuduj i wypisz wizytówkę: pierwsza litera imienia i pełne nazwisko wielkimi literami (A. KOWALSKA), pod spodem klasa, a pod nią linia z tylu myślników, ile znaków ma najdłuższy z tych dwóch wierszy. Długość policz z Length, nie licz jej ręcznie.

PODSUMOWANIE

Co trzeba zapamiętać

  • Napis jest niezmienny — każda metoda zwraca nowy napis, a stary zostaje nietknięty.
  • Bez przypisania wyniku (tekst = tekst.Trim();) nic się nie zmieni.
  • Trim działa tylko na skrajach, Replace zamienia wszystkie wystąpienia.
  • IndexOf zwraca -1, gdy nie znajdzie — nie podawaj tego wyniku do Substring.
  • Dla string operator == porównuje zawartość; wielkość liter ma znaczenie.
  • Porównania niezależne od wielkości liter zapisuj przez string.Equals z StringComparison, nie przez ToLower.

Dokumentacja: Microsoft Learn — temat tej lekcji.