Konwersje typów w C# — casting, Parse, TryParse i Convert bez pomyłek
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: 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.
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” i wybucha na serwerze z inną kulturą systemową.
🚀 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ę →