What a struct is (and what it is not)
- A
structis a value type. Assigning it, returning it, or passing it to a method (by default) copies its bits. - A
classis a reference type. Assigning it copies a reference; both variables point at the same object. - A struct is not "always on the stack". Value types can live on the stack, inside other objects on the heap, inside arrays on the heap, or boxed as an object.
A quick mental model
var a = new Point(1, 2);
var b = a; // copies (b is independent)
b = new Point(9, 9);
var c = new Person("Ada");
var d = c; // copies a reference (c and d are the same object)
d.Name = "Grace";
Benefits of structs
- Fewer allocations: used as locals or fields, structs avoid separate object allocations (and the GC work that follows).
- Better locality: arrays and fields of structs store data inline, which can be more cache-friendly than chasing references.
- "No null" by default: a
Pointis always aPoint(unless you choosePoint?), which can simplify invariants and call sites. - Value semantics match the domain: coordinates, ranges, IDs, small units of measure, and "result bundles" often read better as values.
Drawbacks of structs
- Copy costs: every by-value pass/return/assignment copies the whole struct. If the struct is large, this adds up quickly.
- Surprising mutation behavior: mutating a struct often mutates a copy, not "the one you meant".
- Boxing: treating a struct as
objector as an interface can allocate and copy (and it changes what you are mutating). - The
defaultvalue exists:default(MyStruct)is always legal and does not call your constructors, so your invariants must tolerate it. - Limited OO features: no inheritance from other structs/classes, no finalizers, and interfaces can have performance implications.
Gotchas to watch for
Gotcha 1: property access returns a copy
This is the classic "why won't this compile?" moment:
public struct Point
{
public int X;
public int Y;
}
public sealed class Widget
{
public Point Location { get; set; }
}
var w = new Widget();
w.Location.X = 10; // does not compile: you're trying to mutate a copy
The fix is to mutate a local and assign it back:
var loc = w.Location;
loc.X = 10;
w.Location = loc;
This copy-on-access behavior shows up in other places too (indexers, dictionary lookups, foreach).
Gotcha 2: mutating a struct in a collection usually mutates a copy
var points = new List<Point> { new Point { X = 1, Y = 2 } };
foreach (var p in points)
{
p.X++; // compiles only if Point has mutable fields, but it mutates the foreach copy
}
Console.WriteLine(points[0].X); // still 1
If you need in-place updates, do it by index (or consider that a struct might not be the right shape):
for (var i = 0; i < points.Count; i++)
{
var p = points[i];
p.X++;
points[i] = p;
}
Gotcha 3: boxing creates a copy (and often an allocation)
var p = new Point { X = 1, Y = 2 };
object o = p; // boxes: copies p into an object
Two implications:
- It can allocate (which defeats one of the reasons you chose a struct).
- Any mutation happens on the boxed copy, not on the original
p.
Gotcha 4: default bypasses invariants
Even if you validate in a constructor, default(T) and new T() (for unconstrained generics) produce the all-zero value.
Rule of thumb: either make default a valid state, or make the type hard to misuse (typically by making it immutable and validating on creation).
Gotcha 5: mutable structs and hash-based collections do not mix
If a struct participates in hashing (dictionary key, hash set element), any mutation of fields used for Equals/GetHashCode after insertion can make the value "disappear" from the collection.
If you want to go deeper on equality, see [Fundamentals - Part 17](./Fundamentals - Part 17.md).
The usual fixes are:
- Make the struct immutable (
readonly struct), so mutating members do not exist. - If mutation is required, pass by
ref(and accept the aliasing implications).
When to choose a struct (rules of thumb)
- The type is small (commonly: a few primitive fields; avoid "bags of data" with many fields).
- The type is logically a value (no identity, no lifecycle, no shared mutable state).
- The type is immutable (prefer
readonly structorreadonly record struct). - The type is used in hot paths where allocations matter, or in large arrays where locality matters.
- You can tolerate
default(T)existing without breaking invariants.
If you are unsure, start with a class. You can change a class to a struct later less often than you think, but changing a public struct to a class is a breaking change too, so treat the choice as part of your API design.
A good baseline: small, immutable, equatable
public readonly struct Temperature : IEquatable<Temperature>
{
public decimal Celsius { get; }
public Temperature(decimal celsius) => Celsius = celsius;
public bool Equals(Temperature other) => Celsius == other.Celsius;
public override bool Equals(object? obj) => obj is Temperature t && Equals(t);
public override int GetHashCode() => Celsius.GetHashCode();
public static bool operator ==(Temperature left, Temperature right) => left.Equals(right);
public static bool operator !=(Temperature left, Temperature right) => !left.Equals(right);
}
Notes:
readonly structprevents accidental mutation and helps the compiler avoid defensive copies.IEquatable<T>avoids boxing and keeps equality fast and predictable.
Mutable structs: when they are appropriate (and when they are not)
Appropriate
Mutable structs are usually only a good idea when all of the following are true:
- The struct is short-lived (typically a local variable in a tight loop).
- The struct is not shared (not stored in a field where multiple callers observe it).
- The struct is not used as a key in hash-based collections.
- The struct is not passed around as
object/interface (to avoid boxing and copy confusion). - The performance benefit is measurable and worth the complexity.
Examples of places where mutable structs can make sense:
- An internal accumulator used inside one method to avoid repeated allocations.
- A
ref struct-style "view" over memory (likeSpan<T>), where stack-only rules prevent many misuse patterns.
Inappropriate
Avoid mutable structs when:
- The type is part of a public API that many call sites will use in unpredictable ways.
- The value will be stored in properties, collections, or captured closures, where copy semantics are easy to miss.
- The type must maintain strong invariants (mutation makes it hard to keep the value always-valid).
- You need polymorphism or identity semantics (use classes).
Final thoughts
- Prefer structs for small, immutable values with clear value semantics.
- Treat "struct vs class" as an API design decision, not a micro-optimization.
- If you must use a mutable struct, keep it local, keep it unshared, and assume callers will hit every gotcha you forgot.


