Skip to content

Boxing and Unboxing in C#: Interview Questions and Answers

Boxing and unboxing are asked about in almost every C# interview, often as a "what is the output?" question. They look simple, but interviewers use them to check whether you really understand value types, reference types, memory and performance. This article explains the concept with small examples (every output below comes from a real run on .NET 8), then covers the questions interviewers actually ask.

  • Boxing: converting a value type (int, double, struct...) into a reference type (object or an interface). It is implicit.
  • Unboxing: taking the value back out of the boxed object into a value type. It is explicit (needs a cast).

First, value types and reference types

Value typesReference types
Examplesint, double, bool, char, decimal, DateTime, enum, structclass, string, object, arrays, interfaces, delegates
The variable holdsThe value itselfA reference (address) to an object on the heap
Copying the variableCopies the valueCopies the reference; both point to the same object
MemoryStored inline: on the stack for local variables, or inside the object/array that contains itThe object always lives on the managed heap and is cleaned up by the garbage collector

Every type in C# derives from System.Object, including int. So an int can be stored in an object variable. But an object variable holds a reference to the heap, and an int is a plain value. To make it fit, the runtime puts the value in a "box" on the heap. That is boxing.

Think of it like posting a phone (the value) by courier. You can't hand the courier a loose phone; it must first be packed in a box with a label saying what's inside (boxing). At the other end, someone opens the box and checks the label before taking the phone out (unboxing). Packing and unpacking take time and material, which is the performance cost.

Boxing

int age = 25;          // value type: the variable holds 25 directly
object box = age;      // BOXING: implicit, no cast needed

Console.WriteLine(box);              // 25
Console.WriteLine(box.GetType());    // System.Int32

What happens in memory when object box = age; runs:

  1. A new object is allocated on the heap.
  2. The value 25 is copied into it, together with type information (so the box knows it contains an Int32).
  3. The variable box receives a reference to that new object.
Stack                          Heap
-----                          ----
age  [ 25 ]
box  [ ref ] ───────────────►  [ object header | type: Int32 | 25 ]   (24 bytes on 64-bit)

A boxed int takes about 24 bytes on a 64-bit process (object header + type pointer + the 4-byte value, rounded up), compared to 4 bytes for the int itself.

Unboxing

object box = 25;           // boxing
int value = (int)box;      // UNBOXING: explicit cast required

Console.WriteLine(value + 1);   // 26

When unboxing, the runtime:

  1. checks that box is not null (otherwise NullReferenceException),
  2. checks that the box contains exactly the requested type (otherwise InvalidCastException),
  3. copies the value from the heap back into the value-type variable.

Unboxing must use the exact type

This is the most famous trap:

object boxed = 10;                 // an Int32 is boxed

long big = (long)boxed;            // InvalidCastException!
Output
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'.

Even though an int fits easily into a long, unboxing is not a numeric conversion. It only opens a box of the same type. Unbox first, then convert:

long big = (long)(int)boxed;       // unbox to int, then convert int to long
Console.WriteLine(big);            // 10

There are two allowed exceptions to "exact type": you can unbox into the nullable version ((int?)boxed), and a boxed enum can be unboxed to its underlying type:

object day = DayOfWeek.Friday;
Console.WriteLine((int)day);       // 5

The box is a copy

Boxing copies the value. After that, the original variable and the box are completely independent:

int i = 10;
object o = i;      // the box gets a copy of 10
i = 20;            // changes only i

Console.WriteLine($"i={i}, o={o}");
Output
i=20, o=10

Why boxing matters: performance

One box is cheap. A million boxes are not. Each box is a heap allocation that the garbage collector must clean up later. This program adds 1,000,000 integers to an old non-generic ArrayList (which stores object) and to a generic List<int>, and measures the memory allocated while adding:

using System.Collections;

const int N = 1_000_000;
var arrayList = new ArrayList(N);       // stores object  -> every int is boxed
var list = new List<int>(N);            // stores int     -> no boxing

long before = GC.GetAllocatedBytesForCurrentThread();
for (int k = 0; k < N; k++) arrayList.Add(k);
long arrayListBytes = GC.GetAllocatedBytesForCurrentThread() - before;

