Last Updated: May 17, 2026
C# is strongly typed, so the compiler refuses most "just stuff this value into that variable" moves unless it can prove the conversion is safe. Some conversions are automatic, others need a cast, and a few require a helper method that decides what to do with bad input or null. This lesson covers all of them, from implicit widening to boxing, so you can pick the right tool for each situation.
A shopping cart program juggles a lot of types. The quantity of an item is an int. The price is a decimal. The total weight might be a double. The user types everything as a string into the console. To move values between these types, you need conversions, and C# is strict about which ones happen on their own and which ones you have to ask for.
The reason for the strictness is bugs. Languages that quietly convert anything to anything tend to round prices, drop precision, or flip negative numbers in surprising ways. C# makes you write the cast when data could be lost, so the conversion is visible in the code review.
There are two big buckets to keep in your head. Implicit conversions happen with no syntax because the compiler can prove no data is lost. Explicit conversions need a cast or a method call because they might lose data, overflow, or fail at runtime. Everything else in this lesson is a variation on one of those two ideas.
Implicit conversions are the ones C# does for you, no syntax required. They only happen when the destination type can hold every possible value of the source type. Going from a smaller integer to a larger one, or from an integer to a floating-point type, fits that rule:
No cast, no helper method. int fits inside long (a larger integer) and inside double (a floating-point type with a wider range), so the compiler does the work. This is called widening because the destination type is wider than the source.
Here is the chain of numeric widening conversions C# allows. The arrow direction is the direction conversion can happen implicitly:
A small note on the diagram: decimal isn't in the chain because it isn't part of the implicit numeric ladder. You can implicitly convert int and long to decimal, but you can't implicitly convert double or float to decimal or vice versa. The compiler treats decimal as a separate kind of number for money and forces you to be explicit about precision trade-offs.
Implicit conversions also happen for reference types when the target is a base class or implemented interface, but we'll get to that in the reference conversions section below.
When the destination type can't hold every possible value of the source type, the compiler refuses to convert on its own. You have to write a cast in parentheses to say "yes, I know this might lose data, do it anyway."
The cast (int)rawDiscount chops off the fractional part. It doesn't round, it truncates toward zero. So 9.99 becomes 9, and -9.99 would become -9. This is called narrowing because the destination type is narrower than the source.
Casts between integer types behave the same way. They truncate the high-order bits if the value doesn't fit:
A byte holds values from 0 to 255. The value 300 overflows by 44, and the cast silently keeps only the low 8 bits, leaving 44. That's the kind of silent corruption casts can cause, which is why C# makes you write the cast explicitly.
If you'd rather have an exception than silent corruption, wrap the cast in a checked block:
The checked keyword turns silent overflow into an OverflowException at runtime. The opposite is unchecked, which forces the silent wrap-around behavior. In modern C# the default for casts is unchecked, so you only reach for checked when overflow would be a real bug you want to catch. You can also flip the project default with the <CheckForOverflowUnderflow> setting in .csproj.
Cost: Casts between primitive types are cheap, usually a single CPU instruction. The overhead in checked is one extra branch per cast. The real cost is correctness, not performance: a silent truncation can poison your data and you'll find out months later.
Convert ClassCasts work for numbers and references, but they fall apart the moment you have a string or a null. The Convert class in System fills that gap. It has a To... method for every primitive type, and the methods know how to handle strings, nulls, and conversions between unrelated types.
Two things to notice. First, Convert.ToDecimal("19.99") parses the string into a decimal. A plain cast can't do that, because a string and a decimal have nothing in common in memory. Second, Convert.ToInt32(2.75) returns 3, not 2. Unlike a cast, Convert.ToInt32 rounds. And it uses banker's rounding (round half to even), so 2.5 becomes 2 and 3.5 becomes 4. That surprises people the first time they hit it.
Convert also has opinions about null. Casting a null string to an integer would throw, but Convert.ToInt32(null) returns 0:
Whether you want that behavior depends on the context. For an order quantity, treating null as 0 is usually fine. For a customer's age, it might hide a bug. Be intentional about it.
Convert throws when the string isn't parseable at all:
That line throws System.FormatException: The input string 'five' was not in a correct format. The same call with "99999999999999" would throw OverflowException because the value doesn't fit in an int.
Parse and TryParseThe other way to turn a string into a number is the type's own Parse and TryParse methods. int.Parse, decimal.Parse, double.Parse, and so on are the most common. They live on the type itself, not in a helper class.
int.Parse is the simple one. It takes a string and returns the parsed value, or throws an exception if the input is bad:
If input were "forty-two", that line would throw FormatException. If it were null, it would throw ArgumentNullException. If it were "99999999999999", it would throw OverflowException. Useful when you're certain the string is valid, awful when it isn't.
int.TryParse is the version you should reach for whenever the input came from a user, a file, a network request, or any source you don't control. It never throws. Instead it returns true on success and false on failure, and writes the parsed value into an out parameter:
Sample run with valid input:
Sample run with invalid input:
The pattern out int qty declares the variable and binds the result in one step. When TryParse returns false, qty is set to 0. Most code paths only read qty inside the if (...) block, where it's known to be valid.
Here's how the three options stack up:
| Input | int.Parse(input) | int.TryParse(input, out var n) | Convert.ToInt32(input) |
|---|---|---|---|
"42" | returns 42 | returns true, n = 42 | returns 42 |
"forty-two" | throws FormatException | returns false, n = 0 | throws FormatException |
null | throws ArgumentNullException | returns false, n = 0 | returns 0 |
"99999999999999" | throws OverflowException | returns false, n = 0 | throws OverflowException |
"" (empty) | throws FormatException | returns false, n = 0 | throws FormatException |
The rule of thumb: use TryParse for any input you don't trust, Parse for hardcoded or already-validated strings, and Convert when you specifically want its null-to-zero behavior or you're converting between types other than string-to-number.
So far every conversion has been between value types. Reference types have their own rules. The compiler lets you implicitly convert from a derived type to a base type, because every PremiumCustomer is also a Customer. Going the other direction needs a cast, because not every Customer is a PremiumCustomer.
The first conversion (PremiumCustomer to Customer) is implicit because every PremiumCustomer is a Customer. The second one (Customer back to PremiumCustomer) needs a cast because the compiler can't be sure the actual object on the heap is a PremiumCustomer. In this code it is, so the cast succeeds. If it weren't, the cast would throw InvalidCastException at runtime.
The as operator gives you a softer landing. Instead of throwing, it returns null when the conversion fails:
as only works with reference types and nullable value types. The return is the target type if the conversion succeeds, or null if it doesn't. You almost always pair as with a null check.
The is operator is the other safe option. It tests whether the conversion would succeed and returns a bool:
The form someone is PremiumCustomer premium is a pattern: it tests the type and, on success, introduces a new variable premium already cast to the right type. This is the modern idiom for safe downcasting. For now, treat it as "the shortest way to write a safe downcast."
Cost: Downcasts and as/is checks both do a runtime type lookup. It's not free, but it's not slow either. The real concern is design: if your code is constantly downcasting, the base class is usually missing a method that would make the downcast unnecessary.
Value types and reference types live in different parts of memory. Value types like int, double, and struct live on the stack. Reference types live on the heap, and variables of those types hold a pointer to the heap. When you assign a value type to a variable of type object (or any interface the value type implements), C# has to put the value somewhere on the heap so the reference can point to it. That's called boxing:
The assignment object boxedQty = quantity allocates a small object on the heap, copies the value 5 into it, and stores the heap reference in boxedQty. The original quantity is untouched. Changing quantity afterward does not change boxedQty, because boxing copied the value:
Going the other direction is unboxing: extracting the value back out of the boxed object into a value-type variable. Unboxing requires an explicit cast, and the cast must be to the exact original type:
If the cast type doesn't match the boxed type exactly, the unbox throws InvalidCastException. So (long)boxedQty would fail even though int can be widened to long, because the value on the heap was boxed as an int. You'd have to do (int)boxedQty first and then let the implicit widening happen.
Cost: Boxing allocates on the heap and adds garbage collection pressure. One boxed int is nothing, but boxing inside a tight loop (for example, building an ArrayList of integers, or passing value types into APIs typed as object) can become the slowest part of your program. The fix is usually a generic collection like List<int>, which holds value types without boxing.
The most common way to accidentally box something is to pass a value type into a method or collection that takes object. The old ArrayList (System.Collections.ArrayList) is the classic offender, which is part of why generics replaced it. Modern code that uses List<T>, Dictionary<TKey, TValue>, and similar types avoids boxing because the type parameter T is known at compile time.
Classes and structs you define can declare their own conversions. You'd reach for this when you have a domain type that wraps a primitive and you want assignment to feel natural. A common example is a Money struct that wraps a decimal:
The two operator declarations are the new bit. implicit operator decimal(Money money) says "any Money can be converted to a decimal with no syntax." explicit operator Money(decimal amount) says "converting a decimal to a Money is possible but requires a cast, because we're guessing the currency."
The rule of thumb is: declare implicit only when the conversion never loses information and never surprises the reader. Otherwise declare explicit so the cast appears in the code. We use these sparingly in this course, but you'll see them in libraries like System.DateTimeOffset (implicit from DateTime) and in domain code where a wrapper type benefits from feeling like the underlying value.
checked(...) to get an OverflowException instead.Convert.ToInt32 and friends handle strings and null more gently than a cast. Convert.ToInt32(null) returns 0, and rounding from a double uses banker's rounding.int.TryParse, decimal.TryParse, and so on for any input you don't control. They return bool instead of throwing, and bind the parsed value to an out parameter on success.as to get null on failure, or the modern if (obj is Premium premium) pattern to test and cast in one step.object. Unboxing extracts it back with an explicit cast that must match the original type exactly. Both copy the value and have a small runtime cost worth avoiding in hot paths.implicit and explicit operators let your own types participate in conversions. Make them implicit only when the conversion is safe and obvious.