AlgoMaster Logo

Type Casting & Conversion

Last Updated: May 17, 2026

10 min read

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.

Why Conversion Matters

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 Conversion (Widening)

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.

Explicit Conversion (Casting)

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.

The Convert Class

Casts 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 TryParse

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

Inputint.Parse(input)int.TryParse(input, out var n)Convert.ToInt32(input)
"42"returns 42returns true, n = 42returns 42
"forty-two"throws FormatExceptionreturns false, n = 0throws FormatException
nullthrows ArgumentNullExceptionreturns false, n = 0returns 0
"99999999999999"throws OverflowExceptionreturns false, n = 0throws OverflowException
"" (empty)throws FormatExceptionreturns false, n = 0throws 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.

Reference Conversions

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

Boxing and Unboxing

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.

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.

User-Defined Conversions

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.

Summary

  • C# does implicit conversions only when no data can be lost: smaller numeric types widen to larger ones, and derived classes upcast to base classes.
  • Narrowing conversions need an explicit cast in parentheses. Casts truncate fractional parts and can silently overflow integer types. Wrap them in 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.
  • Use 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.
  • Downcasts between reference types are explicit. Use as to get null on failure, or the modern if (obj is Premium premium) pattern to test and cast in one step.
  • Boxing wraps a value type in a heap object so it can be referenced through 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.
  • User-defined implicit and explicit operators let your own types participate in conversions. Make them implicit only when the conversion is safe and obvious.