Say you’re cleaning up an old order tracker for a Columbus coffee roaster, and it keeps daily order counts in an ArrayList. Every int that goes in gets copied into a new object on the heap, and every read needs a cast back.
That’s boxing and unboxing in C#, and the tracker pays for it on every order.
Quick answer: object o = count; boxes, (int)o unboxes, and the cast must name the exact type. Generics like List<int> avoid it. I ran every example below on .NET 10.
What Is Boxing in C#?
Boxing converts a value type (int, decimal, a struct) to object or to an interface the type implements. The runtime allocates a small object on the managed heap and copies the value into it.
Value types normally live inline, which I cover in the C# data types guide. Here’s one day of roaster orders:
int todayOrders = 42;
object boxed = todayOrders; // boxing: copy on the heap
int unboxed = (int)boxed; // unboxing: explicit cast
Console.WriteLine($"boxed holds: {boxed.GetType()}");
Console.WriteLine($"unboxed: {unboxed}");
todayOrders = 57; // change the original
Console.WriteLine($"todayOrders: {todayOrders}, boxed: {boxed}");
object again = todayOrders; // a second, separate box
Console.WriteLine($"Same box? {ReferenceEquals(boxed, again)}");
Output:
boxed holds: System.Int32
unboxed: 42
todayOrders: 57, boxed: 42
Same box? False

Three things show up. The box remembers its real type (System.Int32), the boxed value stays 42 after the original moves to 57, and boxing the same variable twice creates two different objects.
Note: boxing is implicit, so the compiler never warns you. Unboxing always needs an explicit cast.
How Does Unboxing Work? Exact Type or InvalidCastException
Unboxing checks that the object really holds the value type you name, then copies the value out. “Close enough” types don’t count, which surprises people who know that an int converts to long without a cast:
object boxed = 42; // a boxed int
int count = (int)boxed; // OK: exact type
Console.WriteLine($"(int): {count}");
try { long total = (long)boxed; } // wrong type
catch (InvalidCastException ex) { Console.WriteLine($"(long): {ex.Message}"); }
long widened = (int)boxed; // unbox first, then widen
Console.WriteLine($"(int) then long: {widened}");
Console.WriteLine($"Convert.ToInt64: {Convert.ToInt64(boxed)}");
object? missing = null;
try { int n = (int)missing!; }
catch (NullReferenceException) { Console.WriteLine("(int)null: NullReferenceException"); }
Output:
(int): 42
(long): Unable to cast object of type 'System.Int32' to type 'System.Int64'.
(int) then long: 42
Convert.ToInt64: 42
(int)null: NullReferenceException

(long)boxed threw because the box holds an Int32. Unbox to the exact type first and widen afterward, or call Convert.ToInt64, which reads the boxed value through IConvertible.
Unboxing null throws NullReferenceException instead. Catch neither in real code; try catch in C# is for the rare failure, and type casting in C# explains which conversions are safe.
Where Does Boxing Hide in Everyday C# Code?
You’ll rarely write object o = i; on purpose. Boxing usually sneaks in through APIs that take object. Same 42 roaster orders, four quiet boxes:
using System.Collections;
int orders = 42;
var legacy = new ArrayList { orders }; // boxes
Log(orders); // boxes: object parameter
Console.WriteLine(string.Format("{0} orders", orders)); // boxes
var tally = new Tally();
ITally viaInterface = tally; // boxes a COPY
viaInterface.Add(5);
Console.WriteLine($"struct: {tally.Count}, boxed copy: {viaInterface.Count}");
void Log(object value) => Console.WriteLine($"Logged {value} as {value.GetType().Name}");
interface ITally { int Count { get; } void Add(int n); }
struct Tally : ITally { public int Count { get; private set; } public void Add(int n) => Count += n; }
Output:
Logged 42 as Int32
42 orders
struct: 0, boxed copy: 5
ArrayList stores object, so Add boxes. Log(object) and string.Format‘s object arguments box too.
The struct line is the sneaky one. Assigning a struct to an interface variable boxes a copy, so Add(5) changed the copy (5) and the original tally stayed at 0.
Two old hot spots are mostly gone. Interpolated strings use an interpolation handler since C# 10, so $"{orders}" formats an int without boxing it, and modern .NET’s JIT compiles Enum.HasFlag without the box it needed on .NET Framework (see enums in C#).
Avoid Boxing with Generics: List<int> vs ArrayList
Generics fix the problem at the type level. A List<int> stores real int values in its backing array, so nothing is boxed going in or coming out. I timed 5 million order counts both ways:
using System.Collections;
using System.Diagnostics;
var sw = Stopwatch.StartNew();
long bytes = GC.GetAllocatedBytesForCurrentThread();
var legacy = new ArrayList();
for (int i = 0; i < 5_000_000; i++) legacy.Add(i % 50);
long a = 0; foreach (object o in legacy) a += (int)o;
Report("ArrayList", a);
var typed = new List<int>();
for (int i = 0; i < 5_000_000; i++) typed.Add(i % 50);
long b = 0; foreach (int n in typed) b += n;
Report("List<int>", b);
void Report(string name, long sum) {
long now = GC.GetAllocatedBytesForCurrentThread();
string ms = $"{sw.ElapsedMilliseconds} ms";
Console.WriteLine($"{name,-10} {sum} {ms} {(now - bytes) / 1048576.0:F1} MB");
sw.Restart(); bytes = now; }
Output:
ArrayList 122500000 489 ms 242.4 MB
List<int> 122500000 37 ms 64.0 MB

