C# gives you several ways to turn a value of one type into a value of another type: implicit conversion, an explicit cast, Convert, Parse, TryParse, as and pattern matching with is. They look interchangeable in a lot of code samples, but they fail in different ways, some silently corrupt data, some throw, some quietly hand back a default value instead of the value you actually wanted. This article walks through each one, breaks it on purpose to show exactly what it throws and then shows the version that handles the same situation safely.

Η C# σου δίνει αρκετούς τρόπους να μετατρέψεις μια τιμή ενός τύπου σε τιμή άλλου τύπου: implicit conversion, explicit cast, Convert, Parse, TryParse, as και pattern matching με is. Μοιάζουν εναλλάξιμα σε πολλά παραδείγματα κώδικα, αλλά αποτυγχάνουν με διαφορετικό τρόπο η καθεμία, κάποιες αλλοιώνουν σιωπηλά τα δεδομένα, κάποιες πετάνε exception, κάποιες απλώς επιστρέφουν μια προεπιλεγμένη τιμή αντί για αυτή που πραγματικά ήθελες. Αυτό το άρθρο περνάει από την καθεμία, τη σπάει επίτηδες για να δείξει ακριβώς τι πετάει και μετά δείχνει την έκδοση που χειρίζεται την ίδια κατάσταση με ασφάλεια.

Implicit vs explicit conversion

Implicit έναντι explicit conversion

An implicit conversion happens automatically because it can never lose information or fail: a smaller integer type always fits inside a bigger one, an int always fits inside a double. An explicit conversion, a cast, is required whenever the compiler cannot guarantee that, converting a double to an int can lose the fractional part, converting a base class reference to a derived class can fail outright if the object is not actually that derived type. Requiring the cast is the compiler forcing you to acknowledge the risk.

Μια implicit conversion γίνεται αυτόματα γιατί δεν μπορεί ποτέ να χάσει πληροφορία ή να αποτύχει: ένας μικρότερος integer τύπος χωράει πάντα μέσα σε έναν μεγαλύτερο, ένα int χωράει πάντα μέσα σε ένα double. Μια explicit conversion, ένα cast, απαιτείται όποτε ο compiler δεν μπορεί να το εγγυηθεί αυτό, η μετατροπή ενός double σε int μπορεί να χάσει το δεκαδικό μέρος, η μετατροπή μιας αναφοράς βασικής κλάσης σε παράγωγη κλάση μπορεί να αποτύχει εντελώς αν το αντικείμενο δεν είναι πραγματικά αυτός ο παράγωγος τύπος. Η απαίτηση του cast είναι ο compiler που σε αναγκάζει να αναγνωρίσεις τον κίνδυνο.

int wholeNumber = 42;
double asDouble = wholeNumber; // implicit, always safe

double pi = 3.9;
int truncated = (int)pi; // explicit, truncates to 3

truncated is 3, not 4. A numeric cast to an integer type truncates toward zero, it does not round. That alone catches people who expect (int)3.9 to behave like rounding.

Το truncated είναι 3, όχι 4. Ένα numeric cast σε integer τύπο κάνει truncate προς το μηδέν, δεν στρογγυλοποιεί. Αυτό μόνο του μπερδεύει όσους περιμένουν το (int)3.9 να συμπεριφέρεται σαν rounding.

Narrowing casts can silently lose data

Τα narrowing casts μπορούν να χάσουν δεδομένα σιωπηλά

A byte holds 0 to 255. Casting a bigger number into a byte does not throw by default, it wraps around using the low bits and keeps going, no warning, no exception, just a wrong number that looks perfectly valid.

Ένα byte κρατά από 0 έως 255. Το cast ενός μεγαλύτερου αριθμού σε byte δεν πετάει exception από προεπιλογή, κάνει wrap around χρησιμοποιώντας τα χαμηλά bits και συνεχίζει, χωρίς προειδοποίηση, χωρίς exception, απλώς έναν λάθος αριθμό που φαίνεται εντελώς έγκυρος.

int tooBig = 300;
byte wrapped = (byte)tooBig; // 44, no exception, silently wrong

