AlgoMaster Logo

Reference Types

Last Updated: May 17, 2026

8 min read

The _Value Types_ lesson covered value types, which hold their data directly. Reference types work differently. A reference-typed variable holds a small handle (a reference) that points to an object living somewhere else in memory. Assigning one reference variable to another copies the handle, not the object, so two variables can end up pointing at the same thing. That single fact explains most of the surprises beginners hit when they first work with classes, arrays, and strings in C#.

What a Reference Type Is

A reference type is a type whose values live on the heap, a region of memory the .NET runtime manages for long-lived objects. The variable you write in your code sits on the stack (or inside another object) and stores only the address of the real data, not the data itself.

Consider a tiny Product class:

Now create one:

What happens? The runtime allocates a Product object on the heap with Name = "Laptop" and Price = 999.99m. The variable p1 on the stack stores the address of that heap object. When you write p1.Name, C# follows the reference to the heap, reads the Name field, and gives it back to you.

This is the part that trips people up. The variable is not the object. The variable knows where the object is.

Stack, Heap, and Two Variables Pointing at One Object

The clearest way to see reference semantics is to assign one reference variable to another:

After these two lines, there is still only one Product object on the heap. Both p1 and p2 hold references to it. Changing the object through p2 changes what p1 sees, because they're looking at the same thing.

The diagram shows two stack slots (p1 and p2) both pointing at one heap object. There is no second Product. The assignment copied the reference, not the data behind it.

Here's what that looks like in code, with output you can run yourself:

We only assigned to p2.Name, but p1.Name also changed. That's not a bug. It's the whole point of reference semantics. Both variables point at the same object, so a mutation through either one is visible through both.

Value Type Copy vs Reference Type Copy

Put a value type next to a reference type and the difference becomes obvious. Here's the same program twice, once with int and once with Product:

int is a value type, so stockB = stockA copied the number 10 into a fresh slot. Changing stockB afterward leaves stockA alone. Two slots, two independent values.

Now the reference-type version:

Same shape of code, totally different behavior. The assignment b = a copied the reference, so a and b point at one shared object. Mutating b.Name changes the object both names see.

AspectValue TypeReference Type
StorageStack (or inline in another object)Heap, with a reference held by the variable
Copy semanticsCopies the dataCopies the reference, not the object
Default valueZero-equivalent (0, false, \0)null
Can be null?No, unless declared with ? (e.g. int?)Yes
Equality defaultCompares values field by fieldCompares references (same object?)
Examplesint, double, bool, char, decimal, structclass, string, arrays, object, delegates

Built-in Reference Types

C# ships with several reference types you'll use constantly. The big ones:

class Types

Any type you declare with the class keyword is a reference type. The Product examples above are the canonical case. When you write new Product(...), the runtime allocates a Product on the heap and gives you back a reference.

Arrays

Arrays are reference types in C#, even when their elements are value types. Declaring int[] cartItemIds = new int[3]; allocates an array object on the heap. The variable cartItemIds holds a reference to that array.

The element type (int) is a value type, but the array container itself is a reference type. So alias = cartItemIds copies the reference to the array, and writing through alias[0] updates the same array cartItemIds sees.

string

string is a reference type, but it behaves in a way that feels value-like, and that's intentional. Strings in C# are immutable. Once you create one, you can't change its characters. Any operation that looks like it modifies a string actually returns a new string.

ToUpper() did not change customerName. It returned a new string with the uppercase characters. Because strings can't be mutated, you never run into "I changed one and the other changed too" surprises with them, even though they're reference types under the hood. Strings get a dedicated section later in the course.

object

object is the root of every type in C#. Any value can be assigned to a variable of type object, including value types (more on that under Boxing below). object itself is a reference type.

Delegates

A delegate is a reference type that holds a reference to a method (or a chain of methods). You'll meet delegates properly in the _Delegates & Events_ section, but for now just know they exist and follow reference semantics like everything else here.

Passing References Around

The same rules apply when a reference travels through a method parameter. The method receives a copy of the reference, but that copy still points at the same heap object the caller has, so any mutation the method makes is visible to the caller.