before = GC.GetAllocatedBytesForCurrentThread();
for (int k = 0; k < N; k++) list.Add(k);
long listBytes = GC.GetAllocatedBytesForCurrentThread() - before;

Console.WriteLine($"ArrayList.Add x{N:N0}: {arrayListBytes:N0} bytes allocated");
Console.WriteLine($"List<int>.Add x{N:N0}: {listBytes:N0} bytes allocated");
Output (.NET 8, 64-bit, Release build)
ArrayList.Add x1,000,000: 24,000,016 bytes allocated
List<int>.Add x1,000,000: 0 bytes allocated

About 24 MB of garbage (24 bytes per boxed int) for the ArrayList, and zero for List<int>, because the list stores the int values directly in its internal int[] array. Both lists were given their full capacity up front, so only the boxing is measured. Reading the values back from the ArrayList also needs an unboxing cast for every item. This is the main reason generics were added in C# 2.0.

Hidden boxing: where it happens without you noticing

Writing object o = 5 is obvious. In real code, boxing usually happens silently:

CodeWhy it boxesBetter
arrayList.Add(5), hashtable[1] = 2Non-generic collections store objectList<int>, Dictionary<int, int>
string.Format("{0}", count), Console.WriteLine("{0}", count)The arguments are object parametersString interpolation $"{count}" (C# 10+ avoids boxing)
Passing a value to a method with an object or params object[] parameterThe value must become an objectGeneric method void Log<T>(T value) or specific overloads
IComparable c = 5; or passing a struct as an interfaceConverting a value type to an interface boxes itGeneric constraint: void Sort<T>(T x) where T : IComparable<T>
myStruct.Equals(otherStruct) on a struct without its own Equals(MyStruct)The default Equals(object) boxes the argument (and uses reflection)Implement IEquatable<T> on your structs
Using a custom struct as a Dictionary key without IEquatable<T>Every lookup calls Equals(object)Implement IEquatable<T> and override GetHashCode()
object-typed properties, DataRow["Price"], ADO.NET reader[0]Values are returned as objectreader.GetDecimal(0), row.Field<decimal>("Price")

The string formatting row is easy to measure. In the same test program, string.Format("Age {0}", i) allocated 64 bytes, while $"Age {i}" allocated 40 bytes. The 24-byte difference is exactly one boxed int.

How to avoid unnecessary boxing

  • Use generic collections (List<T>, Dictionary<TKey, TValue>) instead of ArrayList and Hashtable.
  • Write generic methods (T) instead of methods that take object.
  • Use generic interfaces such as IComparable<T> and IEquatable<T>, and implement IEquatable<T> on your own structs.
  • Prefer string interpolation over string.Format with value-type arguments.
  • Use the typed getters of data readers (GetInt32, GetDecimal).
  • Don't worry about the occasional box in normal code. Optimise boxing in hot paths: loops over large data, frequently called methods, high-traffic APIs. A profiler or BenchmarkDotNet will show you where it matters.

Interview questions and answers

1. What is boxing and unboxing in C#?

Boxing converts a value type into object (or an interface it implements) by allocating an object on the heap and copying the value into it. Unboxing extracts the value from that boxed object back into a value-type variable, after checking that the object contains exactly that type.

2. Which one is implicit and which one is explicit?

Boxing is implicit (object o = 5;). Unboxing is explicit and needs a cast (int i = (int)o;), because it can fail at runtime.

3. Why is boxing bad for performance?

Each boxing operation allocates a new object on the heap (about 24 bytes for an int on 64-bit) and copies the value. More allocations mean more garbage-collection work. Unboxing adds a type check and another copy. In a loop over a million items, this becomes megabytes of garbage and measurably slower code.

4. What is the output?

int i = 10;
object o = i;
i = 20;
Console.WriteLine(o);

10. Boxing copied the value; changing i later does not affect the box.

5. What happens here?

object o = 10;
long l = (long)o;

It compiles, but throws InvalidCastException at runtime: "Unable to cast object of type 'System.Int32' to type 'System.Int64'." A boxed value can only be unboxed to its exact type (or the nullable version of it). Fix: long l = (long)(int)o;

6. What is the output?

object a = 5;
object b = 5;
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));
Output
False
True

