Konwersje typów w C# — casting i Parse

C# daje ci co najmniej cztery różne narzędzia do zamiany jednego typu na drugi — rzutowanie (int), int.Parse(), int.TryParse() i Convert.ToInt32() — i wszystkie wyglądają, jakby robiły to samo. Nie robią. Różnice ujawniają się dopiero na granicznych przypadkach: null, liczba zmiennoprzecinkowa z ułamkiem, tekst od użytkownika z przecinkiem zamiast kropki. Ten artykuł zakłada, że znasz już podstawową konwersję z Zmiennych i typów danych — tu wchodzimy głębiej, w miejsca, gdzie realnie coś się psuje.
Niejawna (implicit) vs jawna (explicit) konwersja
int liczbaCalkowita = 150;
double liczbaZmiennoprzecinkowa = liczbaCalkowita; // ✅ niejawna — int mieści się w double bez utraty danych
double pi = 3.14;
int obcietePi = (int)pi; // trzeba jawnie — konwersja MOŻE stracić dane (część ułamkowa)
Kompilator pozwala na niejawną konwersję tylko wtedy, gdy jest bezpieczna — typ docelowy mieści cały zakres typu źródłowego (int → double zawsze się uda). Konwersja, która może stracić dane (double → int, long → int) wymaga jawnego rzutowania — to świadoma zgoda programisty na ryzyko utraty precyzji.
Pułapka: rzutowanie OBCINA, Convert ZAOKRĄGLA
double cena = 19.99;
int rzutowanie = (int)cena; // 19 — obcina część ułamkową (w stronę zera)
int convert = Convert.ToInt32(cena); // 20 — zaokrągla do najbliższej liczby całkowitej
To jedna z najbardziej zaskakujących różnic w C# — (int) i Convert.ToInt32() robią coś innego, mimo że oba “zamieniają double na int”. Rzutowanie zawsze obcina w stronę zera (19.99 → 19, ale też -19.99 → -19, nie -20). Convert.ToInt32 zaokrągla do najbliższej liczby całkowitej, a przy dokładnie .5 używa zaokrąglenia bankierskiego (do najbliższej liczby parzystej):
Convert.ToInt32(2.5); // 2, nie 3 — zaokrąglenie do parzystej
Convert.ToInt32(3.5); // 4
Jeśli potrzebujesz konkretnego zachowania zaokrąglania (zawsze w górę, zawsze matematycznie), użyj Math.Round(cena, MidpointRounding.AwayFromZero) zamiast polegać na domyślnym zachowaniu żadnej z tych dwóch metod.
String → liczba: Parse, TryParse, Convert
string wejscie = "42";
int a = int.Parse(wejscie); // rzuci wyjątek, jeśli string nie jest poprawną liczbą
bool ok = int.TryParse(wejscie, out int b); // bezpieczna wersja, zwraca bool zamiast wyjątku
int c = Convert.ToInt32(wejscie); // podobne do Parse, ale...
Convert.ToInt32(null) nie rzuca wyjątku — zwraca 0. int.Parse(null) rzuca ArgumentNullException. To realna, cicha różnica: kod używający Convert może “działać” z brakującymi danymi, podstawiając zero tam, gdzie Parse uczciwie by się wywalił i ujawnił problem wcześniej.
int.Parse(null); // ❌ ArgumentNullException
Convert.ToInt32(null); // 0 — brak błędu, ale prawdopodobnie nie to, czego oczekujesz
Reguła praktyczna: dane z zewnątrz (formularz, API, plik) — zawsze TryParse. Dane, o których jesteś pewien, że są poprawne (stała w kodzie, wynik wcześniejszej walidacji) — Parse jest ok, bo wyjątek przy nieoczekiwanej wartości to sygnał realnego buga, nie coś do cichego ukrycia.
if (!int.TryParse(wejscieUzytkownika, out int wiek))
{
Console.WriteLine("Nieprawidłowy wiek — wpisz liczbę");
return;
}
// tu wiek jest już bezpieczną, poprawną liczbą
Pułapka: separator dziesiętny zależy od ustawień regionalnych
double.Parse("19,99"); // w Polsce (kultura pl-PL): 19.99 ✅
double.Parse("19.99"); // w Polsce: ❌ FormatException — przecinek to separator dziesiętny, kropka to nic
Parse bez dodatkowego parametru używa bieżącej kultury systemu, na którym działa aplikacja. Serwer skonfigurowany na en-US oczekuje kropki; serwer na pl-PL oczekuje przecinka. Kod, który działa poprawnie na Twoim laptopie, może się wywalić na serwerze produkcyjnym z inną kulturą systemową — klasyczny bug “u mnie działało”. Rozwiązanie: dla danych, które MUSZĄ być parsowane spójnie niezależnie od serwera (API, pliki konfiguracyjne, dane z bazy), zawsze podawaj CultureInfo jawnie:
double.Parse("19.99", CultureInfo.InvariantCulture); // zawsze kropka, niezależnie od serwera
CultureInfo.InvariantCulture to stały, niezależny od regionu format — używaj go wszędzie tam, gdzie format liczby pochodzi z systemu (API, JSON, XML), a CurrentCulture (domyślne zachowanie Parse) tylko tam, gdzie faktycznie chcesz dopasować się do ustawień regionalnych użytkownika (formularz w UI wyświetlany Polakowi).
Pułapka: 19,99 zł zamienia się w 1999 zł — cichy kierunek tego samego błędu
Sekcja wyżej pokazuje głośny kierunek: kropka na polskiej maszynie daje FormatException. Jest drugi kierunek, groźniejszy, bo cichy — przecinek na maszynie z inną kulturą nie rzuca żadnego wyjątku, tylko daje inną liczbę. Pokazuję go w odcinku Program .NET Developer #1; kod poniżej jest dokładnie tym samym kodem, który uruchamiam w filmie.
Ta sama linijka, dwie maszyny
Żeby nie stawiać drugiego komputera, serwer symuluję w jednym programie — przestawiam CultureInfo.CurrentCulture, czyli dokładnie to, co za nas robi system operacyjny serwera:
using System.Globalization;
string priceText = "19,99";
CultureInfo.CurrentCulture = new CultureInfo("pl-PL");
decimal onLaptop = decimal.Parse(priceText);
Console.WriteLine($"Laptop (pl-PL): {onLaptop} zł");
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
decimal onServer = decimal.Parse(priceText);
Console.WriteLine($"Serwer (Invariant): {onServer} zł");Laptop (pl-PL): 19,99 zł
Serwer (Invariant): 1999 złTysiąc dziewięćset dziewięćdziesiąt dziewięć — i żadnego wyjątku. Wynik składa się z trzech faktów. decimal.Parse bez podanej kultury bierze ustawienia maszyny. W kulturze Invariant (tak samo jak w en-US) przecinek jest separatorem tysięcy, a nie dziesiętnym. A parser nie sprawdza, czy separator tysięcy stoi co trzy cyfry — „19,99” czyta jak „1,999” bez jednej cyfry.
02-Blad-Na-Serwerze.cs
Pierwsza linia wypisze Laptop (pl-PL): 19,99 zł. A druga?
FormatException dostajesz w odwrotnym kierunku: "19.99" na pl-PL. Tu parser przyjmuje przecinek jako separator tysięcy.| Dane | Maszyna | Wynik | Rodzaj błędu |
|---|---|---|---|
"19.99" | pl-PL | FormatException | głośny — widać go w logach od razu |
"19,99" | Invariant / en-US | 1999 | cichy — liczba idzie do bazy i na fakturę |
Gdzie serwer ma „inną kulturę” — także kontenery bez ICU
Region serwerowni nie ma tu nic do rzeczy — liczą się ustawienia regionalne systemu, na którym działa proces. Poza serwerami postawionymi z angielskimi ustawieniami są jeszcze kontenery: oficjalne obrazy .NET oparte na Alpine i Ubuntu Chiseled nie zawierają biblioteki ICU, więc aplikacja działa w trybie niezmiennej globalizacji i widzi wyłącznie kulturę Invariant (dokumentacja dotnet-docker). Sprawdziłem to na programie, który na moim laptopie wypisuje „19,99 zł”: uruchomiony w trybie bez ICU wypisuje Cena z cennika: 1999 zł.
Pierwszy odruch — „podam new CultureInfo("pl-PL") jawnie” — zadziała na zwykłym serwerze, ale w kontenerze bez ICU samo utworzenie kultury pl-PL rzuca CultureNotFoundException. Poprawka wywróciłaby aplikację dokładnie tam, gdzie błąd występuje.
Poprawka: format podany jawnie i TryParse ze sprawdzonym wynikiem
using System.Globalization;
string[] priceList = { "19,99", "1 299,00", "19.99 zł" };
// Serwer bez polskich ustawień — kod ma działać i tutaj.
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var priceFormat = new NumberFormatInfo
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = " "
};
foreach (string priceText in priceList)
{
if (TryParsePrice(priceText, priceFormat, out decimal price))
{
Console.WriteLine($"{priceText,-10} -> {price.ToString("N2", priceFormat)} zł");
}
else
{
Console.WriteLine($"{priceText,-10} -> nie rozpoznano ceny, pomijam");
}
}
static bool TryParsePrice(string text, NumberFormatInfo format, out decimal price)
{
return decimal.TryParse(text, NumberStyles.Number, format, out price);
}19,99 -> 19,99 zł
1 299,00 -> 1 299,00 zł
19.99 zł -> nie rozpoznano ceny, pomijamNumberFormatInfo z przecinkiem i spacją to zwykły obiekt, który tworzysz sam — nie pochodzi z systemu, więc nie potrzebuje ICU i działa także w kontenerze. TryParse ze sprawdzonym wynikiem odrzuca złą cenę głośno, zamiast przepuścić ją jako 1999. Ten sam output dostałem normalnie i w trybie bez ICU.
Przed poprawką i po poprawce
Wybierz maszynę. Wyniki są zmierzone na .NET 10 — nie wywnioskowane.
PRZED · 01-Start.cs
PO · 03-Final.cs
Laptop: oba programy dają poprawną cenę — dlatego ten błąd przechodzi przez testy na komputerze programisty.
Trzy pułapki, które wracają po poprawce
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; // serwer
// Pułapka 1: Convert.ToDecimal też czyta ustawienia regionalne maszyny
decimal viaConvert = Convert.ToDecimal("19,99");
Console.WriteLine($"Convert.ToDecimal: {viaConvert}");
// Pułapka 2: wynik TryParse zignorowany — zła cena staje się ceną 0
decimal.TryParse("19.99 zł", NumberStyles.Number, CultureInfo.InvariantCulture, out decimal price);
Console.WriteLine($"Cena po TryParse: {price}");
// Pułapka 3: ToString() bez kultury — serwer zapisuje kropkę...
string saved = 19.99m.ToString();
Console.WriteLine($"Zapisane na serwerze: {saved}");
// ...a polski laptop tej kropki nie rozumie
CultureInfo.CurrentCulture = new CultureInfo("pl-PL");
try
{
decimal readBack = decimal.Parse(saved);
Console.WriteLine(readBack);
}
catch (FormatException)
{
Console.WriteLine("Laptop: FormatException");
}Convert.ToDecimal: 1999
Cena po TryParse: 0
Zapisane na serwerze: 19.99
Laptop: FormatExceptionConvert.ToDecimal nie omija problemu — czyta te same ustawienia maszyny. Zignorowany wynik TryParse zamienia złą cenę w cenę zero, bo tyle dostaje zmienna out po nieudanym parsowaniu. A ToString() bez kultury to ten sam błąd odwrócony: serwer zapisuje kropkę, której polski laptop potem nie odczyta.
Zasada: dane od maszyn (pliki, API, bazy) zapisuj i czytaj w jednym ustalonym formacie — najlepiej
InvariantCulture— w obie strony. Dane naprawdę polskie parsuj z formatem podanym jawnie. Dane z zewnątrz —TryParse, i zawsze sprawdzaj, co zwrócił.
Zadanie z eksperymentem
using System.Globalization;
var priceFormat = new NumberFormatInfo
{
NumberDecimalSeparator = ",",
NumberGroupSeparator = " "
};
string priceText = "1 299,00";
bool ok = decimal.TryParse(priceText, NumberStyles.Number, priceFormat, out decimal price);
Console.WriteLine($"{ok}: {price.ToString(priceFormat)}");Uruchom — konsola wypisze True: 1299,00. Potem celowo zepsuj: w wywołaniu TryParse zamień priceFormat na CultureInfo.InvariantCulture. Wyjątek, cicha zła liczba, czy false? I odpowiedz sobie (albo w komentarzu): dlaczego „19,99” na Invariant daje 1999, a „1 299,00” nie daje żadnej liczby?
Pułapka: przepełnienie przy rzutowaniu między typami całkowitymi milczy domyślnie
int duzaLiczba = 300;
byte maly = (byte)duzaLiczba; // byte mieści 0-255... i co teraz?
Console.WriteLine(maly); // 44 — nie wyjątek, tylko "obcięcie" bitów (300 - 256 = 44)
Rzutowanie między typami całkowitymi, gdzie wartość nie mieści się w typie docelowym, domyślnie nie rzuca wyjątku — po cichu obcina najstarsze bity, dając wynik, który wygląda jak poprawna liczba, ale nią nie jest. To szczególnie niebezpieczne w kodzie liczącym pieniądze albo indeksy. Blok checked wymusza wyjątek zamiast cichego obcięcia:
checked
{
byte maly = (byte)duzaLiczba; // ✅ OverflowException zamiast cichego 44
}
Domyślne zachowanie C# to unchecked (szybsze, ale ryzykowne) — świadomie używaj checked w miejscach, gdzie przepełnienie oznacza realny błąd danych, nie “działanie zgodnie z projektem” (jak w hashowaniu, gdzie zawijanie bitów bywa zamierzone).
Rzutowanie obiektów: (Type) kontra as
object dane = "tekst";
string s1 = (string)dane; // rzutowanie — rzuci InvalidCastException, jeśli się nie uda
string? s2 = dane as string; // operator 'as' — zwróci null zamiast wyjątku, jeśli się nie uda
object liczba = 42;
string? s3 = liczba as string; // null — 42 (int) to nie string, ale bez wyjątku
as działa tylko dla typów referencyjnych (i Nullable<T>) i zamiast wyjątku zwraca null przy niepowodzeniu — wygodne, gdy chcesz sprawdzić typ bez obsługi wyjątku, ale wymaga potem sprawdzenia null zamiast try/catch. Nowoczesna alternatywa, często czytelniejsza:
if (dane is string tekst)
{
Console.WriteLine(tekst.ToUpper()); // 'tekst' jest już rzutowany, gotowy do użycia
}
Wzorzec is z deklaracją zmiennej łączy sprawdzenie typu i rzutowanie w jednym kroku — bez ryzyka wyjątku i bez osobnej zmiennej nullable do sprawdzania.
Konwersje typów często idą w parze z operatorami arytmetycznymi — zobacz Operatory w C#. Podstawy typów, które konwertujesz, znajdziesz w Zmienne i typy danych w C#.
Podsumowanie
Cztery narzędzia, cztery różne zastosowania: rzutowanie (int) do konwersji między znanymi typami liczbowymi (pamiętaj — obcina, nie zaokrągla), TryParse do danych z zewnątrz, których poprawności nie jesteś pewien, Parse do danych, które mają prawo rzucić wyjątek, jeśli są złe, i Convert tam, gdzie świadomie chcesz zachowania “zaokrąglij i nie wywalaj się na null” — co bywa zarówno zaletą, jak i ukrytą pułapką. Największy, najczęściej pomijany szczegół: zawsze podawaj CultureInfo.InvariantCulture przy parsowaniu liczb z danych, które nie pochodzą od użytkownika w UI — inaczej Twój kod “działa u mnie” a na serwerze z inną kulturą systemową albo wybucha, albo — gorzej — po cichu liczy źle: 19,99 zamienia się w 1999.
Chcesz iść dalej?
Konwersja między typami to dwie lekcje z kursu C# Podstawy Programowania — 14 godzin materiału od pierwszej aplikacji po obsługę wyjątków. Kurs jest jednym z 18 kursów w Programie .NET Developer: 12 poziomów od podstaw C# do AI w .NET i 7 projektów do portfolio. Nie jesteś jeszcze gotów? Zacznij od darmowej roadmapy Junior .NET Developera.
Zobacz
- Zmienne i typy danych w C# — typy, które tu konwertujesz
- Porównywanie stringów w C# — pułapka tureckiego I — ta sama kultura, tym razem przy tekście
- Formatowanie daty w ciągach —
CultureInfoprzy datach - Pułapki C# — quiz: zgadnij output — m.in. dlaczego pieniądze trzymamy w
decimal
🚀 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.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
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ę →