Back to Blog
C#

C# Boxing Examples: How Value Types Behave

c# boxing examples: Understand C# boxing with practical examples: what happens on the heap, why unboxing requires exact type casts, and how to avoid boxing with generics.

boxingunboxingvalue typesgenericsperformancetype system
Diagram showing a value type box being wrapped into a heap object with a reference pointer.

The moment you assign an int, struct, or enum to a variable of type object, System.ValueType, or an interface, the compiler inserts a boxing operation. Boxing wraps the value type in a reference-type object on the managed heap, and it has costs and behavioral surprises that show up in surprising places. This article walks through c# boxing examples, explains the runtime behavior, and shows how to avoid boxing when it matters.

What Boxing Actually Does at Runtime

Consider this minimal example:

int number = 42; object boxed = number; // boxing

The assignment to object triggers a boxing conversion. The CLR allocates a new object on the heap, copies the value (42) into that object's fields, and returns a reference. The original number stays on the stack (or in whichever context it already existed) and is unrelated to the heap copy.

Boxing also occurs implicitly when you pass a value type to a parameter typed as object, ValueType, or an interface. For example:

void PrintValue(object value) => Console.WriteLine(value); int x = 7; PrintValue(x); // boxes x

The same applies to returning a value type from a method whose return type is object. Every such conversion copies the value, it does not move the original.

C# Boxing Examples with Common Conversions

The simplest case is casting to object, but boxing also happens when you assign to an interface. Suppose you have a struct implementing an interface:

interface IShape { double Area(); } struct Circle : IShape { public double Radius { get; set; } public double Area() => Math.PI * Radius * Radius; } Circle circle = new Circle { Radius = 2.5 }; IShape shape = circle; // boxes the circle

Even though Circle is a value type, the interface reference requires a heap object, so circle is copied. Any method called through the interface operates on the boxed copy, not the original struct instance. If the method modifies state (which requires a mutable struct), that modification is lost unless you reassign the interface variable.

Another everyday occurrence is string concatenation. The string.Concat overloads accept object arguments, so passing value types causes boxing:

int id = 1001; string label = "ID: " + id; // id is boxed

The + operator translates to a call that takes object, so boxing happens before concatenation. For a single value this is negligible, but in a loop with thousands of iterations, the allocations add up.

Unboxing and the Exact Type Cast Rule

To get the value back, you must unbox. Unboxing extracts the value from the heap object and copies it into a value type variable. The cast must exactly match the boxed type, not a base type. For example:

int original = 12; object boxed = original; int unboxed = (int)boxed; // correct long wrong = (long)boxed; // InvalidCastException

Even though int can be implicitly widened to long, unboxing does not perform numeric conversion. The boxed object stores the exact runtime type, and the cast must be to that type. If you need a different numeric type, unbox first and then convert:

long converted = (int)boxed;

This rule also applies to nullable types. Boxing a Nullable<T> that has a value boxes the underlying T. If the nullable has no value, the boxed result is null. Unboxing into the original nullable type works, but unboxing into the underlying type throws if the box is null.

Boxing in Collections and ArrayList

The older non-generic ArrayList stores object, so every value type added is boxed:

