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.
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.
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.
Memory is not infinite, and how you use it affects performance. If every piece of data were stored the same way, you'd either:
Real programs need both:
.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:
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.
int number = 42;
The runtime allocates exactly 4 bytes on the stack. The variable number contains the value 42 directly.
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.
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.
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.
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.
// 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. p2 is independent.Shape s2 = s1 — copies the reference. Both point to the same heap object.p2.X has no effect on p1.s2.Name changes the shared object, so s1.Name reflects the change.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:
Money is a value type (struct) — cheap to copy, immutable, and thread-safe. Perfect for currency values.Order is a reference type (class) — it contains a list of items and is passed around the system efficiently by reference.ApplyDiscount method modifies the original order because Order is a reference type.Order were a struct, every method call would copy the entire order — including all items — which would be very slow.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.
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.
struct vs classMany 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 |
These are different concepts:
ref, out, or in). 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!
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
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.
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.
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
class for everythingWrong: 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.
struct) when:Point, Money, DateTime, Guid.class) when:Customer, Order, DbContext, Service.record struct gives you value-type semantics with value-based equality.record class gives you reference-type semantics with value-based equality.structclass
struct) store data directly — assignment copies the data.class) store a reference to data — assignment copies the reference.struct for small, immutable data and class for large, mutable, shared data.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#?
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);
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?
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#?
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);
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!
dotnetmadeeasy.com — Learn C# and .NET, the right way.