When silent data loss is not acceptable, wrap the same cast in a checked block. checked tells the runtime to throw OverflowException instead of wrapping:

Όταν η σιωπηλή απώλεια δεδομένων δεν είναι αποδεκτή, τύλιξε το ίδιο cast σε ένα block checked. Το checked λέει στο runtime να πετάξει OverflowException αντί να κάνει wrap:

try
{
    byte guarded = checked((byte)tooBig);
}
catch (OverflowException ex)
{
    Console.WriteLine($"checked (byte)300 threw: {ex.GetType().Name}");
}

checked is the correct pattern whenever a narrowing numeric cast represents a real bug if it ever loses data, counters, identifiers, money amounts, rather than something you are comfortable truncating on purpose.

Το checked είναι το σωστό pattern όποτε ένα narrowing numeric cast αντιπροσωπεύει πραγματικό bug αν χάσει ποτέ δεδομένα, counters, identifiers, χρηματικά ποσά, αντί για κάτι που το κάνεις άνετα truncate επίτηδες.

Parse, TryParse and Convert do not agree on null

Τα Parse, TryParse και Convert δεν συμφωνούν στο null

int.Parse assumes the text is valid and throws when it is not, a FormatException for text that is not a number, an ArgumentNullException for a null string:

Το int.Parse υποθέτει ότι το κείμενο είναι έγκυρο και πετάει exception όταν δεν είναι, ένα FormatException για κείμενο που δεν είναι αριθμός, ένα ArgumentNullException για null string:

try
{
    int.Parse("abc");
}
catch (FormatException ex)
{
    Console.WriteLine($"int.Parse(\"abc\") threw: {ex.GetType().Name}");
}

string? missing = null;

try
{
    int.Parse(missing);
}
catch (ArgumentNullException ex)
{
    Console.WriteLine($"int.Parse(null) threw: {ex.GetType().Name}");
}

Convert.ToInt32 looks like it should behave the same way, but it treats a null input as a special case and quietly returns 0 instead of throwing:

Το Convert.ToInt32 φαίνεται σαν να πρέπει να συμπεριφέρεται με τον ίδιο τρόπο, αλλά αντιμετωπίζει το null input ως ειδική περίπτωση και ήσυχα επιστρέφει 0 αντί να πετάξει exception:

int converted = Convert.ToInt32(missing);
Console.WriteLine(converted); // 0, no exception

That mismatch is a real source of bugs: code that switches from Convert.ToInt32 to int.Parse, or the other way around, silently changes how missing input is handled. The safe default for anything that might not be a valid number, user input, a config value, a field from a file, is TryParse. It never throws, it returns a bool and hands back the parsed value through an out parameter:

Αυτή η ασυμφωνία είναι πραγματική πηγή bugs: κώδικας που αλλάζει από Convert.ToInt32 σε int.Parse, ή αντίστροφα, αλλάζει σιωπηλά το πώς αντιμετωπίζεται το ελλιπές input. Η ασφαλής προεπιλογή για οτιδήποτε μπορεί να μην είναι έγκυρος αριθμός, input χρήστη, μια τιμή ρύθμισης, ένα field από ένα αρχείο, είναι το TryParse. Ποτέ δεν πετάει exception, επιστρέφει ένα bool και δίνει την αναλυμένη τιμή μέσω μιας παραμέτρου out:

bool ok = int.TryParse("abc", out int safeResult);
Console.WriteLine($"ok={ok}, result={safeResult}"); // ok=False, result=0

Reference type conversion: cast vs as vs is

Μετατροπή reference type: cast έναντι as έναντι is

A direct cast on a reference type throws InvalidCastException the moment the object is not actually the target type:

Ένα direct cast σε reference type πετάει InvalidCastException τη στιγμή που το αντικείμενο δεν είναι πραγματικά ο τύπος προορισμού:

object boxedNumber = 42;

try
{
    string forced = (string)boxedNumber;
}
catch (InvalidCastException ex)
{
    Console.WriteLine($"(string)boxedNumber threw: {ex.GetType().Name}");
}

