Structs
C# struct is a value type stored on the stack (or inline in a containing object) — copied on assignment, no allocation, no GC pressure. Use them for small, immutable values: Point, Money, Duration. Modern variants (record struct, ref struct) cover specialised needs.
Value vs reference, immutable, ref struct
EXAMPLE
// 1) Basic struct
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public double DistanceTo(Point other)
{
var dx = X - other.X;
var dy = Y - other.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
}
var p1 = new Point(0, 0);
var p2 = new Point(3, 4);
Console.WriteLine(p1.DistanceTo(p2)); // 5
// 2) Value semantics — copied on assignment
var a = new Point(1, 2);
var b = a; // b is a COPY
b.X = 99; // would error — X is read-only
// If mutable:
// b.X = 99 doesn't affect a
// 3) struct vs class
// struct class
// Value type Reference type
// Stack/inline Heap
// Copy on assign Reference on assign
// Default = zeros Default = null
// Can't inherit (only interfaces) Can inherit
// No finaliser Can have ~ClassName()
// Small (<= 16 bytes typical) sweet spot — bigger, prefer class
// 4) readonly struct — fully immutable
public readonly struct Money
{
public long Cents { get; }
public string Currency { get; }
public Money(long cents, string currency) { Cents = cents; Currency = currency; }
public Money Add(Money other) {
if (Currency != other.Currency) throw new InvalidOperationException();
return new Money(Cents + other.Cents, Currency);
}
}
// readonly enforces immutability + lets compiler optimise (no defensive copies on method calls).
// 5) record struct — concise value type with auto equals/hashcode
public readonly record struct Point2(int X, int Y);
var p = new Point2(3, 4);
var p2 = p with { Y = 5 }; // non-destructive mutation
Console.WriteLine(p == new Point2(3, 4)); // true — value equality
// 6) When to choose struct over class
// ✓ Small (16-24 bytes typical)
// ✓ Immutable (or rarely mutated)
// ✓ Frequently created/destroyed (allocation pressure relief)
// ✓ Value semantics make sense (Money, Vector, Color)
// ✗ Large fields → copying is expensive
// ✗ Inheritance hierarchy needed → use class
// ✗ Identity matters → use class
// 7) ref struct — stack-only, special use cases
// Cannot be on heap, in async methods, or captured by lambdas.
// Used for high-performance buffers like Span<T>.
public ref struct Buffer
{
public Span<byte> Data;
}
// Span<T> is the canonical ref struct — zero-copy access to memory.
Span<int> nums = stackalloc int[10];
nums[0] = 1;
// 8) Auto-boxing pitfall
object obj = p1; // BOXING — copies struct to heap
int hash = ((Point)obj).X; // UNBOXING — copies back to stack
// Boxing kills the perf advantage of structs. Watch for:
// • Storing struct in object/dynamic
// • Passing struct as interface type
// • Adding struct to ArrayList (old API)
// 9) Generic constraints
public class Cache<T> where T : struct
{
private Dictionary<string, T> _map = new();
}
// 'where T : struct' — only value types; 'where T : class' — only reference types.
// 10) Mutable structs — use with care
public struct Counter
{
public int Count;
public void Increment() => Count++;
}
// foreach + struct iteration → COPIES; modifications won't persist:
var list = new List<Counter> { new Counter() };
foreach (var c in list) c.Increment(); // doesn't affect list elements!
// Use class or readonly struct + return new for safety.
// 11) Performance considerations
// • Pass structs by 'in' for read-only by reference (no copy):
public int Process(in BigStruct data) { return data.Field; }
// • Pass by 'ref' for mutation
public void Modify(ref MyStruct data) { data.Field = 5; }
// • out for output parameters
public bool TryParse(string s, out Point p) { /* ... */ p = default; return false; }
// 12) Default values
// Structs have an implicit parameterless constructor that zeros all fields.
var p = new Point(); // Point(0, 0)
Point p2 = default; // same
// Struct constructors must assign every field (or use default).
// 13) Nullable structs
int? maybe = null;
int? value = 42;
if (value.HasValue) Console.WriteLine(value.Value);
int v = value ?? 0;
// Nullable<T> wraps a struct; allows null + value.
// 14) System struct examples
// • int, double, bool, char — built-in primitives
// • DateTime, DateTimeOffset, TimeSpan
// • Guid
// • KeyValuePair<TKey, TValue>
// • DateOnly, TimeOnly (.NET 6+)
// • Span<T>, ReadOnlySpan<T>, Memory<T>
// 15) Common bugs
// • Mutable struct in foreach — modifications lost; use class or readonly struct
// • Struct > 16-24 bytes — copying expensive; use class instead
// • Boxing inside hot loops — invisible perf killer; profile with allocation tracker
// • Using struct where identity matters (e.g. cache lookups by reference)
// • Forgetting to mark struct readonly when fields are immutable → defensive copy on call
// • Storing huge structs in arrays → memory bloat (no shared references)
// • Using struct as dictionary KEY without overriding equals → slow + wrong behaviour
// • ref struct in async method → compile error; only works in synchronous code
// • Returning struct that contains references — references survive but value copy obscures lifecycle
// • Wrong constructor — not assigning all fields fails (until C# 11 'required' members)
Why it matters
struct is C#’s value type: copied on assignment, no GC pressure, stack-allocated. Use for small (<24 bytes), immutable values — Point, Money, Duration. Reach for readonly record struct for auto equality + concise syntax, ref struct for Span<T>-style buffers, and watch for boxing when a struct is treated as object or an interface.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
public struct Point { public int X, Y; }
var p = new Point { X = 1, Y = 2 }; // value type
Try it Yourself »
Discussion
Loading…