C# Value Types vs Reference Types Explained
Understanding value types and reference types is the foundation of how memory works in .NET.
Imagine you're building a banking application. You create a Customer object with a name, account number, and balance. When you pass that object to a method, does the method get a copy of the customer or a reference to the original? If it gets a reference, any changes inside the method affect the original object. If it gets a copy, changes are isolated.
This is the fundamental difference between value types and reference types — and it affects everything from performance to bug prevention. In this lesson, you'll learn exactly what they are, how they work in memory, and how to choose the right one for your code.
What Is It?
The Simple Explanation
Every piece of data in your C# program lives somewhere in memory. Value types hold their data directly where they are declared. Reference types hold a pointer (a reference) to where the data actually lives.
- Value type — the variable is the data. When you assign it to another variable, the data is copied.
- Reference type — the variable points to the data. When you assign it to another variable, the reference is copied, not the data.
The Technical Definition
In C#, types are divided into two fundamental categories:
Important: string is a reference type, but it behaves like a value type in many ways because it is immutable — you can't change the contents of an existing string, so modifications create new strings. We covered this in the previous lesson.
Why Does It Exist?
The Problem
Memory is not infinite, and how you use it affects performance. If every piece of data were stored the same way, you'd either:
- Copy everything everywhere — which is slow for large objects, or
- Reference everything everywhere — which makes it difficult to reason about who owns what data.
Real programs need both:
- Small, independent pieces of data (like numbers, booleans, dates) that are cheap to copy and should be isolated.
- Large, shared data structures (like customer records, order histories) that are expensive to copy and need to be shared across methods.
The Solution
.NET gives you both value types and reference types. You choose which behavior you need for each piece of data. Value types are cheap and isolated; reference types are efficient for sharing and large data.
This design also enables:
- Garbage Collection — reference types are automatically managed on the heap.
- Stack efficiency — value types can live on the stack, which is faster to allocate and deallocate.
- Immutability — value types naturally encourage immutability because they are copied, not shared.
Big Picture
Here's how value types and reference types fit into the memory model of a .NET application:
int, bool, struct fields, and references (pointers) to heap objects.
class instances, string objects, array data, object instances. The stack holds references to these objects.
How It Works
Step 1 — Declaring a value type
int number = 42;
The runtime allocates exactly 4 bytes on the stack. The variable number contains the value 42 directly.
Step 2 — Declaring a reference type
Customer customer = new Customer();
The runtime allocates memory on the heap for the Customer object. The variable customer on the stack holds a reference (a memory address) pointing to that heap object.
Step 3 — Assigning copies values
int a = 10;
int b = a; // b gets a COPY of a's value
b = 20;
Console.WriteLine(a); // 10 — unchanged!
Value types are copied on assignment. b gets its own independent copy of the value. Changing b does not affect a.
Step 4 — Assigning references
Customer a = new Customer { Name = "Alice" };
Customer b = a; // b gets a COPY of the REFERENCE (pointer)
b.Name = "Bob";
Console.WriteLine(a.Name); // "Bob" — changed!
Reference types share data. b gets a copy of the reference (the pointer), not the object itself. Both variables point to the same object in memory. Changing the object through b affects a.
Step 5 — Passing parameters
void ModifyValue(int x) { x = 100; }
void ModifyReference(Customer c) { c.Name = "Changed"; }
int num = 5;
ModifyValue(num);
Console.WriteLine(num); // 5 — unchanged
Customer cust = new Customer { Name = "Original" };
ModifyReference(cust);
Console.WriteLine(cust.Name); // "Changed" — changed!
When you pass a value type to a method, you pass a copy. When you pass a reference type, you pass a copy of the reference — the method can modify the original object.
Simple Example
// Value type (struct)
public struct Point
{
public int X;
public int Y;
public Point(int x, int y) { X = x; Y = y; }
}
// Reference type (class)
public class Shape
{
public string Name { get; set; }
public Shape(string name) { Name = name; }
}
// Usage
Point p1 = new Point(10, 20);
Point p2 = p1; // COPY: p2 is a separate copy
p2.X = 99;
Console.WriteLine(p1.X); // 10 — unchanged
Shape s1 = new Shape("Circle");
Shape s2 = s1; // REFERENCE: s2 points to the same object
s2.Name = "Square";
Console.WriteLine(s1.Name); // "Square" — changed!
Code → Meaning → Result
Point p2 = p1— copies the entire value.p2is independent.Shape s2 = s1— copies the reference. Both point to the same heap object.- Modifying
p2.Xhas no effect onp1. - Modifying
s2.Namechanges the shared object, sos1.Namereflects the change.
Real-World Example
Imagine an e-commerce system with an order processing pipeline:
// Value type: immutable money amount
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency) => (Amount, Currency) = (amount, currency);
public Money Add(decimal amount) => new Money(Amount + amount, Currency);
}
// Reference type: order (large, mutable, shared)
public class Order
{
public int OrderId { get; set; }
public string CustomerName { get; set; } = "";
public List<OrderItem> Items { get; } = new();
public Money Total { get; set; }
public void AddItem(string name, decimal price, int quantity)
{
Items.Add(new OrderItem(name, price, quantity));
Total = Total.Add(price * quantity);
}
}
// Usage
Order order = new Order { OrderId = 1024, CustomerName = "Alice" };
// Passing by reference: method modifies the original order
void ApplyDiscount(Order o, decimal discountPercent)
{
// Modifies the original order object
decimal discount = o.Total.Amount * (discountPercent / 100);
o.Total = o.Total.Add(-discount);
}
ApplyDiscount(order, 10); // Order is modified in place
Console.WriteLine(order.Total.Amount); // Discount applied!
Why this matters:
Moneyis a value type (struct) — cheap to copy, immutable, and thread-safe. Perfect for currency values.Orderis a reference type (class) — it contains a list of items and is passed around the system efficiently by reference.- The
ApplyDiscountmethod modifies the original order becauseOrderis a reference type. - If
Orderwere a struct, every method call would copy the entire order — including all items — which would be very slow.
Analogy
Value types are like photocopies of a document. If you give someone a photocopy and they write on it, your original stays clean. Each person has their own independent copy.
Reference types are like a whiteboard in a shared room. Everyone gets a pointer to the same whiteboard. If someone writes on it, everyone sees the change. The pointer itself is small (a reference), but the whiteboard can be huge.
ref) = The original documentWhen you use ref with a value type, you're passing the original document itself, not a copy. The method can modify it directly.
Under the Hood
What actually happens inside the .NET runtime at the memory level?
Stack: [int num = 42] ← the value 42 is stored directly
No heap allocation. No GC pressure. Fast allocation and deallocation.
Stack: [Customer cust] → 0x1A2B3C
Heap: 0x1A2B3C: [Customer object] { Name="Alice", Id=1024 }
The stack holds a pointer (4 or 8 bytes). The actual object lives on the heap.
int a = 10; int b = a; // COPY: 10 is copied from a's stack slot to b's stack slot b = 20; // a still has 10, b has 20
The value is bitwise copied from one stack location to another.
Customer a = new Customer(); // a = 0x1A2B3C (heap address) Customer b = a; // b = 0x1A2B3C (same address, copied) b.Name = "Bob"; // modifies the object at 0x1A2B3C
The pointer is copied, not the object. Both variables point to the same heap memory.
When no references point to a heap object, the GC can reclaim it. Value types are reclaimed automatically when they go out of scope.
Reference types have GC overhead; value types do not.
Common Confusion
1. struct vs class
Many beginners assume struct and class are interchangeable. They are not.
| Feature | struct (Value) | class (Reference) |
|---|---|---|
| Storage | Stack (usually) | Heap |
| Assignment | Copy | Reference copy |
| Inheritance | Cannot inherit from another struct | Can inherit |
| Default constructor | Always has a default constructor | Only if you define one |
| Nullability | Cannot be null (unless nullable) | Can be null |
| GC overhead | None | Managed by GC |
| Use for | Small, immutable data | Large, shared, mutable data |
2. "Pass by value" vs "Pass by reference"
These are different concepts:
- Pass by value — the method receives a copy of the argument. Changes inside the method do not affect the caller's variable. This is the default for both value and reference types.
- Pass by reference — the method receives a reference to the variable itself (using
ref,out, orin). Changes inside the method do affect the caller's variable.
void PassByValue(int x) { x = 100; }
void PassByReference(ref int x) { x = 100; }
int a = 5;
PassByValue(a);
Console.WriteLine(a); // 5 — unchanged
PassByReference(ref a);
Console.WriteLine(a); // 100 — changed!
3. Reference types inside value types
If a struct contains a reference type (like a string), copying the struct copies the reference to the string, not the string itself. The struct's value semantics apply to its fields, but reference-type fields still share their objects.
struct Person
{
public string Name; // reference type inside a value type
}
Person p1 = new Person { Name = "Alice" };
Person p2 = p1; // p2 gets a COPY of p1 (including the reference)
p2.Name = "Bob"; // BUT: Name is a string — immutable!
Console.WriteLine(p1.Name); // "Alice" — because strings are immutable
// If Name were a mutable class, p2.Name would affect p1.Name
Common Mistakes
Mistake 1 — Assuming a struct is a class
Wrong: Treating a struct like a class and expecting reference semantics.
struct Point { public int X; }
Point p1 = new Point { X = 10 };
Point p2 = p1;
p2.X = 20;
// p1.X is still 10 — you might have expected it to change
Correct: Understand that structs are copied on assignment. If you need reference semantics, use a class.
Mistake 2 — Large structs
Wrong: Creating a struct with many fields (e.g., 100+ bytes) and copying it frequently.
Correct: Use a class for large data to avoid expensive copying. The general recommendation is to keep structs under 16 bytes.
Mistake 3 — Mutating a struct returned from a property
Wrong: Modifying a struct returned by a property does nothing because you're modifying a copy.
struct Vector { public int X; }
class Container { public Vector Position { get; set; } }
var c = new Container();
c.Position.X = 10; // This modifies a COPY — it does NOT update the stored struct!
Correct: Assign the entire struct or make it immutable.
c.Position = new Vector { X = 10 }; // Correct
Mistake 4 — Using class for everything
Wrong: Using a class for tiny, immutable data creates unnecessary heap allocations and GC pressure.
Correct: Use a readonly struct for small, immutable data like Point, Money, Color.
When Should I Use It?
Use a value type (struct) when:
- The data is small (typically ≤ 16 bytes).
- The data is immutable — it doesn't change after creation.
- You want copy semantics — each variable should have its own independent copy.
- The data is short-lived and doesn't need to survive beyond its scope.
- Examples:
Point,Money,DateTime,Guid.
Use a reference type (class) when:
- The data is large — copying it would be expensive.
- You need reference semantics — multiple parts of your program should share the same instance.
- The data is mutable — it changes over time.
- You need inheritance or polymorphism.
- Examples:
Customer,Order,DbContext,Service.
When in doubt:
- Start with a class — it's more flexible and easier to change later.
- Use a struct only when you have a clear reason: small, immutable, and performance-critical.
- In C# 10+,
record structgives you value-type semantics with value-based equality. record classgives you reference-type semantics with value-based equality.
Mental Model
Reference Type (class) = The variable points to the data.
Assignment:
· Value type: copy the data
· Reference type: copy the pointer
Passing to a method:
· Value type: method gets a copy → changes don't affect original
· Reference type: method gets a copy of the pointer → changes affect the original object
Memory:
· Value type: stack (fast, no GC)
· Reference type: heap (managed by GC)
Rule of thumb:
· Small + immutable →
struct· Large + mutable + shared →
class
Key Takeaway
- Value types (
struct) store data directly — assignment copies the data. - Reference types (
class) store a reference to data — assignment copies the reference. - Value types live on the stack (usually), reference types on the heap.
- Value types are cheap to allocate and have no GC overhead.
- Reference types are efficient for sharing large data structures.
- Use
structfor small, immutable data andclassfor large, mutable, shared data. - Understand the difference between "pass by value" and "pass by reference" — they are not the same as value type vs reference type.
- In .NET 10 / C# 14, records give you both value-based equality and the choice of value or reference semantics.
Check Your Understanding
You've seen how value types and reference types behave differently in memory. Let's see if you can apply this knowledge.
1. What is the fundamental difference between a value type and a reference type in C#?
Show answer
Correct: B
Why B is correct: The core behavioral difference is that value types are copied on assignment (each variable has its own independent copy), while reference types share the same object (assignment copies the reference, not the object).
Why A is incorrect: Value types are typically stored on the stack, and reference types are stored on the heap — not the other way around.
Why C is incorrect: Reference types can be null; value types cannot (unless using nullable types). This is a consequence, not the fundamental difference.
Why D is incorrect: Reference types support inheritance; value types (structs) do not — again, this is a consequence, not the fundamental difference.
Reinforcement: The key behavioral difference is copy semantics (value types) vs reference semantics (reference types).
2. What does the following code output?
struct Point { public int X; }
class Wrapper { public Point Point; }
var w1 = new Wrapper { Point = new Point { X = 5 } };
var w2 = w1;
w2.Point.X = 10;
Console.WriteLine(w1.Point.X);
Show answer
Correct: B
Why B is correct: w1 and w2 both reference the same Wrapper object on the heap. The Point field is a value type stored inline inside the Wrapper object. Since both w1 and w2 point to the same Wrapper instance, modifying w2.Point.X changes the same Point that w1 sees.
Why A is incorrect: Point is a struct, but it is stored inside a reference type (Wrapper). The Wrapper object is shared, so the Point inside it is also shared.
Why C is incorrect: This code compiles and runs without errors.
Why D is incorrect: The Point is initialized to 5, then modified to 10.
Reinforcement: When a value type is a field inside a reference type, it lives on the heap inside that object. The object is shared, so the value-type field is also shared.
3. You are designing a geometry library. You need a Point type with X and Y coordinates. The type should be immutable and used heavily in performance-critical calculations. Which approach is most appropriate?
Show answer
Correct: B
Why B is correct: A readonly struct is a value type that is immutable. It is small (two integers = 8 bytes), will be copied efficiently on the stack, and has no GC overhead — perfect for performance-critical geometry calculations.
Why A is incorrect: A class would allocate each point on the heap, causing GC pressure and slower performance in hot paths. It also allows mutability unless carefully designed.
Why C is incorrect: A record class is a reference type. It would have similar heap allocation overhead as a class. While it provides value-based equality, it's not the best choice for a small, performance-critical value.
Why D is incorrect: dynamic bypasses compile-time type checking and is used for interoperability or dynamic languages — not appropriate here at all.
Reinforcement: Use readonly struct for small, immutable, frequently used data types — they combine value semantics, stack allocation, and immutability.
4. What is the difference between "pass by value" and "pass by reference" in C#?
Show answer
Correct: C
Why C is correct: "Pass by value" means the method receives a copy of the argument's value. Changes inside the method do not affect the caller's variable. "Pass by reference" (using ref, out, or in) means the method receives a reference to the variable itself. Changes inside the method do affect the caller's variable.
Why A is incorrect: They are fundamentally different — one copies the value, the other passes a reference to the variable.
Why B is incorrect: Both value types and reference types can be passed by value (default) or by reference (using ref). For reference types, "pass by value" means passing a copy of the reference — the method can still modify the object, but it cannot reassign the caller's variable.
Why D is incorrect: Performance depends on the size of the data and the context. Passing a large struct by reference can be faster than copying it. This is not a simple rule.
Reinforcement: "Pass by value" vs "pass by reference" is about how the argument is passed to the method, not about whether the type is a value type or reference type.
5. Consider this code. What is the output?
void Update(ref int x) { x = 100; }
int a = 5;
Update(ref a);
Console.WriteLine(a);
Show answer
Correct: B
Why B is correct: The ref keyword passes the a variable by reference, not a copy. The Update method modifies the original variable, changing its value from 5 to 100.
Why A is incorrect: 5 would be the result if a was passed by value (without ref).
Why C is incorrect: This code is perfectly valid C# and compiles without errors.
Why D is incorrect: 0 is the default value for int, but a is explicitly initialized to 5.
Reinforcement: The ref keyword enables pass-by-reference, allowing methods to modify value-type variables directly.
You now have a clear mental model of value vs reference types — from stack vs heap to pass-by-value vs pass-by-reference!