Value types vs reference types
Predict exactly what happens when variables are copied, compared and passed to methods.
11 min read
You will learn
- Tell value types and reference types apart
- Predict the effect of assignment and method calls on each
- Compare objects by reference and by value
- Avoid accidental shared-state bugs
The most important mental model in C#
Many confusing bugs come down to one question: when I copy this variable, do I get a new thing, or another name for the same thing? The answer depends on whether the type is a value type or a reference type.
| Value types | Reference types |
|---|---|
int, double, bool, char, decimal | class instances |
DateTime, TimeSpan, Guid | record (class) instances |
struct, record struct, enum | arrays, List<T>, Dictionary<K,V> |
tuples like (int, string) | string, object, delegates |
Picture it
Value type Reference type
int a = 5; var p = new Person("Ada");
┌─────┐ ┌─────┐ ┌──────────────┐
│a: 5 │ │p: ●─┼──────▶│ Person │
└─────┘ └─────┘ │ Name = "Ada" │
└──────────────┘
int b = a; var q = p;
┌─────┐ ┌─────┐ ┌─────┐
│a: 5 │ │b: 5 │ two copies │p: ●─┼──┐ ┌──────────────┐
└─────┘ └─────┘ └─────┘ ├───▶│ Person │
┌─────┐ │ │ Name = "Ada" │
│q: ●─┼──┘ └──────────────┘
└─────┘ one object, two references- A value-type variable contains the data itself. Assignment copies the data.
- A reference-type variable contains a reference (an address). Assignment copies the reference, so both variables point to the same object.
var p = new Person("Ada");
var q = p;
q.Name = "Grace";
Console.WriteLine(p.Name); // Grace — p and q are the same objectStep through it yourself — including what happens when a method is called and when the last reference disappears:
Stack & heap
References: two variables, one object
Code
1int age = 36;2var ada = new Person("Ada");3var alias = ada;4alias.Name = "Grace";5Rename(ada);6ada = null;7alias = null;89void Rename(Person p)10{11 p.Name = "Linus";12}
Stack
Main()
- intage36
Heap
(empty)
age is an int — a value type. The number 36 is stored directly inside the method's stack frame.Passing to methods
Arguments are always copied (unless you use ref). What gets copied is the variable's content — the value, or the reference:
void Birthday(Person person) => person.Age++; // modifies the shared object ✅ visible to caller
void Replace(Person person) => person = new Person("Bob"); // re-points the local copy ❌ invisible
void Increment(int n) => n++; // modifies a copy ❌ invisibleThe rule of thumb: through a reference you can modify the object, but you can't make the caller's variable point somewhere else.
Arrays and lists are references too
This catches everyone at least once:
int[] original = { 1, 2, 3 };
int[] copy = original; // NOT a copy — the same array
copy[0] = 99;
Console.WriteLine(original[0]); // 99
int[] realCopy = (int[])original.Clone(); // or original.ToArray() with LINQShared state bugs
If a method stores a list it was given and someone else modifies that list later, the method sees the change. When you need independence, make a copy: new List<T>(source) or source.ToList().
Equality: same object, or same value?
var a = new Person("Ada");
var b = new Person("Ada");
Console.WriteLine(a == b); // False — different objects (classes default to reference equality)
Console.WriteLine(ReferenceEquals(a, a)); // True
Console.WriteLine("hi" == "h" + "i"); // True — string compares text
Console.WriteLine(new Point(1, 2) == new Point(1, 2)); // True for records and record structsStrings: reference type, value behavior
string is a class, but it's immutable: no method can change a string's contents. So even though variables share references, nobody can modify the shared string, and it behaves like a value. Its == compares text.
Where they live (briefly)
You'll often hear "value types live on the stack, reference types on the heap". It's a useful first approximation: objects are allocated on the managed heap and cleaned up by the garbage collector, while local value-type variables usually sit on the stack. Track 5 goes into the details — including the exceptions to that rule.
Try it
C# runtime not loaded
Key takeaways
- Value types (numbers,
bool, structs, enums) are copied on assignment. - Reference types (classes, records, arrays, lists, strings) share one object between copies.
- Methods can modify objects passed to them, but not the caller's variables (without
ref). - Classes compare by reference by default; records, structs and strings compare by value.
Exercise
ResetScores should set every element of the given array to 0 (the caller must see the change). CopyAndDouble returns a NEW array with every value doubled, leaving the input alone. Wallet is a struct, so AddCash receives a copy — return the updated wallet so callers can use it.
C# runtime not loaded
Hint 1
An array is a reference type — the parameter points to the same array as the caller's variable, so writing scores[i] = 0 changes it for everyone.
Hint 2
For CopyAndDouble, create var result = new int[source.Length]; and fill it.
Hint 3
AddCash: wallet.Cash += amount; return wallet; — the change only reaches the caller because you return the copy.
Show solution
var scores = new[] { 5, 8, 2 };
ResetScores(scores);
Console.WriteLine(string.Join(", ", scores));
void ResetScores(int[] scores)
{
for (int i = 0; i < scores.Length; i++)
{
scores[i] = 0;
}
}
int[] CopyAndDouble(int[] source)
{
var result = new int[source.Length];
for (int i = 0; i < source.Length; i++)
{
result[i] = source[i] * 2;
}
return result;
}
Wallet AddCash(Wallet wallet, decimal amount)
{
wallet.Cash += amount;
return wallet;
}
struct Wallet
{
public decimal Cash;
}Quiz
4 questions · 3 correct to pass1.`void Rename(Person p) { p.Name = "Bob"; }` — Person is a class. After `Rename(ada)`, what is ada.Name?
2.`void Replace(Person p) { p = new Person("Bob"); }` — after `Replace(ada)`, what is ada.Name?
3.Which type is a value type?
4.How does `==` compare two variables of a class type that doesn't override equality?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)