🔥 Zapisy zamknięte, ale możesz pobrać Roadmapę .NET i dołączyć do listy oczekujących — Pobierz i dołącz do Listy VIP →

Porównywanie stringów w C# — pułapka tureckiego I

Łańcuchy, czyli ciągi znaków w C#, mają typ string — sekwencję znaków char w standardzie Unicode. Słowo kluczowe string jest aliasem do System.String — oba zapisy są równoważne. Ten artykuł skupia się na jednym, często pomijanym temacie: jak poprawnie porównywać stringi. Po pełną głębię niezmienności (immutability) i konsekwencje wydajnościowe zajrzyj do artykułu string w C# — czym jest i jak działa, a po komplet metod manipulacji tekstem (Replace, Split, Substring i inne) — do Manipulacji tekstem w C#.

Podstawy — tworzenie i indeksowanie

string napis = "Tekst";
//              T  e  k  s  t
// indeks:      0  1  2  3  4

char a = napis[1];    // 'e'
int  n = napis.Length; // 5

Dlaczego == dla stringów porównuje treść, a nie referencję

string jest typem referencyjnym, ale w przeciwieństwie do zwykłych klas, operator == jest dla niego przeciążony tak, by porównywał zawartość, a nie adres w pamięci — dokładnie tak, jak intuicyjnie oczekujesz:

string a = "test";
string b = new string("test".ToCharArray());

Console.WriteLine(a == b);            // true — porównanie treści
Console.WriteLine(ReferenceEquals(a, b)); // false — to dwa różne obiekty w pamięci

To wyjątek, o którym warto pamiętać — dla większości innych typów referencyjnych == domyślnie porównuje referencje, nie zawartość.

StringComparison — kultura kontra porządek znaków

Prosty operator == zawsze porównuje ordinalnie (bajt po bajcie kodu Unicode), z rozróżnieniem wielkości liter. To nie zawsze jest to, czego potrzebujesz — dlatego Equals i Compare przyjmują parametr StringComparison:

string a = "Kraków";
string b = "KRAKÓW";

a.Equals(b);                                          // false — różne wielkości liter
a.Equals(b, StringComparison.OrdinalIgnoreCase);       // true — ignoruje wielkość liter, bez wpływu kultury
a.Equals(b, StringComparison.CurrentCultureIgnoreCase); // true, ale wynik zależy od bieżącej kultury systemu
  • Ordinal / OrdinalIgnoreCase — porównanie binarne, niezależne od ustawień regionalnych. Wybierz do porównań technicznych: kluczy, nazw plików, identyfikatorów, adresów URL, haseł.
  • CurrentCulture / CurrentCultureIgnoreCase — porównanie z uwzględnieniem reguł językowych bieżącej kultury systemu (kolejność alfabetyczna, znaki diakrytyczne). Wybierz do sortowania i porównań widocznych dla użytkownika w interfejsie.
  • InvariantCulture / InvariantCultureIgnoreCase — reguły językowe niezależne od ustawień systemu, ale wciąż “kulturowe” (inne niż czysto binarne Ordinal). Rzadziej potrzebne niż dwie powyższe opcje.

Pułapka: turecki problem z literą “i”

To klasyczny, realny bug, który pokazuje, dlaczego wybór StringComparison ma znaczenie. W kulturze tureckiej wielka litera odpowiadająca małemu i to İ (z kropką), a nie zwykłe I:

string status = "INACTIVE";

// Na maszynie z turecką kulturą systemową ToUpper()/ToLower() bez parametru
// używa CurrentCulture — "inactive".ToUpper() może NIE dać "INACTIVE"!
bool czyNieaktywny = status.ToUpper() == "INACTIVE"; // ryzykowne — zależy od kultury systemu

// Bezpieczne: porównanie niezależne od kultury
bool bezpieczne = string.Equals(status, "INACTIVE", StringComparison.OrdinalIgnoreCase);

Dla porównań technicznych (kody statusów, klucze konfiguracyjne, stałe w kodzie) zawsze używaj wariantu Ordinal — to jednocześnie bezpieczniejsze i szybsze niż warianty kulturowe, bo pomija cały mechanizm reguł językowych.

Compare() — porządkowanie, nie tylko równość

Gdy potrzebujesz nie tylko “czy równe”, ale też “co jest wcześniej w porządku sortowania”, użyj string.Compare() — zwraca liczbę ujemną, zero lub dodatnią:

int wynik = string.Compare("Ala", "Basia", StringComparison.Ordinal);
// wynik < 0  => "Ala" jest przed "Basia" w porządku sortowania
// wynik == 0 => równe
// wynik > 0  => "Ala" jest po "Basia"

To dokładnie ten mechanizm, na którym opierają się domyślne przeciążenia List<T>.Sort() i LINQ OrderBy() dla stringów.

Szybka ściąga: Join, Concat, ToCharArray

  • string.Join(separator, elementy) — łączy elementy kolekcji w jeden łańcuch, z separatorem między nimi.
  • string.Concat(...) — skleja łańcuchy bez separatora (odpowiednik operatora +).
  • napis.ToCharArray() — kopiuje znaki do tablicy char[], gdy potrzebujesz API operującego na tablicach.
string[] slowa = { "Ala", "ma", "kota" };
string zdanie = string.Join(" ", slowa); // "Ala ma kota"

Jeśli chcesz zrozumieć, dlaczego string w C# zachowuje się tak, a nie inaczej — referencje, niezmienność, stack vs heap — zobacz string w C# — czym jest i jak działa.

FAQ

Której opcji StringComparison użyć domyślnie?
Jeśli nie masz konkretnego powodu, by uwzględniać kulturę użytkownika, wybieraj Ordinal/OrdinalIgnoreCase — jest przewidywalny, szybki i bezpieczny niezależnie od tego, na jakim systemie działa aplikacja.

Czy == i Equals() dla stringów to to samo?
Domyślnie tak — oba porównują treść ordinalnie. Różnica pojawia się dopiero, gdy jawnie podasz StringComparison do Equals(), czego operator == nie obsługuje.

👨‍💻
Mariusz Jurczenko
Senior .NET Developer · 10+ lat doświadczenia komercyjnego

Programista .NET z doświadczeniem komercyjnym w firmach takich jak NFZ, Kamsoft, Diagnostyka, Hermes Reply Polska czy Etisoft Smart Solutions. Twórca kursów, z których skorzystało już ponad 11 000 osób w Strefie Kursów i ponad 1 000 kursantów na dev-hobby.pl.

Specjalizacja: Clean Code, Clean Architecture i uczenie programowania tak, żeby dało się je naprawdę zrozumieć — nie wykuć.

🚀 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.

2 comments

Dodaj komentarz

czytanie to początek

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ę →