ArrayList list = new ArrayList { 1, // boxing "two", // string is already a reference type, no boxing 3.5 // boxing };

Reading them back requires unboxing with the exact type. The non-generic collections predate generics and are rarely the right choice today. The generic List<T> removes boxing entirely for value types, because the internal storage is typed as T, not as object.

Performance and Memory Costs Beyond the Allocation

Boxing has three measurable effects: heap allocation for each boxed value, an extra memory copy of the value, and an additional indirection when accessing the boxed object. Frequent boxing in a tight loop produces GC pressure because the boxes become garbage quickly.

A simple demonstration of the difference:

void SumBoxed(int count) { object sum = 0; for (int i = 0; i < count; i++) { sum = (int)sum + i; // boxing and unboxing per iteration } } void SumGeneric(int count) { int sum = 0; for (int i = 0; i < count; i++) { sum += i; // no boxing involved } }

In the boxed version, each addition first unboxes the current sum, adds the integer, then boxes the new value. That is two heap allocations per iteration and two copies. The generic version uses a simple local integer. Production code rarely needs a synthetic benchmark to decide which is better; the boxed pattern is clearly worse for any loop that executes enough times to matter.

Even a single boxing operation can be a concern in hot paths, such as logging an integer in a high-throughput service. The cost is not the allocation itself but the combination of allocation, copying, and eventual GC. If the value is logged once per request, the impact is small; if it happens thousands of times per second, it becomes relevant.

Where Boxing Appears Without Explicit Casting

Several C# constructs cause implicit boxing even though no cast appears in your code:

  • Passing a value type to a parameter typed object or ValueType
  • Returning a value type as object from a method
  • Storing a value type in an interface variable
  • Calling a method on a value type via an interface (like GetType() on an int)
  • Pattern matching where the pattern binds an object
  • Using value types with dynamic, which boxes as part of runtime binding

A common hidden case is calling object.GetType() on a value type:

int n = 5; Type t = n.GetType(); // boxing occurs

GetType() is a virtual method on object, so the call boxes the int first. The same happens when you call ToString() on a struct that overrides it? Actually, if the struct overrides ToString(), the call can be dispatched without boxing because the override is part of the struct's own type. The boxing happens only if you call a non-overridden virtual method that requires the object reference.

Avoiding Boxing with Generics and Careful APIs

Generic classes and methods are the primary tool for eliminating boxing. A generic List<T> never boxes its elements because it stores them in a T[] array. The same holds for Dictionary<TKey, TValue> and other generic collections.

Generic methods also avoid boxing:

void Compare<T>(T a, T b) where T : IComparable<T> { int result = a.CompareTo(b); // no boxing for value types }

When you constrain T to an interface that the value type implements, calls are dispatched directly on the value type without boxing. This is a key advantage over passing IComparable as a parameter:

void CompareInterface(IComparable<int> a, IComparable<int> b) { int result = a.CompareTo((int)b); // boxing when passed a struct }

In the second version, passing an int to the parameter boxes it because the interface reference requires a heap object.

Another way to avoid boxing is to avoid object parameters in your own APIs. If you have a method that only needs integers, type it as int rather than object. If you need to accept any numeric value, consider generic overloads or the INumber<T> interface introduced with .NET 7, which allows generic math without boxing.

When Boxing Is Acceptable

Not every boxing occurrence deserves heavy refactoring. For one-off calls or code that runs rarely, such as configuring an object during startup, boxing is irrelevant. The same goes for passing an integer to string.Format once per user action. The cost is a tiny allocation, and the GC handles it easily.

The problems arise in tight loops, high frequency event handlers, or data processing pipelines. If profiling shows a large number of heap allocations and high GC pressure, look for boxing sites. You can spot them by searching for casts to object and interface assignments of struct types, or by examining IL instructions like box.

When you must accept an object for API compatibility, consider a generic overload that the C# compiler prefers when the argument type is a value type. For example, List<T> avoids the need for ArrayList in modern code. Similarly, generic extension methods can process value types without boxing.

Common Pitfalls in Real-World Code

A frequent bug is assuming that modifying a boxed struct through an interface affects the original variable. Consider:

interface IIncrement { void Increment(); } struct Counter : IIncrement { public int Value { get; private set; } public void Increment() => Value++; } Counter counter = new Counter(); IIncrement boxed = counter; boxed.Increment(); Console.WriteLine(counter.Value); // still 0

The Increment call modifies the boxed copy, not the original counter. Because IIncrement is a reference type, the struct is copied onto the heap, and the mutation stays there. This is a classic reason to avoid mutable structs and to be careful when passing structs as interfaces.

Another pitfall is mixing unboxing with type conversion. A boxed int cannot be cast directly to double, even though an implicit conversion exists. You must unbox first, then convert. The same holds for boxed long to short, or boxed float to decimal. Always unbox to the exact original type first.

How Generics Interact with Boxing in Collections

Generic collections such as List<T> are more than just a syntax convenience. They change the fundamental storage model. ArrayList boxes every value type; List<int> stores raw integers in a contiguous array. That means generic collections have predictable memory layout and avoid per-element allocation. This is why List<T> is the default choice for storing value types.

However, generics do not automatically eliminate boxing if you misuse them. For example, placing a value type into a List<object> still boxes, because the type argument is object. Similarly, a generic method without a constraint cannot call interface methods directly, so you might need to box to call an interface method. The constraint system exists to let you avoid this:

T Max<T>(T a, T b) where T : IComparable<T> { return a.CompareTo(b) >= 0 ? a : b; }

If you omit the constraint, you cannot call CompareTo without boxing to IComparable. The constraint gives the compiler enough information to dispatch the call directly on the value type.

Analyzing IL for Boxing Calls

To confirm a boxing operation, you can view the IL of a compiled method. The box instruction appears explicitly. For example, the method void PrintObject(object o) shows box if you pass an integer to it. Running ildasm or dotnet tooling will reveal these calls. In many cases, the simple rule is enough: any conversion from a value type to a reference type (except string, which is a reference type) causes boxing.

Here is a practical example that mixes several potential boxing sites:

void ProcessValues(object first, object second) { if (first is int f && second is int s) { Console.WriteLine($"{f + s}"); } }

The is pattern matching does not cause boxing when the input is already object. But if the caller passes integers, they are boxed at the call site. The pattern match itself works on the existing boxed object without additional boxing. The string interpolation again boxes the sum because interpolation uses IConsoleBuilder internally? Actually, string interpolation calls ToString() on the value, and if the value type overrides ToString(), no boxing occurs. For a simple int, ToString() is overridden, so interpolation does not box an integer. However, formatting with a format string might call an interface method that causes boxing. The exact behavior depends on implementation details.

Putting It Together: A Pattern to Reduce Boxing

If you are designing a method that might be called with value types and you want to avoid boxing, prefer generics over object parameters. Here is a comparison:

void StoreBoxed(object value) { ... } void StoreGeneric<T>(T value) { ... }

When calling StoreBoxed(1), boxing occurs. When calling StoreGeneric(1), the compiler infers T as int and no boxing happens inside the method. The method body might still box if it internally casts to object or calls an interface method without a constraint, but the entry itself is safe.

This pattern extends to interfaces. A generic constraint can keep the value on the stack:

void PrintArea<T>(T shape) where T : IShape { Console.WriteLine(shape.Area()); }

Calling PrintArea(circle) with Circle as the type argument does not box. The compiler generates a constrained call that avoids heap allocation. This is a significant advantage when the method is called frequently or in a loop.

Realistic C# Boxing Examples to Review

The following code shows three scenarios side by side:

// Scenario 1: explicit object assignment int a = 10; object o1 = a; // boxing // Scenario 2: interface assignment IShape s = new Circle { Radius = 1 }; // boxing // Scenario 3: generic call, no boxing PrintDefault(new Circle()); static T GetDefault<T>() where T : new() => new T();

In the first scenario, the value 10 is copied to the heap. In the second, a Circle value is boxed. In the third, the generic method receives type T, and since Circle is a struct, the compiler uses the value directly. The new() constraint does not introduce boxing; it just allows object creation.

Another realistic example involves nullable values:

int? maybe = 5; object box = maybe; // boxes the 5, not the Nullable<int> int? unboxed = (int?)box; // works

The box contains the underlying int, not the nullable wrapper. This is why unboxing to int? succeeds, while unboxing to int on a null box fails.

Choosing When to Refactor Away from Boxing

Deciding whether to remove boxing should follow profiling, not speculation. If a method is called a few times per request, a few boxing allocations are negligible. If it runs in a loop over thousands of items, the repeated allocations become measurable. The right tool is a memory profiler or the allocation-tracking features in modern .NET diagnostics. Look for a large number of box IL instructions and a corresponding number of small heap objects.

When you do refactor, prefer changing parameter types from object to generic or specific types. That change is often straightforward and does not alter call-site behavior for reference types. For value types, it eliminates both the allocation and the later unboxing. The main tradeoff is that generic code cannot call methods unless constrained, so you may need to add constraints that match the operations the method requires.

A final consideration is API design. If your library exposes an object-typed parameter for extensibility, consider adding a generic overload. This preserves flexibility for callers who need to pass reference types, while giving value type callers a path that avoids boxing. The generic overload can delegate to the non-generic one if necessary, but the value type caller will not need to use it frequently.

c# boxing examples: Practical Usage and Code Examples | RYUSLOG DEV