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.

csharp
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, and union C# 15.0 declarations are also structs. 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.

csharp
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 ​

  • Commonly on the stack when they are local variables or method parameters, though the runtime and JIT may hoist or otherwise rewrite storage in optimized code paths
  • 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. Since C# 13 they can implement interfaces and be used as type arguments for generics that declare allows ref struct C# 13.0, but they still cannot be converted to an interface type because that would box them.

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 for Equals by default: two instances are equal if all their fields are equal. The default ValueType.Equals can fall back to reflection, which is slow, so override it (or implement IEquatable<T>) for structs you compare often. The == operator is not available on a struct unless you overload it; record structs C# 10.0 generate Equals, == and != for you.

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 or out modifiers. The in C# 7.2 and ref readonly C# 12.0 modifiers also pass by reference but read-only, so they avoid copying large structs without letting the method reassign the variable.

csharp
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.

csharp
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.

Last updated:

Powered by Intracia