Skip to content

Reference & value types

How reference types and value types behave differently.

Every type in C# is either a reference type or a value type. The distinction affects how values are stored in memory, how they are passed to methods, how equality works, and when the garbage collector gets involved. Understanding this is fundamental to writing correct and efficient C#.

Reference types

Reference types are declared using the class, record class (or just record), interface, or delegate keywords. Arrays and string are also reference types.

When an instance is created, memory is allocated on the heap, a region of memory available to the entire program. The variable itself holds only a reference (a pointer to the heap address), not the object data.

Assigning a reference type to another variable, whether directly with = or by passing it to a method, copies the reference, not the object. Both variables then point to the same instance, so changes made through one are visible through the other.

C#
var a = new Person { Name = "Alice" };
var b = a;          // b points to the same object
b.Name = "Bob";
Console.WriteLine(a.Name); // "Bob", same object

When no references to an object remain, the .NET Garbage Collector (GC) will eventually free it. The GC uses a generational strategy: short-lived objects (generation 0) are collected frequently, while long-lived objects (generation 2) and large objects (>85 KB, on the Large Object Heap) are collected less often but still freed once unreachable.

Default value

Reference type variables default to null.

Equality

By default, == and Equals compare references, so two variables are equal only if they point to the same object. Records C# 9.0 change this by providing automatic value-based equality for reference types.

Value types

Value types are declared using the struct, record struct, or enum keywords. C# has many built-in value types: int, byte, bool, float, double, decimal, char, etc.

A value type stores its data directly in the variable. Assigning or passing it creates an independent copy, so changes to one copy do not affect the other.

C#
var a = new Point { X = 1, Y = 2 };
var b = a;          // b is a separate copy
b.X = 99;
Console.WriteLine(a.X); // 1, unaffected

Where value types live in memory

  • On the stack when they are local variables or method parameters
  • Inline inside the containing object on the heap when they are fields of a class or elements of an array
  • Captured onto the heap when referenced by a closure (lambda or anonymous method)

The GC does not directly manage value types. Stack-allocated values are freed instantly when the method returns. Values embedded in a heap object are freed when the containing object is collected.

ref struct C# 7.2 types are value types that the compiler guarantees will never be moved to the heap. They cannot be boxed, captured by closures, or used as fields of classes. Span<T> and ReadOnlySpan<T> are the most common examples.

Default value

Value type variables default to their "zero" value: 0 for numbers, false for bool, '\0' for char, and all fields zeroed for structs.

Equality

Value types use value-based equality by default. Two instances are equal if all their fields are equal.

Nullability

Value types cannot be null. To represent the absence of a value, use Nullable<T> C# 2.0 (written as T?), which wraps the value type with an additional HasValue flag.

Passing to methods

By default, both reference types and value types are passed by value in C#:

  • For a value type, the entire value is copied. The method works on its own copy.
  • For a reference type, the reference is copied. The method can mutate the object through it, but reassigning the parameter (e.g. setting it to null or a new instance) does not affect the caller's variable.

To pass by true reference so that the method can reassign the caller's variable, use the ref, out, or in modifiers.

C#
void Reassign(ref object o) { o = null; }

object x = new object();
Reassign(ref x);
Console.WriteLine(x is null); // True, caller's variable was changed

Boxing and unboxing

When a value type is assigned to a variable of type object, an interface, or another reference type, the runtime performs boxing: it allocates a new object on the heap and copies the value into it. The reverse, extracting the value from the box, is unboxing.

C#
int n = 42;
object boxed = n;         // boxing: heap allocation + copy
int unboxed = (int)boxed; // unboxing: type check + copy

Boxing is expensive in tight loops because each box is a separate heap allocation. Generics C# 2.0 are the primary tool for avoiding boxing. See the generics, variance & boxing guide for more detail.