π Lesson 4.1: The Memory Model: Value vs Reference
To write high-performance C#, you need a precise picture of where your data lives and how it's copied. This lesson makes the value-vs-reference distinction concrete: stack vs heap, copy semantics, and the hidden cost of boxing.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish value types from reference types precisely
- Explain what goes on the stack vs. the heap (and the nuances)
- Predict copy semantics for structs and classes
- Recognize boxing/unboxing and its cost
- Choose between a
structand aclassdeliberately
Estimated Time: 75 minutes
Project: Observe copy semantics and eliminate hidden boxing.
In This Lesson
Two Kinds of Types
Every C# type is either a value type or a reference type, and that single fact drives how it's stored, copied, and compared.
| Value types | Reference types | |
|---|---|---|
| Declared with | struct, enum, primitives | class, record (class), interface, arrays, delegates |
| A variable holds | the actual data | a reference (pointer) to the data |
| Examples | int, double, bool, DateTime, Guid | string, List<T>, your classes |
| Default value | zero/"empty" (e.g. 0) | null |
π The core difference
A value-type variable contains its value. A reference-type variable contains a reference to an object stored elsewhere. Copying a value copies the data; copying a reference copies only the pointer β both then point to the same object.
Stack vs. Heap
.NET uses two memory regions:
- The stack β fast, automatically managed, per-thread. Holds local variables and method call frames; freed instantly when a method returns.
- The heap β where objects live. Managed by the garbage collector (GC), which reclaims objects no longer referenced (Lesson 4.3).
(the value itself)"] S2["Person p
(a reference)"] end subgraph Heap["Heap (GC-managed)"] H1["Person object
Name, Age"] end S2 -->|"points to"| H1 style Stack fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style Heap fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
β οΈ "Value types live on the stack" is an oversimplification
The accurate rule is about copy behavior, not location. A value type lives wherever it's declared: a local int is on the stack, but an int field inside a class lives on the heap (inside that object), and a boxed value lives on the heap. Don't over-index on "stack vs heap" β focus on value vs reference semantics.
Copy Semantics
This is where the distinction bites. Assigning or passing a value type copies the data; the copy is independent:
struct PointStruct { public int X; public int Y; }
var a = new PointStruct { X = 1, Y = 2 };
var b = a; // COPY β b is a separate PointStruct
b.X = 99;
Console.WriteLine(a.X); // 1 β a is unaffected
Console.WriteLine(b.X); // 99
Assigning a reference type copies the reference; both variables point to the same object:
class PointClass { public int X; public int Y; }
var c = new PointClass { X = 1, Y = 2 };
var d = c; // COPY OF THE REFERENCE β same object
d.X = 99;
Console.WriteLine(c.X); // 99 β c and d are the same object!
Console.WriteLine(d.X); // 99
β οΈ Passing structs to methods copies them too
A struct passed to a method is copied, so changes inside the method don't affect the caller's copy (unless passed by ref). Large structs copied frequently can hurt performance β every assignment and call duplicates all their fields. This is a key reason to keep structs small.
π‘ Equality follows suit
By default, value types compare by their contents and reference types by identity (same object) β which is exactly the value-vs-reference-equality story from records in Intermediate. A record struct gives you a value type with generated value equality.
Boxing and Unboxing
Sometimes a value type needs to be treated as a reference type β for example, stored in an object or a non-generic collection. The runtime boxes it: wraps the value in a heap object. Extracting it back is unboxing.
int n = 42;
object boxed = n; // BOXING β allocates a heap object holding 42
int back = (int)boxed; // UNBOXING β copies the value back out
(on the stack)"] -->|"box β allocates"| B["object on heap
wrapping 42"] B -->|"unbox β copies out"| C["int back = 42"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style B fill:#fff3cd,stroke:#ffc107,stroke-width:2px style C fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
β οΈ Boxing is a hidden allocation β and it hides everywhere
Each box allocates on the heap and adds GC pressure. It often happens invisibly:
- Putting a value type into
objector a non-generic collection (ArrayList). - Calling a method that takes
object(e.g. oldstring.Formatwith value args). - Using a value type through a non-generic interface reference.
β Generics eliminate boxing
This is a core reason generics (Intermediate) exist. A List<int> stores ints directly β no boxing β while the old non-generic ArrayList boxed every element. Prefer generic collections and APIs, and avoid unnecessary trips through object, to keep value types allocation-free.
Choosing struct vs. class
Default to class. Reach for a struct only when the type genuinely models a small value and you'll benefit from value semantics or reduced allocations.
β
Consider a struct when ALL of these hold
- It's small (roughly β€ 16 bytes is the common guideline).
- It's immutable (mutable structs are a well-known source of bugs).
- It logically represents a single value (a point, a money amount, a coordinate).
- It won't be boxed frequently.
β οΈ Prefer a class whenβ¦
- The type is large (copying is expensive), or
- It needs reference identity (shared, mutable state that many holders should see), or
- It participates in inheritance hierarchies (structs can't inherit).
π‘ The examples in .NET tell the story:int,DateTime,Guid,TimeSpan, anddecimalare structs β small, immutable values.List<T>,string, and your entities are classes. When in doubt, chooseclass; a wrongstructchoice is harder to undo.
Modern Struct Features
Modern C# adds tools for high-performance value types:
| Feature | What it gives you |
|---|---|
readonly struct | An immutable struct; the compiler enforces no field mutation and can avoid defensive copies. |
record struct | A value type with generated value equality and ToString (Intermediate records, as a struct). |
ref struct | A struct that must live on the stack only (never boxed or heap-allocated) β the basis of Span<T>, next lesson. |
in parameters | Pass a large struct by read-only reference to avoid copying it. |
// Immutable value type β no defensive copies, safe to share by value
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
}
// Pass a big struct without copying it, using 'in'
static decimal Total(in Money a, in Money b) => a.Amount + b.Amount;
π‘ readonly struct is the safe default struct
If you do write a struct, make it a readonly struct. Immutability avoids the classic mutable-struct pitfalls (surprising copies discarding changes) and lets the compiler optimize away hidden defensive copies. ref struct is specialized β you'll meet it properly with Span<T> in Lesson 4.2.
Exercise & Quiz
ποΈ Exercise: Semantics and Boxing
Objective: Demonstrate copy semantics and spot/remove boxing.
Instructions:
- Create a new project called
MemoryModel. - Define
struct Vec { public int X, Y; }andclass Node { public int Value; }. - Show that copying a
Vecand mutating the copy leaves the original unchanged, while copying aNodereference and mutating it changes both variables. Print to prove it. - Write a method
void Move(Vec v)that setsv.X = 100; show the caller'sVecis unaffected (copy). Then make arefversion and show it is affected. - Boxing: add several
ints to anobject[](boxing) and to aList<int>(no boxing). Note which allocates.
Starter Code:
struct Vec { public int X, Y; }
class Node { public int Value; }
var v1 = new Vec { X = 1, Y = 1 };
var v2 = v1; // copy
v2.X = 99;
Console.WriteLine($"v1.X={v1.X}, v2.X={v2.X}"); // TODO: predict then verify
var n1 = new Node { Value = 1 };
var n2 = n1; // copy of reference
n2.Value = 99;
Console.WriteLine($"n1.Value={n1.Value}, n2.Value={n2.Value}"); // TODO: predict
// TODO: Move(Vec) vs Move(ref Vec); object[] boxing vs List<int>
π‘ Hint
Vec is a value type β v1.X stays 1, v2.X is 99. Node is a reference type β both are 99. For Move, a plain parameter copies (no effect); ref Vec v mutates the original. object[] a = {1,2,3}; boxes each int; List<int> does not.
β Solution
struct Vec { public int X, Y; }
class Node { public int Value; }
var v1 = new Vec { X = 1, Y = 1 };
var v2 = v1; // independent copy
v2.X = 99;
Console.WriteLine($"v1.X={v1.X}, v2.X={v2.X}"); // v1.X=1, v2.X=99
var n1 = new Node { Value = 1 };
var n2 = n1; // same object
n2.Value = 99;
Console.WriteLine($"n1.Value={n1.Value}, n2.Value={n2.Value}"); // both 99
Move(v1);
Console.WriteLine($"after Move(copy): v1.X={v1.X}"); // 1 β unaffected
MoveRef(ref v1);
Console.WriteLine($"after Move(ref): v1.X={v1.X}"); // 100 β changed
// Boxing vs no boxing
object[] boxed = { 1, 2, 3 }; // each int is boxed (3 heap allocations)
List<int> noBox = new() { 1, 2, 3 }; // stored as ints β no boxing
Console.WriteLine($"{boxed.Length} boxed, {noBox.Count} unboxed");
static void Move(Vec v) { v.X = 100; } // mutates a copy
static void MoveRef(ref Vec v) { v.X = 100; } // mutates the caller's Vec
π― Quick Quiz
Question 1: What does a reference-type variable actually hold?
Question 2: After var b = a; where a is a struct, modifying bβ¦
Question 3: What is boxing?
Summary
π Key Takeaways
- Value types (
struct, primitives) hold their data; reference types (class, arrays,string) hold a reference to a heap object. - The stack holds locals/frames (fast, auto); the heap holds objects (GC-managed). "Value types are always on the stack" is an oversimplification β focus on copy semantics.
- Copying a value duplicates the data (independent); copying a reference shares the same object. Structs are copied when passed to methods too.
- Boxing wraps a value type in a heap object β a hidden allocation; generics avoid it, so prefer generic collections/APIs.
- Default to
class; use a small, immutablereadonly structfor single-value types. Modern features:record struct,ref struct,inparameters.
π Additional Resources
π What's Next?
You understand where data lives and how it's copied. Next, we use that to process data with zero extra allocations. In Lesson 4.2: Span and Memory, you'll slice arrays and buffers without copying them.
π Memory model, mastered!
You can now reason precisely about copies and allocations. Next: high-performance slicing with Span.