On my PC, ArrayList took 489 ms and allocated 242.4 MB, while List<int> took 37 ms and allocated 64.0 MB. Your milliseconds will differ, but the allocation gap won’t.
Most of the List<int> number is its backing array doubling as it grows. ArrayList grows a bigger array of references and adds a separate heap object for every boxed count on top.
For your own methods, do the same thing: void Log<T>(T value) receives an int as an int, while void Log(object value) boxes it at every call.
Boxing Nullable Types: Why int? null Boxes to null
Nullable value types follow two special rules. An empty int? boxes to a plain null reference, and a filled one boxes the underlying int, not a Nullable<int>:
int? noOrders = null;
int? someOrders = 18;
object? boxedNull = noOrders; // HasValue false -> null reference
object? boxedSome = someOrders; // boxes the int, not Nullable<int>
Console.WriteLine($"boxedNull is null: {boxedNull is null}");
Console.WriteLine($"boxedSome type: {boxedSome?.GetType()}");
int? back = (int?)boxedSome; // unbox a boxed int to int?
int? backNull = (int?)boxedNull; // null unboxes to an empty int?
Console.WriteLine($"back: {back}, backNull.HasValue: {backNull.HasValue}");
Output:
boxedNull is null: True
boxedSome type: System.Int32
back: 18, backNull.HasValue: False

That’s why someOrders.GetType() reports System.Int32. Unboxing to int? is the safe way to read a box that might be null; converting null to int in C# covers the other options.
Common Boxing Errors and How to Fix Them
| Problem | Cause | Fix |
|---|---|---|
InvalidCastException on unbox | Cast type isn’t the boxed type ((long) on an int) | Unbox to the exact type, then convert |
NullReferenceException on unbox | The object is null | Unbox to int? or use is int n |
| Struct change “disappears” | Interface or object variable holds a boxed copy | Call methods on the struct itself, or use a class |
| High GC pressure in loops | ArrayList, Hashtable or object parameters | Switch to List<T>, Dictionary<K,V>, generics |
For safe reads, pattern matching beats a raw cast: if (boxed is int n) never throws. Arrays like int[] don’t box either, as shown in the C# array guide.
When Is Boxing Fine?
Don’t chase every box. A few per request, like logging one value, cost nothing you’d notice. Fix boxing in hot loops, large collections and code that mutates structs through interfaces.
These pair well with this one if you’re tightening up type-heavy code:
- Swap Hashtable for a typed Dictionary in C#
- Brush up on value and reference variables in C#
- Return several values without object arrays using tuples
- Format values with ToString instead of object-based APIs
Frequently Asked Questions
What is boxing and unboxing in C# in simple words?
Boxing wraps a value type such as an int in an object on the heap so it can be treated as an object. Unboxing copies the value back out with an explicit cast, for example int n = (int)o;.
Is boxing implicit or explicit in C#?
Boxing is implicit: object o = 42; boxes without a cast. Unboxing is explicit and must name the exact value type, as explained in Boxing and Unboxing on Microsoft Learn.
Why does unboxing an int as long throw InvalidCastException?
Unboxing only succeeds for the exact type that was boxed. A boxed Int32 can’t be unboxed as Int64. Unbox it as int and then assign it to a long, or use Convert.ToInt64.
Does List<int> cause boxing?
No. List<int> stores int values directly. Boxing happens with ArrayList, Hashtable, object parameters and non-generic interfaces.
What happens when you box a null int??
You get a null reference, not a box. A non-null int? boxes the underlying int, so GetType() returns System.Int32.
Is boxing slow in C#?
Each box is a heap allocation plus garbage collection work later, so it adds up in loops over large data. A handful of boxes per request isn’t worth optimizing.
Bijay Kumar is a renowned software engineer, accomplished author, and distinguished Microsoft Most Valuable Professional (MVP) specializing in SharePoint. With a rich professional background spanning over 15 years, Bijay has established himself as an authority in the field of information technology. He possesses unparalleled expertise in multiple programming languages and technologies such as ASP.NET, ASP.NET MVC, C#.NET, and SharePoint, which has enabled him to develop innovative and cutting-edge solutions for clients across the globe. Read more…