Value Types vs Reference Types, Struct vs Class, and Boxing & Unboxing
These three topics are deeply connected.
You cannot fully understand struct vs class without understanding the stack and heap.
You cannot fully understand boxing and unboxing without understanding value and reference types.
This guide covers all three together — the way they should be taught.
How .NET Manages Memory
Before anything else, you need to understand two memory regions.
The Stack
- Fast allocation and deallocation
- LIFO (Last In, First Out) structure
- Stores value types and method call frames
- Memory is automatically reclaimed when the method exits
- Limited in size
The Heap
- Slower allocation
- Stores reference types (objects)
- Managed by the Garbage Collector (GC)
- Much larger than the stack
- Memory is reclaimed by the GC, not deterministically
This distinction is the foundation of everything that follows.
Value Types
A value type holds its data directly in memory.
When you assign a value type to another variable, a copy is made.
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10 — not affected
Console.WriteLine(b); // 20
Common value types
-
int,float,double,decimal -
bool,char -
DateTime,TimeSpan structenum- Nullable value types (
int?)
Where they live
Value types declared as local variables live on the stack.
Value types declared as fields inside a class live on the heap alongside the object.
Reference Types
A reference type stores a reference (pointer) to the actual data on the heap.
When you assign a reference type to another variable, both variables point to the same object.
var person1 = new Person { Name = "Alice" };
var person2 = person1;
person2.Name = "Bob";
Console.WriteLine(person1.Name); // Bob — both point to the same object
Common reference types
classstringinterfacedelegate- Arrays
object
Where they live
The reference (the pointer) can live on the stack.
The actual object data always lives on the heap.
The Key Difference — Side by Side
| Value Type | Reference Type | |
|---|---|---|
| Stores | Actual data | Reference to data |
| Assignment | Creates a copy | Shares the reference |
| Memory | Stack (usually) | Heap |
| Null | Not nullable by default | Nullable |
| GC involvement | None | Yes |
| Examples |
int, struct
|
class, string
|
Struct vs Class
struct is a value type.
class is a reference type.
This single difference drives all the behaviour below.
Struct example
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1; // copy
p2.X = 99;
Console.WriteLine(p1.X); // 1 — unaffected
Console.WriteLine(p2.X); // 99
Class example
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1; // same reference
p2.X = 99;
Console.WriteLine(p1.X); // 99 — both affected
Console.WriteLine(p2.X); // 99
Struct vs Class — Feature Comparison
| Feature | Struct | Class |
|---|---|---|
| Type | Value type | Reference type |
| Memory | Stack (typically) | Heap |
| Inheritance | Cannot inherit | Supports inheritance |
| Default constructor | Implicit (zeroes fields) | Can define |
| Nullable | No (unless Nullable<T>) |
Yes |
| Passed by | Value (copy) | Reference |
| GC pressure | None | Yes |
| Best for | Small, immutable data | Complex objects, behaviour |
When to Use Struct
Use a struct when:
- The data is small (typically under 16 bytes)
- The type is logically a single value (a point, a colour, a coordinate)
- Immutability is desired
- You want to avoid heap allocation and GC pressure
- The type will be used heavily in tight loops or collections
// Good struct candidates
public struct Coordinate { public double Lat; public double Lon; }
public struct Rgb { public byte R; public byte G; public byte B; }
public struct Money { public decimal Amount; public string Currency; }
When NOT to use struct
- The data is large (copying becomes expensive)
- The type needs inheritance
- The type has mutable state that gets passed around
- The type will be used as an interface (triggers boxing — explained below)
Boxing and Unboxing
Boxing and unboxing happen when a value type is treated as a reference type.
Boxing
Converting a value type to object (or an interface type).
int number = 42;
object boxed = number; // Boxing — a copy is placed on the heap
What happens under the hood:
- Memory is allocated on the heap
- The value is copied from the stack into the heap object
- The
objectvariable holds a reference to that heap memory
Unboxing
Converting a boxed object back to a value type.
object boxed = 42;
int unboxed = (int)boxed; // Unboxing — copies value back from heap to stack
What happens under the hood:
- The runtime checks the type matches
- The value is copied from the heap back to the stack
Why Boxing is Expensive
Boxing seems harmless. It isn't.
Every boxing operation:
- Allocates heap memory
- Triggers the Garbage Collector more frequently
- Copies data (twice — once to box, once to unbox)
- Adds CPU overhead from type checking
In hot paths — tight loops, high-frequency operations, large collections — boxing degrades performance significantly.
The Classic Boxing Trap — ArrayList
// ❌ Old way — ArrayList boxes every int
var list = new ArrayList();
for (int i = 0; i < 1_000_000; i++)
{
list.Add(i); // Boxing happens 1,000,000 times
}
// ✅ New way — List<T> avoids boxing entirely
var list = new List<int>();
for (int i = 0; i < 1_000_000; i++)
{
list.Add(i); // No boxing
}
List<T> uses generics — no boxing required.
ArrayList treats everything as object — boxing on every insert.
Another Common Trap — Interfaces and Structs
public interface IPrintable
{
void Print();
}
public struct Message : IPrintable
{
public string Text;
public void Print() => Console.WriteLine(Text);
}
IPrintable msg = new Message { Text = "Hello" }; // ❌ Boxing!
Assigning a struct to an interface causes boxing.
The struct is copied to the heap so it can be referenced as an interface.
Spotting Boxing at Runtime
Boxing shows up as:
- Increased GC collections (especially Gen0)
- Memory pressure in profilers
-
System.Objectallocations in heap snapshots
Use tools like:
- dotMemory (JetBrains)
- BenchmarkDotNet with memory diagnostics
- Visual Studio Diagnostic Tools
-
GC.GetTotalMemory(false)for quick checks
Real-World Scenario
Scenario: Logging with string interpolation
int userId = 101;
bool isActive = true;
// ❌ Older pattern — potential boxing with format args
string.Format("User {0} active: {1}", userId, isActive);
// ✅ Modern — string interpolation compiles efficiently
$"User {userId} active: {isActive}";
// ✅ Best for hot paths — structured logging with no boxing via generics
_logger.LogInformation("User {UserId} active: {IsActive}", userId, isActive);
ILogger<T> uses generic overloads to avoid boxing on primitive arguments.
Interview-Ready Summary
- Value types store data directly — assignment copies the value
- Reference types store a pointer — assignment shares the reference
- Stack = fast, limited, deterministic; Heap = larger, GC-managed
-
structis a value type;classis a reference type - Use
structfor small, immutable, short-lived data - Boxing converts a value type to
objectand allocates on the heap - Unboxing copies the value back from the heap
- Boxing in loops or large collections silently destroys performance
- Generics (
List<T>) were introduced specifically to eliminate boxing - Assigning a struct to an interface triggers boxing
A strong interview answer:
"Value types hold their data directly and live on the stack by default, while reference types store a pointer to heap memory. Structs are value types best suited for small, immutable data. Boxing wraps a value type in a heap-allocated object — it's implicit, easy to miss, and expensive at scale. Generics eliminate boxing by preserving type information at compile time."
⭐ Add-On — Why This Matters More Now With record struct
C# 10 introduced record struct — a value type with built-in immutability and value equality.
public record struct Coordinate(double Lat, double Lon);
var a = new Coordinate(1.0, 2.0);
var b = new Coordinate(1.0, 2.0);
Console.WriteLine(a == b); // True — value equality, no boxing
This gives you the clean syntax of records without the heap allocation of reference-type records.
For high-performance scenarios where you want immutable data structures, record struct is now the go-to choice.
Top comments (0)