String to najpopularniejszy typ w C# (oprócz int). Każda aplikacja używa tekstów - do wyświetlania UI, komunikacji z API, logowania, przechowywania danych. W tym wpisie nauczysz się wszystkiego o String'ach w C# 14 i .NET 10!
Poznasz raw string literals (C# 11), UTF-8 literals (C# 11), zaawansowaną string interpolation, Span<char> dla wydajności, i najlepsze praktyki pracy z tekstami w 2026 roku.
🔍 Czym jest string w C#?
string to typ referencyjny reprezentujący sekwencję znaków Unicode. Jest immutable (niezmienialny) - każda "zmiana" stringa tworzy nowy string w pamięci!
string s = "Hello";
s = s + " World"; // To NIE zmienia oryginalnego "Hello"!
// Tworzy NOWY string "Hello World"
// "Hello" zostaje w pamięci (garbage collector posprząta później)
string to alias dla System.String - to samo, krótsza nazwa.
String interpolation - podstawy i zaawansowane użycie
Podstawowa string interpolation (C# 6+)
string name = "Jan";
int age = 30;
// String interpolation z $
string message = $"Cześć, {name}! Masz {age} lat.";
Console.WriteLine(message); // Cześć, Jan! Masz 30 lat.
// Możesz używać wyrażeń wewnątrz {}
string info = $"{name} urodzil się w {2026 - age} roku.";
Console.WriteLine(info); // Jan urodzil się w 1996 roku.
// Wywołania metod
string upper = $"Imię wielkimi literami: {name.ToUpper()}";
Console.WriteLine(upper); // Imię wielkimi literami: JAN
❌ Stary sposób - konkatenacja
string name = "Jan";
int age = 30;
// Brzydkie i podatne na błędy!
string message = "Cześć, " + name + "! Masz " + age + " lat.";
// Jeszcze gorsze z wieloma zmiennymi
string complex = "Użytkownik " + name +
" (lat: " + age + ") " +
"zalogował się o " + DateTime.Now + ".";
// 😱 Nieczytelne!
✅ Nowoczesny sposób - interpolation
string name = "Jan";
int age = 30;
// Czytelne i przyjemne!
string message = $"Cześć, {name}! Masz {age} lat.";
// Łatwe do czytania nawet z wieloma zmiennymi
string complex = $"Użytkownik {name} (lat: {age})
zalogował się o {DateTime.Now}.";
// ✅ Super czytelne!
// Verbatim string (@) - zachowuje znaki specjalne
string path1 = "C:\\Users\\Jan\\Documents"; // trzeba escape'ować \
string path2 = @"C:\Users\Jan\Documents"; // ✅ łatwiejsze!
// Połączenie $ i @ - interpolation + verbatim (C# 8+)
string username = "Jan";
string path = $@"C:\Users\{username}\Documents";
Console.WriteLine(path); // C:\Users\Jan\Documents
// Kolejność ma znaczenie w C# < 11:
// C# 6-10: TYLKO $@, nie @$
string correct = $@"Path: {username}"; // ✅
// string wrong = @$"Path: {username}"; // ❌ błąd w C# < 11
// C# 11+: można obie kolejności
string both1 = $@"Path: {username}"; // ✅
string both2 = @$"Path: {username}"; // ✅ też działa od C# 11
🔥 Raw String Literals - C# 11
Problem z multi-line strings
// Stary sposób - verbatim string z @
string oldJson = @"{
""name"": ""Jan"",
""age"": 30,
""email"": ""jan@example.com""
}";
// Problem: trzeba escape'ować quotes (podwójne "")
// Brzydkie i podatne na błędy!
string oldHtml = @"<div class="" container"">
<h1>Hello</h1>
<p>World</p>
</div>";
// Znowu podwójne "" 😞
Rozwiązanie - raw string literals
🎉 NOWOŚĆ C# 11 - Raw String Literals
Użyj """ (trzy cudzysłowy) zamiast " - nie musisz escape'ować NICZEGO!
// Raw string literal - """ (trzy cudzysłowy)
string json = """
{
"name": "Jan",
"age": 30,
"email": "jan@example.com"
}
""";
// ✅ Nie trzeba escape'ować quotes!
// ✅ Zachowuje formatowanie!
// ✅ Super czytelne!
string html = """
Hello
World
""";
// ✅ Quotes działają normalnie!
// Możesz użyć cudzysłowów wewnątrz bez problemu
string withQuotes = """
She said: "Hello World!"
He replied: "Hi there!"
""";
🔍 Jak działają raw string literals?
Zaczynasz z """ (minimum 3 cudzysłowy)
Kończysz z """ (ta sama liczba cudzysłowów)
Zawartość jest między - BEZ escape'owania!
Zachowuje wszystkie znaki specjalne: ", \, \n, etc.
Automatycznie usuwa wspólne wcięcie (indentation)
Interpolation w raw strings
string name = "Jan";
int age = 30;
// Raw string + interpolation - użyj $"""
string message = $"""
Witaj, {name}!
Masz {age} lat.
""";
// Jeśli potrzebujesz {} w tekście, użyj $$""" i {{}}
string json = $$"""
{
"name": "{{name}}",
"age": {{age}},
"metadata": {
"created": "2026-02-06"
}
}
""";
// $$ = interpolacja używa {{}}
// pojedyncze {} to literalne nawiasy!
Console.WriteLine(json);
// {
// "name": "Jan",
// "age": 30,
// "metadata": {
// "created": "2026-02-06"
// }
// }
💡 Praktyczne przykłady raw strings
// SQL query bez escape'owania
string userId = "123";
string sql = $"""
SELECT * FROM Users
WHERE UserId = '{userId}'
AND Status = 'Active'
ORDER BY CreatedDate DESC
""";
// Regex pattern
string emailPattern = """
^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$
""";
// Nie trzeba escape'ować \ w regex! ✅
// HTML template
string title = "Moja Strona";
string content = "Witaj na stronie!";
string htmlTemplate = $$"""
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<p>{{content}}</p>
<script>
console.log("Hello from {{title}}!");
</script>
</body>
</html>
""";
// JSON z zagnieżdżonymi obiektami
string jsonConfig = $$"""
{
"app": {
"name": "MyApp",
"version": "1.0.0",
"settings": {
"debug": {{true.ToString().ToLower()}},
"maxUsers": {{100}}
}
}
}
""";
Indentation w raw strings
void PrintMessage()
{
string message = """
Linia 1
Linia 2
Linia 3 (wcięta)
Linia 4
""";
Console.WriteLine(message);
}
// Output (wcięcie automatycznie usunięte względem zamykającego """):
// Linia 1
// Linia 2
// Linia 3 (wcięta)
// Linia 4
// Zamykające """ określa poziom wcięcia!
void Example2()
{
string text = """
To jest
wieloliniowy
tekst
"""; // wcięcie 8 spacji - usunięte!
}
🔥 UTF-8 String Literals - C# 11
Problem z kodowaniem
Normalnie, stringi w C# są kodowane jako UTF-16 (2 bajty na znak). Gdy wysyłasz dane po sieci (HTTP, gRPC), zazwyczaj używasz UTF-8. To wymaga konwersji:
// Stary sposób - konwersja UTF-16 → UTF-8
string message = "Hello World";
byte[] utf8Bytes = Encoding.UTF8.GetBytes(message); // alokacja + konwersja!
// Używane w HTTP, gRPC, JSON serialization, etc.
await httpClient.PostAsync(url, new ByteArrayContent(utf8Bytes));
⚡ Problem wydajnościowy
Każda konwersja UTF-16 → UTF-8:
Alokuje nową tablicę bajtów (heap allocation)
Kopiuje i konwertuje każdy znak
Garbage Collector musi potem posprzątać
W aplikacjach webowych może to się dziać MILIONY razy na sekundę! 😱
Rozwiązanie - UTF-8 literals
🎉 NOWOŚĆ C# 11 - UTF-8 String Literals
Dodaj u8 na końcu stringa - kompilator wygeneruje UTF-8 bezpośrednio!
// UTF-8 literal - dodaj u8 na końcu
ReadOnlySpan utf8Message = "Hello World"u8;
// ✅ ZERO alokacji!
// ✅ ZERO konwersji!
// ✅ UTF-8 bytes bezpośrednio w binary!
// Porównanie:
// Stary sposób:
string str = "Hello"; // UTF-16 string w pamięci
byte[] bytes = Encoding.UTF8.GetBytes(str); // konwersja + alokacja
// Nowy sposób:
ReadOnlySpan utf8 = "Hello"u8; // UTF-8 bytes bezpośrednio!
string a = "Hello";
string b = "hello";
// == porównuje WARTOŚCI (nie referencje!)
Console.WriteLine(a == b); // false (różna wielkość liter)
//Equals - to samo co ==
Console.WriteLine(a.Equals(b)); // false
// Porównanie case-insensitive
Console.WriteLine(a.Equals(b, StringComparison.OrdinalIgnoreCase)); // true!
// CompareTo - porządek alfabetyczny
Console.WriteLine("apple".CompareTo("banana")); // -1 (apple < banana)
Console.WriteLine("banana".CompareTo("apple")); // 1 (banana > apple)
Console.WriteLine("apple".CompareTo("apple")); // 0 (równe)
⚠️ String comparison - pułapki
// ❌ ŹLE - porównanie case-sensitive gdy nie powinno być
string userInput = "yes";
if (userInput == "Yes") // false! (różna wielkość)
{
// Nie wykona się!
}
// ✅ DOBRZE - case-insensitive
if (userInput.Equals("Yes", StringComparison.OrdinalIgnoreCase))
{
// Wykona się! ✅
}
// Jeszcze lepiej z pattern matching (C# 11+)
if (userInput is "yes" or "Yes" or "YES")
{
// Wykona się dla wszystkich wariantów
}
// Walidacja inputu użytkownika - użyj IsNullOrWhiteSpace
string? userName = GetUserInput();
if (string.IsNullOrWhiteSpace(userName))
{
Console.WriteLine("Imię nie może być puste!");
return;
}
// Sprawdzanie czy JSON field istnieje - użyj IsNullOrEmpty
string? jsonField = GetJsonField("name");
if (!string.IsNullOrEmpty(jsonField))
{
// Pole istnieje i ma wartość
}
StringBuilder - dla wydajności
Problem z konkatenacją stringów
// ❌ BARDZO WOLNE gdy wiele konkatenacji
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString() + ","; // Każda iteracja tworzy NOWY string!
}
// 1000 iteracji = 1000 alokacji stringów!
// Garbage Collector płacze 😭
// ✅ SZYBKIE - StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
sb.Append(',');
}
string result = sb.ToString(); // Tylko JEDNA alokacja na końcu!
// StringBuilder jest mutable - modyfikujesz ten sam obiekt
// Append dodaje do bufora (nie tworzy nowych stringów)
StringBuilder API
var sb = new StringBuilder();
// Append - dodaj na koniec
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");
// AppendLine - dodaj + nowa linia
sb.AppendLine("Linia 1");
sb.AppendLine("Linia 2");
// AppendFormat - formatowanie (stary styl)
sb.AppendFormat("Liczba: {0}, Tekst: {1}", 42, "test");
// C# 10+ AppendInterpolatedString - interpolation!
int age = 30;
string name = "Jan";
sb.AppendLine($"Użytkownik {name} ma {age} lat");
// Insert - wstaw w określonym miejscu
sb.Insert(0, "START: "); // wstaw na początku
// Replace - zamień wszystkie wystąpienia
sb.Replace("World", "C#");
// Remove - usuń zakres znaków
sb.Remove(0, 5); // usuń pierwsze 5 znaków
// Clear - wyczyść całość
sb.Clear();
// ToString - konwertuj do string
string finalString = sb.ToString();
// Length i Capacity
Console.WriteLine($"Length: {sb.Length}"); // aktualna długość
Console.WriteLine($"Capacity: {sb.Capacity}"); // zarezerwowana pojemność
💡 Praktyczny przykład - generowanie CSV
var users = new[]
{
new { Id = 1, Name = "Jan", Email = "jan@example.com", Age = 30 },
new { Id = 2, Name = "Anna", Email = "anna@example.com", Age = 25 },
new { Id = 3, Name = "Piotr", Email = "piotr@example.com", Age = 35 }
};
var csv = new StringBuilder();
// Header
csv.AppendLine("Id,Name,Email,Age");
// Data rows
foreach (var user in users)
{
csv.Append(user.Id);
csv.Append(',');
csv.Append(user.Name);
csv.Append(',');
csv.Append(user.Email);
csv.Append(',');
csv.AppendLine(user.Age.ToString());
}
string csvContent = csv.ToString();
Console.WriteLine(csvContent);
// Output:
// Id,Name,Email,Age
// 1,Jan,jan@example.com,30
// 2,Anna,anna@example.com,25
// 3,Piotr,piotr@example.com,35
Kiedy używać StringBuilder?
❌ StringBuilder NIE jest potrzebny
// Kilka konkatenacji - string interpolation OK
string name = "Jan";
int age = 30;
string message = $"Hello {name}, masz {age} lat";
// ✅ To jest OK! Nie trzeba StringBuilder
// Join - string.Join jest szybki
var words = new[] { "a", "b", "c" };
string joined = string.Join(",", words);
// ✅ To jest OK!
✅ StringBuilder potrzebny
// Pętla z wieloma konkatenacjami
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
sb.Append(',');
}
// ✅ StringBuilder! String by był wolny
// Budowanie dużego dokumentu
var html = new StringBuilder();
foreach (var item in items)
{
html.AppendLine("
");
html.Append(item.Name);
html.AppendLine("
");
}
// ✅ StringBuilder dla wielu append
⚡ Zasada: StringBuilder gdy 5+ konkatenacji w pętli
Złota zasada: Jeśli masz pętlę która buduje string, użyj StringBuilder. Dla 2-3 konkatenacji, zwykły string interpolation jest OK (kompilator może to zoptymalizować).
Span<char> - zero-allocation string operations
Problem z Substring
// Substring alokuje NOWY string
string original = "Hello World";
string sub = original.Substring(0, 5); // "Hello"
// Problem: alokacja nowego stringa w pamięci!
// W pętli to jest drogie:
for (int i = 0; i < 1000; i++)
{
string part = original.Substring(0, 5); // 1000 alokacji!
// ... użycie part
}
Rozwiązanie - Span<char>
🎉 Span<T> - zero-allocation slicing
Span<char> to "view" (widok) na fragment stringa - NIE kopiuje danych!
string original = "Hello World";
// AsSpan() tworzy Span - zero-allocation!
ReadOnlySpan span = original.AsSpan();
// Slice - jak Substring, ale BEZ alokacji!
ReadOnlySpan hello = span.Slice(0, 5); // "Hello" (bez alokacji!)
ReadOnlySpan world = span.Slice(6, 5); // "World" (bez alokacji!)
// Range operator - jeszcze czytelniej
ReadOnlySpan hello2 = span[0..5]; // "Hello"
ReadOnlySpan world2 = span[6..11]; // "World"
// W pętli - ZERO alokacji!
for (int i = 0; i < 1000; i++)
{
ReadOnlySpan part = original.AsSpan(0, 5); // zero alokacji! 🎉
// ... użycie part
}
🔍 Jak działa Span<T>?
Span<T> to "okno" na istniejące dane. Nie kopiuje - tylko wskazuje na fragment:
string text = "Hello World";
// 01234567890
ReadOnlySpan hello = text.AsSpan(0, 5);
// hello wskazuje na znaki 0-4 w oryginalnym stringu
// NIE tworzy nowego stringa!
To jak wskaźnik w C, ale bezpieczny (C# pilnuje granic)!
// CSV line parsing BEZ alokacji stringów!
string csvLine = "Jan,Kowalski,30,Warszawa";
ReadOnlySpan<char> span = csvLine;
int firstComma = span.IndexOf(',');
ReadOnlySpan<char> firstName = span[..firstComma]; // "Jan"
span = span[(firstComma + 1)..]; // "Kowalski,30,Warszawa"
int secondComma = span.IndexOf(',');
ReadOnlySpan<char> lastName = span[..secondComma]; // "Kowalski"
span = span[(secondComma + 1)..]; // "30,Warszawa"
int thirdComma = span.IndexOf(',');
ReadOnlySpan<char> ageSpan = span[..thirdComma]; // "30"
ReadOnlySpan<char> city = span[(thirdComma + 1)..]; // "Warszawa"
// Parse age z Span (zero alokacji!)
int age = int.Parse(ageSpan);
Console.WriteLine($"Imię: {firstName.ToString()}");
Console.WriteLine($"Nazwisko: {lastName.ToString()}");
Console.WriteLine($"Wiek: {age}");
Console.WriteLine($"Miasto: {city.ToString()}");
// Cały parsing BEZ alokacji (oprócz końcowego ToString)!
Kiedy używać Span<char>?
✅ Parsing (CSV, JSON, XML)
✅ String processing w pętlach
✅ Performance-critical code
✅ Mikro-optymalizacje w hot paths
❌ NIE używaj dla normalnego kodu aplikacyjnego
❌ Span nie może być polem klasy (tylko zmienna lokalna/parametr)
⚠️ Ograniczenia Span<T>
Span<T> to ref struct - może być TYLKO na stacku:
class MyClass
{
// ❌ BŁĄD - Span nie może być polem klasy!
private Span<char> _data;
// ❌ BŁĄD - Span nie może być w async metodzie!
async Task ProcessAsync(Span<char> data) { }
// ✅ OK - jako parametr w synchronicznej metodzie
void Process(Span<char> data) { }
// ✅ OK - jako zmienna lokalna
void Method()
{
Span<char> local = stackalloc char[100];
}
}
Jeśli potrzebujesz przechować długoterminowo, użyj Memory<T>.
Podsumowanie
To był mega wpis o String'ach! Nauczyłeś się wszystkiego o pracy z tekstami w C# 14: