.NET Academy

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 typesReference types
int, double, bool, char, decimalclass instances
DateTime, TimeSpan, Guidrecord (class) instances
struct, record struct, enumarrays, 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 object

Step through it yourself — including what happens when a method is called and when the last reference disappears:

Interactive

Stack & heap

References: two variables, one object

1/9

Code

1int age = 36;
2var ada = new Person("Ada");
3var alias = ada;
4alias.Name = "Grace";
5Rename(ada);
6ada = null;
7alias = null;
8
9void 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 ❌ invisible

The 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 LINQ

Shared 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 structs

Strings: 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

Program.cs
Loading editor…

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.

Program.cs
Loading editor…

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 pass
  1. 1.`void Rename(Person p) { p.Name = "Bob"; }` — Person is a class. After `Rename(ada)`, what is ada.Name?

  2. 2.`void Replace(Person p) { p = new Person("Bob"); }` — after `Replace(ada)`, what is ada.Name?

  3. 3.Which type is a value type?

  4. 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)