DEV Community

Cover image for Value Types vs Reference Types, Struct vs Class, and Boxing & Unboxing — The Complete C# Guide
Libin Tom Baby
Libin Tom Baby

Posted on

Value Types vs Reference Types, Struct vs Class, and Boxing & Unboxing — The Complete C# Guide

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
Enter fullscreen mode Exit fullscreen mode

Common value types

  • int, float, double, decimal
  • bool, char
  • DateTime, TimeSpan
  • struct
  • enum
  • 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
Enter fullscreen mode Exit fullscreen mode

Common reference types

  • class
  • string
  • interface
  • delegate
  • 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

What happens under the hood:

  1. Memory is allocated on the heap
  2. The value is copied from the stack into the heap object
  3. The object variable 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
Enter fullscreen mode Exit fullscreen mode

What happens under the hood:

  1. The runtime checks the type matches
  2. 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
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.Object allocations 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);
Enter fullscreen mode Exit fullscreen mode

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
  • struct is a value type; class is a reference type
  • Use struct for small, immutable, short-lived data
  • Boxing converts a value type to object and 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
Enter fullscreen mode Exit fullscreen mode

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)