C# Value Types vs Reference Types: A Complete Guide
When you assign one variable to another in C#, are you copying the data itself or copying a way to reach that data? The answer depends on whether the type is a value type or a reference type.
This distinction affects assignments, method calls, equality, mutation, performance, and API design. It is also a common source of bugsāand a frequent topic in senior .NET interviews.
Value Types vs Reference Types at a Glance
| Behavior | Value type | Reference type |
|---|---|---|
| Variable contains | The value itself | A reference to an object |
| Assignment copies | The value | The reference |
| Default value | Usually zero-like (0, false, and so on) |
null |
| Common examples | int, bool, enum, struct, record struct |
class, record class, string, arrays, delegates |
| Typical design use | Small logical values | Objects with identity or a lifecycle |
The Core Difference
A variable of a value type contains its value directly. Assigning it to another variable copies that value.
A variable of a reference type contains a reference to an object. Assigning it to another variable copies the reference, so both variables can refer to the same object.
int firstNumber = 10;
int secondNumber = firstNumber;
secondNumber = 20;
Console.WriteLine(firstNumber); // 10
Console.WriteLine(secondNumber); // 20Code language: C# (cs)
int is a value type. secondNumber receives an independent copy, so changing it does not affect firstNumber.
Now compare that with a class:
var firstPerson = new Person { Name = "Alice" };
var secondPerson = firstPerson;
secondPerson.Name = "Bob";
Console.WriteLine(firstPerson.Name); // Bob
Console.WriteLine(secondPerson.Name); // Bob
public sealed class Person
{
public string Name { get; set; } = string.Empty;
}Code language: C# (cs)
Person is a reference type. The assignment copies the reference, not the object. Both variables point to the same Person, so a mutation made through either variable is visible through the other.
Which C# Types Belong to Each Group?
Common value types include:
- Integral numeric types such as
int,long, andbyte - Floating-point and decimal types such as
doubleanddecimal boolandcharenumtypesstructandrecord structtypes- Nullable value types such as
int?
Common reference types include:
class and record class typesstringobjectstring deserves special attention. It is a reference type, but it is immutable. Operations that appear to change a string produce a new string instead of modifying the existing object.
string first = "hello";
string second = first;
second = second.ToUpperInvariant();
Console.WriteLine(first); // hello
Console.WriteLine(second); // HELLOCode language: C# (cs)
This can look like value-type behavior, but it is the result of immutabilityānot because string is a value type.
Assignment Copies the Variable's Value
A precise way to describe assignment is: C# copies the value stored in the source variable.
This wording avoids the misleading idea that reference-type assignment performs no copy. A copy does occur; it is the reference that is copied.
Passing Values to Methods
Parameters are passed by value by default in C#. This rule applies to both value types and reference types.
Passing a value type
static void Increment(int number)
{
number++;
}
int count = 5;
Increment(count);
Console.WriteLine(count); // 5Code language: C# (cs)
The method receives a copy of count. Reassigning or changing that local copy does not change the caller's variable.
Passing a reference type
static void Rename(Person person)
{
person.Name = "Bob";
}
var person = new Person { Name = "Alice" };
Rename(person);
Console.WriteLine(person.Name); // BobCode language: C# (cs)
The method receives a copy of the reference. That copied reference still points to the same object, so the method can mutate the object.
However, assigning the parameter to another object does not replace the caller's reference:
static void Replace(Person person)
{
person = new Person { Name = "Charlie" };
}
var person = new Person { Name = "Alice" };
Replace(person);
Console.WriteLine(person.Name); // AliceCode language: C# (cs)
The method changes only its local copy of the reference.
Using ref, out, and in
Parameter modifiers change the default behavior:
ref passes a variable by reference and allows the method to read or replace it.out passes a variable by reference and requires the method to assign it.in passes a variable by readonly reference, helping avoid a copy for some large structs.static void Replace(ref Person person)
{
person = new Person { Name = "Charlie" };
}
var person = new Person { Name = "Alice" };
Replace(ref person);
Console.WriteLine(person.Name); // CharlieCode language: C# (cs)
Here, ref gives the method access to the caller's variable, so the method can replace the stored reference.
Stack and Heap: A Useful but Incomplete Explanation
Value types are often described as āstored on the stack,ā while reference types are described as āstored on the heap.ā That shortcut is not a reliable definition.
Storage depends on context:
The important semantic distinction is copying:
The runtime's actual storage strategy is an implementation detail unless you are investigating performance or interoperability.
Value Types Inside Reference Types
A class can contain value-type fields. Those values are stored as part of the class instance.
public sealed class Order
{
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}Code language: C# (cs)
Order is a reference type, while Quantity and UnitPrice are value types. Copying an Order variable copies the reference to the same Order; it does not independently copy its fields.
Reference Types Inside Value Types
A struct can contain reference-type fields. Copying the struct copies each field, including any references.
public struct Team
{
public string[] Members { get; set; }
}
var members = new[] { "Alice", "Bob" };
var firstTeam = new Team { Members = members };
var secondTeam = firstTeam;
secondTeam.Members[0] = "Charlie";
Console.WriteLine(firstTeam.Members[0]); // CharlieCode language: C# (cs)
firstTeam and secondTeam are separate struct values, but their Members fields contain references to the same array. This is a shallow copy.
This is one reason mutable structsāespecially structs containing mutable reference typesācan be surprising.
Equality Is a Separate Concern
Value versus reference semantics does not completely determine how == behaves. Each type can define its own equality rules.
For many value types, equality compares contained values:
int a = 10;
int b = 10;
Console.WriteLine(a == b); // TrueCode language: C# (cs)
For ordinary classes that do not override equality, equality typically uses object identity:
var first = new Person { Name = "Alice" };
var second = new Person { Name = "Alice" };
Console.WriteLine(first == second); // FalseCode language: C# (cs)
The two objects contain the same text but are different instances.
Records provide value-based equality by default, even though a record class is a reference type:
public sealed record Customer(string Name);
var first = new Customer("Alice");
var second = new Customer("Alice");
Console.WriteLine(first == second); // TrueCode language: C# (cs)
This is another reason not to equate āreference typeā with āalways compared by reference.ā
Boxing and Unboxing
Boxing converts a value type to object or to an interface it implements. The runtime creates an object containing a copy of the value.
int number = 42;
object boxed = number; // Boxing
number = 100;
Console.WriteLine(boxed); // 42Code language: C# (cs)
Unboxing extracts the value from the boxed object and requires a compatible value type:
object boxed = 42;
int number = (int)boxed; // UnboxingCode language: C# (cs)
Boxing creates an allocation and adds conversion overhead. Generics usually avoid unnecessary boxing:
var numbers = new List<int>(); // Stores int values without boxing each item
numbers.Add(42);Code language: C# (cs)
Avoid boxing in hot paths when measurement shows it matters, but do not sacrifice clarity for speculative micro-optimization.
Nullable Value Types and Nullable References
int? is shorthand for Nullable<int>, which is itself a value type. It represents either an int value or no value.
int? age = null;
age = 30;Code language: C# (cs)
Nullable reference type annotations, such as string?, are different. They are compiler-assisted annotations that help detect possible null usage; they do not create a new runtime wrapper type.
string? middleName = null;Code language: C# (cs)
The two features both express possible absence, but their runtime models are different.
Common Mistakes
Assuming a method cannot mutate an object
Passing a reference type by value prevents the method from replacing the caller's variable, but it does not prevent mutation of the referenced object.
Use immutable models, readonly interfaces, or defensive copies when callers must not observe changes.
Assuming every struct copy is completely independent
A struct copy is independent at the field level, but reference-type fields may still point to shared mutable objects.
Using large mutable structs
Large structs can be expensive to copy, while mutable structs can behave unexpectedly when returned from properties or used in collections. Prefer small, immutable structs that represent a single value.
Relying on the stack-versus-heap shortcut
It can produce incorrect conclusions about captured variables, fields, arrays, boxing, and runtime optimizations. Reason from language semantics first.
Confusing immutability with value semantics
string is immutable but remains a reference type. A type's mutability, storage, copying behavior, and equality behavior are related design concerns, but they are not the same thing.
Choosing Between a Class and a Struct
Consider a struct when the type:
Examples include coordinates, measurements, dates, ranges, and identifiers.
Consider a class when the type:
These are guidelines, not hard size-based rules. Measure performance-sensitive code and choose the model that best expresses the domain.
A Real-World Example
Money is often a good candidate for an immutable value type because two amounts with the same currency and quantity represent the same logical value.
public readonly record struct Money(decimal Amount, string Currency)
{
public Money Add(Money other)
{
if (Currency != other.Currency)
{
throw new InvalidOperationException(
"Cannot add amounts with different currencies.");
}
return new Money(Amount + other.Amount, Currency);
}
}
var subtotal = new Money(100m, "CAD");
var tax = new Money(13m, "CAD");
var total = subtotal.Add(tax);
Console.WriteLine(total); // Money { Amount = 113, Currency = CAD }Code language: C# (cs)
An Order, in contrast, is usually modeled as a class because it has identity and a lifecycle. Its status and contents can change while it remains the same order.
Senior Developer Interview Questions
Are reference types passed by reference in C#?
Not by default. A reference-type variable is passed by value, which copies the reference. The callee can mutate the referenced object but cannot replace the caller's variable unless the parameter uses ref or out.
Are value types always allocated on the stack?
No. Their location depends on context and runtime optimization. A value-type field can live inside a heap-allocated object, and boxing creates a heap object containing a copied value.
What happens when a struct contains a reference-type field?
Copying the struct copies the reference field. The struct instances are distinct, but they may refer to the same underlying object.
Is string a value type?
No. It is an immutable reference type. Its immutability makes many operations appear value-like.
How do records relate to value and reference types?
A record class is a reference type with value-based equality by default. A record struct is a value type with value-based equality by default.
Why can boxing affect performance?
Boxing generally creates a managed allocation and copies a value into it. Repeated boxing in frequently executed code increases allocation and garbage-collection pressure.
Key Takeaways
Understanding these rules makes C# code easier to reason about and helps prevent subtle bugs involving shared state, method calls, and copying.