🔥 Darmowa Roadmapa .NET — kompletny plan nauki do pierwszej pracy. Pobierz i dołącz do Listy VIP →

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 → intlong → 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 0int.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.

Zgadnij, zanim klikniesz

02-Blad-Na-Serwerze.cs

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ł");

Pierwsza linia wypisze Laptop (pl-PL): 19,99 zł. A druga?

Niestety nie. Na kulturze Invariant przecinek nie jest separatorem dziesiętnym, tylko separatorem tysięcy.
Zgadłeś — 1999 zł. W Invariant przecinek to separator tysięcy, a parser nie sprawdza, czy stoi co trzy cyfry.
To byłby dobry błąd — ale go nie ma. FormatException dostajesz w odwrotnym kierunku: "19.99" na pl-PL. Tu parser przyjmuje przecinek jako separator tysięcy.
Konsola (uruchomione na .NET 10):Laptop (pl-PL): 19,99 zł Serwer (Invariant): 1999 zł
DaneMaszynaWynikRodzaj błędu
"19.99"pl-PLFormatExceptiongłośny — widać go w logach od razu
"19,99"Invariant / en-US1999cichy — 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, pomijam

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

Uruchom na innej maszynie

Przed poprawką i po poprawce

Wybierz maszynę. Wyniki są zmierzone na .NET 10 — nie wywnioskowane.

PRZED · 01-Start.cs

string priceText = "19,99"; decimal price = decimal.Parse(priceText); Console.WriteLine($"Cena z cennika: {price} zł");
Cena z cennika: 19,99 zł

PO · 03-Final.cs

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, pomijam

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: FormatException

Convert.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 InvariantCulturew 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

Mariusz Jurczenko
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.

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