Back to Blog
C#

C# Object Casting and Boxing: How Type Conversion Works

c# object casting and boxing: Understand how C# object casting and boxing work, their runtime cost, and when to prefer generics over object conversions.

C#BoxingType CastingPerformanceGenerics
Diagram showing a value type being boxed into an object reference and later unboxed back to a value type.

When you assign a value type to a variable of type object, the runtime performs boxing. When you cast that object back to the original type, it performs unboxing. These operations are part of C# object casting and boxing, and they have specific behavior and costs that matter in performance-sensitive code.

The Difference Between Casting and Boxing

Casting and boxing are often mentioned together, but they address different concerns. Casting changes the compile-time type of a reference or value. Boxing converts a value type into a reference type by wrapping it in an object or an interface. Unboxing reverses that process.

Consider this example:

int number = 42; object boxed = number; // boxing int unboxed = (int)boxed; // unboxing

Here, number is a value type stored on the stack (or inline in a containing object). When you assign it to object, the CLR allocates a new object on the heap and copies the value into it. The variable boxed holds a reference to that heap object. When you cast back to int, the CLR checks that the boxed object actually contains an int and copies the value back to the stack.

Casting without boxing occurs when you convert between reference types in an inheritance hierarchy. For example, casting a string to object is a reference conversion that does not copy data. The distinction matters because boxing has a measurable runtime cost, while reference casts are typically cheap pointer adjustments.

Implicit and Explicit Casting in C#

C# provides two kinds of casts: implicit and explicit. Implicit casts happen automatically when the conversion is guaranteed to succeed and no data will be lost. Explicit casts use the cast operator (type) and signal that the developer accepts the risk of an InvalidCastException or data loss.

For numeric types, implicit conversions exist when the target type can represent every value of the source type. For example, int to long is implicit, but long to int requires an explicit cast because the value might overflow.

long big = 100_000_000; int smaller = (int)big; // explicit, may overflow

For object references, implicit casting occurs when you assign a derived type to a base type. Explicit casting is needed when you want to treat a base reference as a derived type. This is common when reading from a collection of object.

object value = "hello"; string text = (string)value; // explicit reference cast

If value does not actually reference a string, the cast throws InvalidCastException. The as operator provides a safer alternative that returns null instead of throwing:

string text = value as string; if (text != null) { // use text }

How Boxing and Unboxing Behave at Runtime

Boxing is not a simple cast. It involves three steps: allocating a heap object, copying the value into that object, and returning a reference. The boxed object is a full object with a type handle and a copy of the original value. Any later modification to the original variable does not affect the boxed copy.

int count = 5; object boxedCount = count; count = 10; Console.WriteLine(boxedCount); // prints 5

Unboxing requires an exact type match. You cannot unbox an int as a long directly. The following code throws an InvalidCastException:

object boxedInt = 42; long wrong = (long)boxedInt; // InvalidCastException

You must first unbox to the original type and then perform a numeric conversion:

long correct = (int)boxedInt; // unbox to int, then implicit conversion to long

This strictness prevents silent data corruption and is a key reason why boxing and unboxing require careful handling.

Common Casting Errors and How to Avoid Them

The most frequent error is assuming a boxed value can be unboxed to a different but compatible type. As shown above, unboxing requires the exact type. Another common mistake is casting a null reference without checking it. If value is null, (string)value returns null for reference types, but (int)value throws NullReferenceException because you cannot unbox null to a value type.

Use the is pattern or as operator to avoid exceptions:

if (value is int number) { Console.WriteLine(number); }

This pattern checks the type and performs the unboxing in one step. It is both safer and more readable than a manual cast followed by a null check.

When working with collections that store object, such as ArrayList or a List<object>, you must verify the type before unboxing. The is pattern is the recommended approach because it eliminates the risk of InvalidCastException and makes the code self-documenting.

Performance and Memory Cost of Boxing

Boxing has a real performance cost because it allocates a new object on the heap. Every boxed value type becomes a separate object, increasing memory pressure and triggering garbage collection more often. Unboxing itself is cheap, but it requires a type check and a copy operation.

Consider a loop that boxes an integer thousands of times:

List<object> items = new List<object>(); for (int i = 0; i < 10000; i++) { items.Add(i); // boxing each iteration }

Each Add call boxes i, creating a new heap object. The list stores references to these objects, and later unboxing each element adds further overhead. In contrast, a List<int> stores the values directly without any boxing.

The C# compiler sometimes introduces boxing implicitly. For example, calling ToString() on a value type does not box because the method is overridden. But concatenating a value type with a string using + boxes it if the compiler cannot use a specialized overload. Being aware of these implicit boxes helps you write more efficient code.

If you need to store a mix of types, consider using generics with an interface constraint instead of falling back to object. For example, List<IComparable> still boxes value types that implement IComparable, but you can design your own generic interface to avoid boxing when the operations are known.

When to Use Casting vs. Generics

Generics are the preferred way to avoid boxing when the type is known at compile time. A List<int> or Dictionary<string, int> does not box its elements. If you need a collection that can hold any type, you have two options: use List<object> and cast, or use a generic base type and derive specific implementations.

The choice depends on whether the type is truly unknown or just not specified. If you control the API, prefer generics. For example, a method that processes a single value can be generic:

public static T ProcessValue<T>(T value) { // no boxing for value types return value; }

When you must store heterogeneous data, such as in a serialization framework, boxing is unavoidable. In those cases, minimize the number of boxing operations by batching conversions and using pattern matching to unbox only once.

Casting is also necessary when you work with legacy APIs that accept object, such as ArrayList or reflection. You cannot eliminate boxing there, but you can isolate it behind a well-defined interface so the rest of your code stays type-safe.

Practical Example: Casting in a Collection of Objects

Suppose you receive a list of object from a data layer. Each element is either an int, a double, or a string. You need to sum the numeric values. A naive implementation might cast repeatedly and throw exceptions:

object[] values = { 1, 2.5, "skip", 4 }; double sum = 0; foreach (object item in values) { if (item is int intValue) { sum += intValue; } else if (item is double doubleValue) { sum += doubleValue; } }

The is pattern performs the type check and unboxing in one step. This avoids the overhead of a separate cast and null check. The code is also easier to extend if new numeric types appear.

If you control the data source, consider using a generic wrapper instead. For example, a List<object> could be replaced by a custom List<NumericValue> where NumericValue is a struct that implements an interface. This eliminates boxing entirely and gives you compile-time safety.

Boxing and casting are not inherently bad. They are tools that solve specific problems. The key is to recognize when they are necessary and when a generic alternative would be cleaner and faster. In performance-critical paths, measure the impact of boxing and refactor to generics when the type is known. In flexible frameworks, boxing is a reasonable tradeoff for the ability to handle arbitrary data.

c# object casting and boxing: Practical Usage and Code Examp | RYUSLOG DEV