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 (objector 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 types | Reference types | |
|---|---|---|
| Examples | int, double, bool, char, decimal, DateTime, enum, struct | class, string, object, arrays, interfaces, delegates |
| The variable holds | The value itself | A reference (address) to an object on the heap |
| Copying the variable | Copies the value | Copies the reference; both point to the same object |
| Memory | Stored inline: on the stack for local variables, or inside the object/array that contains it | The 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:
- A new object is allocated on the heap.
- The value
25is copied into it, together with type information (so the box knows it contains anInt32). - The variable
boxreceives 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:
- checks that
boxis notnull(otherwiseNullReferenceException), - checks that the box contains exactly the requested type (otherwise
InvalidCastException), - 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!
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}");
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");
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:
| Code | Why it boxes | Better |
|---|---|---|
arrayList.Add(5), hashtable[1] = 2 | Non-generic collections store object | List<int>, Dictionary<int, int> |
string.Format("{0}", count), Console.WriteLine("{0}", count) | The arguments are object parameters | String interpolation $"{count}" (C# 10+ avoids boxing) |
Passing a value to a method with an object or params object[] parameter | The value must become an object | Generic method void Log<T>(T value) or specific overloads |
IComparable c = 5; or passing a struct as an interface | Converting a value type to an interface boxes it | Generic 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 object | reader.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 ofArrayListandHashtable. - Write generic methods (
T) instead of methods that takeobject. - Use generic interfaces such as
IComparable<T>andIEquatable<T>, and implementIEquatable<T>on your own structs. - Prefer string interpolation over
string.Formatwith 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));
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}");
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
| Point | Boxing | Unboxing |
|---|---|---|
| Direction | Value type to object / interface | object / interface to value type |
| Syntax | Implicit: object o = i; | Explicit: int i = (int)o; |
| Memory | Allocates a new object on the heap and copies the value | Copies the value out of the heap object; no allocation |
| Can it fail? | No | Yes: InvalidCastException (wrong type) or NullReferenceException (null) |
| IL instruction | box | unbox.any |
| Cost | Higher (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
objector 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
inton 64-bit) and garbage-collection work. - Watch for hidden boxing: non-generic collections,
objectparameters, interfaces on structs, the default structEquals. - Avoid it with generics,
IEquatable<T>, string interpolation and typed APIs, focusing on hot paths.