readonly and ref struct

readonly struct promises a struct never changes; ref struct keeps a type off the heap, which is how Span<T> works.

Syntaxpublic readonly struct Name { } // and: public ref struct Name { }

Two modifiers change what a struct is allowed to do. They exist for different reasons.

readonly struct

A readonly struct promises the value never changes after it is built. The compiler enforces that: every field must carry the readonly modifier, and every property must be get-only or init-only. Every instance member except a constructor is then implicitly readonly too.

This is not only documentation. When you pass a plain struct to a method by in reference, the compiler sometimes makes a hidden copy to protect the caller from a member that might mutate it. A readonly struct cannot mutate, so that copy is skipped. If the whole type cannot be immutable, put readonly on the individual members that do not change.

ref struct

Memory comes in two areas. The stack holds a method's own local variables and is discarded when the method returns. The heap holds objects that outlive the method that made them. A ref struct is a struct the compiler refuses to let reach the heap.

So a ref struct cannot be an array element, cannot be a field of a class or of a normal struct, cannot be boxed to object, and cannot be captured by a lambda or a local function. In exchange it may hold a ref field: a reference straight into memory it does not own. That is exactly how Span<T> and ReadOnlySpan<T> are built, and why they are the standard way to read part of an array without copying it.

C# 13 loosened three of the rules: a ref struct may implement an interface, may be a type argument where the type parameter says allows ref struct, and may appear as a local in an iterator or an async method as long as it is not in the same block as a yield return or an await.

Example

Example · csharp
Rectangle card = new Rectangle(4, 5);
Console.WriteLine(card);
Console.WriteLine(card.Area);

// Span<T> is a ref struct: a window onto memory it does not own.
int[] numbers = [1, 2, 3, 4, 5];
Span<int> middle = numbers.AsSpan(1, 3);   // the elements 2, 3, 4
middle[0] = 99;                            // writes straight into numbers

Console.WriteLine(numbers[1]);
Console.WriteLine(Total(middle));          // Span converts to ReadOnlySpan

static int Total(ReadOnlySpan<int> values)
{
    int sum = 0;
    foreach (int value in values)
    {
        sum += value;
    }

    return sum;
}

readonly struct Rectangle
{
    public Rectangle(int width, int height)
    {
        Width = width;
        Height = height;
    }

    public int Width { get; }
    public int Height { get; }

    // Implicitly readonly, like every member of a readonly struct.
    public int Area => Width * Height;

    public override string ToString() => $"{Width}x{Height}";
}

// Output:
// 4x5
// 20
// 99
// 106

When to use it

  • Pass a large struct by in reference without the compiler inserting a defensive copy on every member call.
  • Parse a section of a byte or char array in place, with no substring or array copy.
  • Guarantee that a value used as a Dictionary key cannot be mutated after insertion.
  • Write a generic helper that can accept Span<T> as its type argument.

More examples

readonly on single members, not the whole type

Accumulator has to change to be useful, so the whole type cannot be readonly. Marking the members that do not change still earns the optimisation on those members, and it documents intent. The catch worth knowing: a readonly member that calls a non-readonly member forces the compiler to copy the struct first, so the call runs against a copy and your change is silently lost. If a readonly method stops having an effect, that is why.

Example · csharp
Accumulator acc = new Accumulator();
acc.Add(4);
acc.Add(6);

Console.WriteLine(acc.Total);
Console.WriteLine(acc.Describe());

struct Accumulator
{
    private int _total;

    // Not readonly: it changes _total, so it cannot be.
    public void Add(int amount)
    {
        _total += amount;
    }

    // readonly: a promise to touch no field. The compiler checks it.
    public readonly int Total => _total;

    public readonly string Describe() => $"total {_total}";
}

// Output:
// 10
// total 10

A ref struct that reads through a span

This is the usual reason to write a ref struct of your own: the type needs a Span field, and a Span field is only legal inside another ref struct. Reader walks an array without copying any of it. The restrictions follow immediately: you cannot store a Reader in a class field, cannot put it in a List, and cannot hold one across an await. If you need any of those, copy the data out into an array first.

Example · csharp
int[] buffer = [10, 20, 30, 40];
Reader reader = new Reader(buffer);

Console.WriteLine(reader.Next());
Console.WriteLine(reader.Next());
Console.WriteLine(reader.Remaining);

ref struct Reader
{
    // A ref struct may hold another ref struct. A class cannot.
    private readonly ReadOnlySpan<int> _data;
    private int _position;

    public Reader(ReadOnlySpan<int> data)
    {
        _data = data;
        _position = 0;
    }

    public int Remaining => _data.Length - _position;

    public int Next()
    {
        int value = _data[_position];
        _position++;
        return value;
    }
}

// Output:
// 10
// 20
// 2

allows ref struct, added in C# 13

Generic code used to be closed to spans: Describe<ReadOnlySpan<int>> was a compile error, which is why helper methods often had a separate span overload copied by hand. The allows ref struct clause is an anti-constraint, meaning it removes a restriction rather than adding one. Inside such a method the compiler assumes T might be a ref struct, so boxing it, assigning it to object, or storing it in a field is rejected.

Example · csharp
ReadOnlySpan<int> numbers = [1, 2, 3];

Console.WriteLine(Inspector.Describe(numbers));
Console.WriteLine(Inspector.Describe(42));

static class Inspector
{
    // Before C# 13 a ref struct could never be a type argument, so this
    // method simply could not accept a Span. `allows ref struct` lifts that,
    // and in exchange the body may not box a T.
    public static string Describe<T>(T value) where T : allows ref struct
    {
        return typeof(T).Name;
    }
}

// Output:
// ReadOnlySpan`1
// Int32

Discussion

  • Be the first to comment on this lesson.