Value Types vs Reference Types (C# Deep Dive)
Overview
Understanding the fundamental difference between value types and reference types is critical for writing efficient C# code and acing senior-level interviews.
Core Differences
| Aspect | Value Types | Reference Types |
|---|---|---|
| Storage | Stack (or inline in parent object) | Heap |
| Assignment | Copies the entire value | Copies the reference |
| Default value | Zero/null fields | null |
| Inheritance | Cannot inherit (except from ValueType) | Can inherit |
| Boxing | Boxed when cast to object | No boxing needed |
| Examples | int, struct, enum, record struct | class, record class, string, arrays |
Memory Layout
// Value type - stored on stack
public struct Point
{
public int X; // Inline in struct
public int Y; // Inline in struct
}
Point p = new Point { X = 10, Y = 20 };
// Entire struct (8 bytes) is on stack
// Reference type - object on heap, reference on stack
public class Person
{
public string Name; // Reference to string on heap
public int Age; // Value inline in object
}
Person person = new Person { Name = "Alice", Age = 30 };
// 'person' variable holds address (8 bytes on stack)
// Person object itself is on heap
Boxing and Unboxing
What is Boxing?
Boxing is converting a value type to object or interface type, which creates a heap-allocated copy.
int value = 42; // Value type on stack
object boxed = value; // Boxing: copy to heap
int unboxed = (int)boxed; // Unboxing: copy back to stack
Performance Impact
// ❌ BAD: Boxing in loops
List<object> list = new();
for (int i = 0; i < 1000; i++)
{
list.Add(i); // Boxing on each iteration! 1000 heap allocations
}
// ✅ GOOD: No boxing
List<int> list = new();
for (int i = 0; i < 1000; i++)
{
list.Add(i); // No boxing, stays on stack
}
Surprising Boxing Scenarios
// Boxing when calling interface methods on structs
public struct Counter : IComparable<Counter>
{
public int Value;
public int CompareTo(Counter other) => Value.CompareTo(other.Value);
}
Counter c1 = new Counter { Value = 5 };
// ❌ Boxing: struct cast to interface
IComparable<Counter> comparable = c1; // Boxes!
comparable.CompareTo(c1);
// ✅ No boxing: direct call
c1.CompareTo(c1); // No boxing
struct vs class
When to Use struct
// ✅ Good struct candidates: small, immutable, value semantics
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// ❌ Bad struct: large, mutable, reference semantics
public struct HugeStruct // Don't do this!
{
public byte[] Data; // 1MB array
public List<string> Items;
public int Counter;
}
Struct Guidelines
✅ Use struct when:
- Size ≤ 16 bytes
- Logically represents a single value
- Immutable
- Short-lived
- Will not be boxed frequently
❌ Use class when:
- Represents complex entities
- Needs inheritance
- Large data structures
- Long-lived objects
- Needs reference semantics
record struct vs record class
record struct (C# 10+)
// Immutable value type with value equality
public readonly record struct Point3D(double X, double Y, double Z);
var p1 = new Point3D(1, 2, 3);
var p2 = new Point3D(1, 2, 3);
Console.WriteLine(p1 == p2); // True - value equality
record class (C# 9+)
// Immutable reference type with value equality
public record Person(string Name, int Age);
var person1 = new Person("Alice", 30);
var person2 = new Person("Alice", 30);
Console.WriteLine(person1 == person2); // True - value equality
Console.WriteLine(ReferenceEquals(person1, person2)); // False - different objects
Comparison Table
| Feature | record struct | record class | struct | class |
|---|---|---|---|---|
| Storage | Stack/Inline | Heap | Stack/Inline | Heap |
| Equality | Value | Value | Default (reference-like) | Reference |
| Mutability | Can be mutable | Can be mutable | Can be mutable | Can be mutable |
with expressions | ✅ | ✅ | ❌ | ❌ |
| Inheritance | ❌ | ✅ | ❌ | ✅ |
with Expressions
// record struct
public readonly record struct Config(int Timeout, bool EnableCache);
var config1 = new Config(30, true);
var config2 = config1 with { Timeout = 60 }; // Creates new instance
// record class
public record User(int Id, string Name, string Email);
var user1 = new User(1, "Alice", "alice@example.com");
var user2 = user1 with { Email = "newemail@example.com" }; // Creates new instance
Pass by Value vs Pass by Reference
Value Type Behavior
void ModifyValue(int x)
{
x = 100; // Modifies local copy only
}
int num = 42;
ModifyValue(num);
Console.WriteLine(num); // Still 42
Reference Type Behavior
void ModifyReference(Person person)
{
person.Name = "Bob"; // Modifies the object on heap
}
var person = new Person { Name = "Alice" };
ModifyReference(person);
Console.WriteLine(person.Name); // "Bob" - changed!
ref, in, and out
// ref: pass by reference, can read and modify
void Increment(ref int value)
{
value++;
}
int num = 10;
Increment(ref num);
Console.WriteLine(num); // 11
// in: pass by reference, read-only (optimization for large structs)
void ProcessLarge(in LargeStruct data)
{
// data = new LargeStruct(); // ❌ Compiler error
Console.WriteLine(data.Value); // ✅ Can read
}
// out: pass by reference, must be initialized in method
bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
ref struct (Stack-only Types)
// ref struct can ONLY exist on stack - perfect for high-performance scenarios
public ref struct SpanWrapper\<T\>
{
private Span\<T\> _span;
public SpanWrapper(Span\<T\> span)
{
_span = span;
}
}
// ❌ Cannot do these with ref struct:
// - Store in class field
// - Box to object
// - Use in async methods
// - Store in regular struct
// - Use with Task or any heap-allocated generic
Common Interview Questions
Q: What happens when you pass a struct to a method?
A: The entire struct is copied (unless using ref, in, or out). For large structs, this can be expensive. That's why structs should be small.
Q: Why is string a reference type but seems immutable?
A: String is a reference type stored on the heap, but it's designed to be immutable for thread-safety and optimization. Operations that seem to modify strings actually create new string instances.
Q: When does boxing occur and why is it bad?
A: Boxing occurs when a value type is cast to object or interface type. It's bad because it causes heap allocation, GC pressure, and performance overhead. It happens in collections like ArrayList, calling interface methods on structs, and concatenating value types with strings.
Q: What's the difference between record struct and readonly struct?
A: A record struct automatically implements value equality, ToString, and supports with expressions. A readonly struct ensures all fields are readonly but doesn't get these automatic implementations. You can combine them: readonly record struct.
Q: Can a struct contain a reference type field?
A: Yes! A struct can contain reference type fields. The struct itself is a value type (copied on assignment), but its reference fields point to heap objects. Example:
public struct Container
{
public int Value; // Value type field
public string Text; // Reference type field
}
var c1 = new Container { Value = 1, Text = "Hello" };
var c2 = c1; // Struct is copied, but both point to same "Hello" string
Practice Exercises
- Implement a
Moneystruct with proper value equality - Demonstrate boxing behavior with different scenarios
- Create a
record structfor a 3D vector with mathematical operations - Show the memory difference between struct and class with a profiler
- Implement a high-performance algorithm using
ref struct
Performance Tips
// ✅ Use readonly struct to enable compiler optimizations
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
// ✅ Use 'in' parameter for large structs to avoid copying
public double Calculate(in Matrix4x4 matrix)
{
// Read matrix without copying
return matrix.M11 + matrix.M22;
}
// ✅ Use ref return for modifying struct in place
ref struct GetElement(ref Span<int> span, int index)
{
return ref span[index];
}
Related Concepts
- Span<T> and Memory Management
- Garbage Collection
- Primary Constructors