Each assignment creates a separate box. == on two object variables compares references, and these are two different objects, so False. Equals is virtual and is overridden by Int32 to compare the values, so True.

7. What is the output? (struct and interface)

interface ICounter
{
    void Increment();
    int Value { get; }
}

struct Counter : ICounter
{
    public int Value { get; private set; }
    public void Increment() => Value++;
}

Counter c = new Counter();
ICounter ic = c;           // boxing: ic points to a COPY of c
ic.Increment();
ic.Increment();

Console.WriteLine($"c.Value={c.Value}, ic.Value={ic.Value}");
Output
c.Value=0, ic.Value=2

Assigning a struct to an interface variable boxes it. ic refers to a copy on the heap, so the increments change the boxed copy, not the original c. This is a common source of bugs with mutable structs, and a good reason to make structs immutable.

8. Can boxing happen without assigning to object?

Yes. Converting a value type to any interface it implements boxes it. Passing a value type to a parameter of type object or params object[] (for example string.Format), adding it to non-generic collections like ArrayList, and calling the default Equals(object) on a struct all box too.

9. Does List<int> cause boxing?

No. Generic collections are specialised for value types: List<int> stores the values directly in an internal int[]. Adding 1,000,000 ints to a pre-sized List<int> allocated 0 bytes in our test, versus about 24 MB for an ArrayList.

10. What happens when you box a nullable type?

int? empty = null;
object o1 = empty;
Console.WriteLine(o1 == null);         // True: a null nullable becomes a null reference

int? five = 5;
object o2 = five;
Console.WriteLine(o2.GetType());       // System.Int32 (not Nullable<Int32>)

int? back = (int?)o2;
Console.WriteLine(back);               // 5

Nullable types get special treatment. A null nullable boxes to a plain null reference (no box is created). A nullable with a value boxes as the underlying type (a boxed Int32). A boxed int can be unboxed to either int or int?.

11. What happens if you unbox null?

Unboxing null to a non-nullable value type, for example int x = (int)(object)null;, throws NullReferenceException. Unboxing null to a nullable type, int? x = (int?)(object)null;, works and gives null.

12. Is boxing the same as type casting?

No. Casting between reference types (for example Animal to Dog) only changes how you view the same object; nothing is copied or allocated. Boxing creates a new object and copies the value. Numeric conversion (int to long) changes the value's representation without any heap allocation. Unboxing only extracts the value; it never converts it to another type.

13. Is a string a boxed value?

No. string is already a reference type, so assigning a string to object does not box anything. Only value types are boxed.

14. What IL instructions are used for boxing and unboxing?

The compiler emits the box instruction for boxing and unbox.any (or unbox) for unboxing. You can see them by opening the compiled assembly in a disassembler such as ILSpy or ildasm. This is a quick way to find hidden boxing in code.

15. How do you avoid boxing in real projects?

Use generic collections and generic methods, implement IEquatable<T> on structs used as keys or compared often, prefer string interpolation to string.Format with value types, use typed data-reader methods, and use a profiler to find boxing in hot paths before optimising.

Boxing vs unboxing at a glance

PointBoxingUnboxing
DirectionValue type to object / interfaceobject / interface to value type
SyntaxImplicit: object o = i;Explicit: int i = (int)o;
MemoryAllocates a new object on the heap and copies the valueCopies the value out of the heap object; no allocation
Can it fail?NoYes: InvalidCastException (wrong type) or NullReferenceException (null)
IL instructionboxunbox.any
CostHigher (heap allocation + later garbage collection)Lower (type check + copy)

Summary

  • Boxing wraps a value type in a heap object so it can be treated as object or an interface; it is implicit.
  • Unboxing copies the value back out with an explicit cast, and the type must match exactly.
  • The box is a copy: changing the original variable doesn't change the box, and vice versa.
  • Each box costs a heap allocation (24 bytes for an int on 64-bit) and garbage-collection work.
  • Watch for hidden boxing: non-generic collections, object parameters, interfaces on structs, the default struct Equals.
  • Avoid it with generics, IEquatable<T>, string interpolation and typed APIs, focusing on hot paths.