as attempts the same conversion but returns null instead of throwing when it fails and only works for reference types and nullable types:

Το as επιχειρεί την ίδια μετατροπή αλλά επιστρέφει null αντί να πετάξει exception όταν αποτυγχάνει και δουλεύει μόνο για reference types και nullable types:

string? safe = boxedNumber as string;
Console.WriteLine(safe ?? "null"); // null, no exception

Pattern matching with is is usually the cleanest of the three: it checks the type and gives you a correctly typed variable in one step, only entering the block when the conversion actually succeeds:

Το pattern matching με is είναι συνήθως το πιο καθαρό από τα τρία: ελέγχει τον τύπο και σου δίνει μια σωστά τυποποιημένη μεταβλητή σε ένα βήμα, μπαίνοντας στο block μόνο όταν η μετατροπή πραγματικά πετυχαίνει:

if (boxedNumber is int number)
{
    Console.WriteLine($"boxedNumber is int: {number}");
}

Reach for a direct cast only when you are certain of the type and want a loud failure if that certainty turns out to be wrong. Reach for as or is whenever the type is only a possibility, which is most of the time.

Χρησιμοποίησε direct cast μόνο όταν είσαι σίγουρος για τον τύπο και θέλεις μια δυνατή αποτυχία αν αυτή η σιγουριά αποδειχθεί λάθος. Χρησιμοποίησε as ή is όποτε ο τύπος είναι απλώς μια πιθανότητα, που είναι οι περισσότερες φορές.

Every broken example above next to its safe counterpart, runnable and editable:

Κάθε σπασμένο παράδειγμα από πάνω δίπλα στο ασφαλές αντίστοιχό του, εκτελέσιμο και επεξεργάσιμο:

Live example, editable, runs on .NET Fiddle
Ζωντανό παράδειγμα, επεξεργάσιμο, τρέχει στο .NET Fiddle

Takeaways

Συμπεράσματα

  • Implicit conversions are always safe. Explicit casts exist precisely because the compiler cannot guarantee that, treat every cast as a place where something can go wrong.
  • A numeric cast to an integer type truncates instead of rounding and a narrowing cast wraps around silently unless it is wrapped in checked.
  • Parse throws on bad or missing input. Convert.ToInt32 quietly turns null into 0 instead of throwing. Know which one a piece of code is actually calling.
  • TryParse never throws and is the right default for anything that might not be a valid number, user input above all.
  • Prefer as or pattern matching with is over a direct cast on reference types. A direct cast throws InvalidCastException on a mismatch, as and is degrade gracefully.
  • Boxing, unboxing and string-specific formatting and parsing details have their own sharp edges and get their own articles.
  • Οι implicit conversions είναι πάντα ασφαλείς. Τα explicit casts υπάρχουν ακριβώς επειδή ο compiler δεν μπορεί να το εγγυηθεί αυτό, αντιμετώπισε κάθε cast σαν σημείο όπου κάτι μπορεί να πάει στραβά.
  • Ένα numeric cast σε integer τύπο κάνει truncate αντί να στρογγυλοποιεί και ένα narrowing cast κάνει wrap around σιωπηλά εκτός αν είναι τυλιγμένο σε checked.
  • Το Parse πετάει exception σε λάθος ή ελλιπές input. Το Convert.ToInt32 ήσυχα μετατρέπει το null σε 0 αντί να πετάξει exception. Να ξέρεις ποιο από τα δύο καλεί πραγματικά ο κώδικας.
  • Το TryParse ποτέ δεν πετάει exception και είναι η σωστή προεπιλογή για οτιδήποτε μπορεί να μην είναι έγκυρος αριθμός, πάνω απ' όλα input χρήστη.
  • Προτίμησε το as ή το pattern matching με is αντί για direct cast σε reference types. Ένα direct cast πετάει InvalidCastException σε αναντιστοιχία, τα as και is υποχωρούν με χάρη.
  • Το boxing και το unboxing, καθώς και οι λεπτομέρειες formatting και parsing συγκεκριμένα για strings, έχουν τις δικές τους παγίδες και θα καλυφθούν σε ξεχωριστά άρθρα.