Skip to main content
CodeOath
← All posts

C#64 min total · 17 parts

C# Fundamentals: Value Types, Reference Types, Boxing, and the Type System

Part 15 of 17 · ~2 min

readonly struct and ref struct

The advice so far has been "keep your structs immutable, and be careful with them on the stack." Two keywords take that advice out of the realm of good intentions and put the compiler in charge of it instead.

readonly struct StockSnapshot
{
    public readonly string Sku;
    public readonly int OnHand;
    public readonly DateTime CapturedAt;

    public StockSnapshot(string sku, int onHand, DateTime capturedAt)
    {
        Sku = sku; OnHand = onHand; CapturedAt = capturedAt;
    }
    // any method that would assign a field here is a compile error — there is no way to write Adjust()
}

Mark the struct readonly and its fields all become implicitly read-only; try to write a method body that would touch any of them and the build simply fails. The previous chapter's whole category of bug — a mutation quietly landing on a throwaway copy — has nowhere left to happen, because there's no longer any method capable of mutating anything in the first place. There's a second, less obvious payoff too: normally, whenever the compiler hands a struct member off through something it can't fully trust not to have side effects — an in parameter, say — it has to silently generate a defensive copy first, just in case. A readonly struct gives the compiler a guarantee it can actually rely on, so that extra copy gets skipped entirely. Small, easy to miss, and it's a saving a plain mutable struct never gets.

ref struct BarcodeScanBuffer
{
    public Span<byte> RawBytes;
}

ref struct is the modifier quietly doing the work behind Span<T> and ReadOnlySpan<T>, and what it buys you is a struct the compiler will never let escape the stack. Tested directly rather than assumed: stashing a BarcodeScanBuffer as a field on an ordinary class refuses to build, with CS8345: Field or auto-implemented property cannot be of type 'BarcodeScanBuffer' unless it is an instance member of a ref struct. Boxing it is off the table too, a lambda can't close over it, and it can't survive on either side of an await. Every one of those restrictions exists for the same reason: they're what makes it safe for BarcodeScanBuffer to be a direct, zero-copy window onto bytes the scanner hardware just handed over, with the compiler itself guaranteeing there's no way for that window to outlive the memory sitting behind it.