ApplyDiscount got a copy of the reference, not a copy of the Product. It used that reference to change the same Product object the caller is holding, so the caller sees the new price after the call returns. This is the most common way mutation crosses function boundaries in C#.

Reassigning the parameter inside the method is a different story. That only changes the local copy of the reference, not the caller's variable:

Calling Replace(laptop) does not change what laptop points at in the caller. To replace the caller's reference itself, you'd use ref or out, which we cover in the methods chapter.

null and Reference Types

A reference variable can also point at nothing. That state is represented by the literal null.

The ? after Product opts the variable into the nullable reference type system that's on by default in modern C# (.NET 6+ templates have <Nullable>enable</Nullable> in the .csproj). It tells the compiler "this variable might be null, and I know it." Without the ?, the compiler issues a warning when you assign null to a reference variable.

null is not zero, and it's not an empty object. It means the reference doesn't point anywhere. Trying to use a member through a null reference throws NullReferenceException:

What's wrong with this code?

selected is null, so .Name has nothing to read from. The runtime throws NullReferenceException, and the compiler also issues CS8602: Dereference of a possibly null reference to warn you in advance.

Fix: Check first, or use the null-conditional operator:

selected?.Name short-circuits to null if selected is null, and ?? "(no selection)" substitutes a fallback string. The _Nullable Types_ lesson covers nullable reference types and the operators around them in detail.

Reference Equality vs Value Equality

Comparing references and comparing values are two different things, and the default behavior depends on the type.

For most classes you define, == and Equals compare references by default. Two references are "equal" only when they point at the same heap object.

a and b are two different Product objects on the heap that happen to hold the same Name. By default, that's not enough to be equal. They're not the same object, so reference equality returns False. a and c are two variables pointing at one object, so they compare equal.

object.ReferenceEquals(x, y) is the bulletproof way to ask "are these the same object?" because it ignores any equality overrides the type might have.

string is the famous exception. Strings override == and Equals to compare character values instead of references, which is almost always what you want:

Records and any class that overrides Equals and == can also implement value equality. For now, the rule of thumb is: classes you write get reference equality unless you override it, and string is the special case that ships with value equality built in.

Boxing: When a Value Type Visits the Heap

There's one place a value type sneaks onto the heap: when you assign it to a variable of a reference type, such as object. That conversion is called boxing.

The boxed object contains a snapshot of the value, not a live link back to the original stock. Changing stock afterward doesn't affect boxed, because boxing copied the value before wrapping it. Unboxing (casting the object back to int) returns yet another copy.

Boxing comes up enough that there's a full treatment in the casting and conversion chapter. The thing to remember here: a value type can end up on the heap, but only by being copied into a wrapper. The original on the stack is unaffected.

Heap Allocation and the Garbage Collector

Every new on a reference type allocates memory on the managed heap. You never free that memory by hand. The .NET runtime tracks which objects are still reachable from your code and reclaims the rest in a process called garbage collection.

That's the short version. The longer version, when GC runs, what generations are, what costs it has, and how to write GC-friendly code, lives in the _Memory Management_ section. For now, two practical takeaways:

  • Allocating reference types isn't free. It's cheap, but it's not nothing. Allocating millions of small objects in a hot path can put real pressure on the GC.
  • You never need to deallocate manually. Once nothing in your program holds a reference to an object, the GC eventually cleans it up.

Summary

  • Reference types live on the heap. A reference-typed variable holds a handle to that heap object, not the object itself.
  • Assigning one reference variable to another copies the reference, so two variables can point at the same object. Mutating through one is visible through the other.
  • Built-in reference types include class types, arrays (int[], string[]), string, object, and delegates.
  • string is a reference type but immutable, which makes it feel value-like. It also overrides == and Equals for value equality.
  • Reference types default to null. Reading a member through a null reference throws NullReferenceException. Use ?., ??, or an is not null check to guard against it.
  • == on classes you write does reference equality by default. ReferenceEquals is the bulletproof "same object?" check that ignores overrides.
  • Boxing copies a value type into a heap wrapper when it's assigned to a reference type like object. The original and the box are independent copies.
  • Heap allocations cost a bit of memory and add work for the garbage collector. The _Memory Management_ section covers the details.