Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C# 12.0: ref readonly

In C#, method declarations can require parameters to be passed by reference. This has consequences for semantics and performance. C# 12.0 adds a new annotation, ref readonly, which, as I'll show, seems awfully similar to the existing in annotation. We now have two ways to declare that arguments must be passed by reference but that the method isn't allowed to make changes through that reference. Why?

By reference redux

To understand the place of the new ref readonly, it's useful to recall the roles that the other by-reference annotations play.

In the beginning (v1.0) C# offered ref and out. With either of these, a parameter wasn't like a normal variable with its own value: instead it acted as an alias for whatever variable the caller chose to pass in. A function with a ref or out parameter was able to assign values into that parameter, which would have the effect of modifying whatever variable the caller had passed by reference.

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

With ref, a method could read or write through the reference, e.g.:

static void Increment(ref int x)
{
    x += 1; // Updates whichever variable the caller passed a reference to
}

A method is also free to do nothing at all with a ref parameters. This is quite different from out parameters, where methods aren't allow to read the existing value, and they are required to assign a value into all out parameters before returning (unless they throw an exception).

These, the oldest annotations, were really about behaviour: they make it possible to write methods that do things you simply couldn't do otherwise.

Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

However, enterprising C# developers (not to be confused with enterprise C# developers, although there is some overlap) noticed that ref also had an interesting performance characteristic: with large value types, it could be more efficient to pass them by reference than by value.

This led to the unfortunate practice of declaring parameters that were logically input-only, but which used ref for performance reasons. So in C# 7.2 it became possible to use the in keyword to signify this intent: this tells the compiler that we want the performance characteristics of passing by reference, but behaviourally, we want it to work just like a normal parameter, in that the method doesn't get to use the reference to modify the variable that the caller supplied.

What's wrong with in?

The in keyword means that an argument must be passed by reference, but the method can only use that method to read the caller-supplied variable. We might described this as "by reference, read-only." So why would we need a new ref readonly annotation?

A feature of in is that unlike out and ref, callers don't need to annotate call sites to indicate that they know the variable is being passed by reference. A classic out usage is the Try... pattern, e.g.:

if (d.TryGetValue(key, out string v))
...

The compiler would reject code that omitted the out keyword here, and likewise for ref parameters. But although you can annotate arguments to in parameters, it's optional. You can't tell from looking at this code:

SomeMethod(myVariable);

whether myVariable is being passed by value, or as an in reference. The justification for this is that use of in is for performance, not behaviour. There's no need for call sites to consent to pass-by-reference because there's no behavioural difference.

However, this is sometimes unhelpful.

For example, consider the IsNullRef method in the runtime libraries. This is used in performance-sensitive code that uses ref and ref-like types in low-level ways, and which may need to check that ref has not been initialized to null. (Although you can't assign null into a ref-typed variable, ref struct variables containing ref fields can be initialized with default into a zero-like state, at which point you'll have a ref field that is initialized to null. If you're writing this sort of code you're supposed to check for this, and that's what the IsNullRef method is for.)

Since IsNullRef only inspects the reference, it might seem like this is a job for in. But there's a problem with that. If IsNullRef had been declared with an in argument, the C# compiler would see nothing wrong with writing this sort of thing:

int i = 42;
if (Unsafe.IsNullRef(i)) ...

If the parameter were in, the compiler would silently generate code that passes a reference to the i variable. But this code is pointless: that compiler-generated reference would never be null.

We want code like this to produce a compiler error so we know that the method is being misused. And that's why IsNullRef is declared as having a ref argument. This way the call site requires a ref keyword to indicate that we know a reference is being passed. Of course we could still do something pointless, like passing ref i above, but it would at least make it a bit more obvious what's happening.

But this created a different problem: if a method like IsNullRef uses ref to avoid the problem just described, its signature does not indicate that the method won't use that reference to modify the variable to which it refers.

Enter ref readonly

By-reference annotations can achieve three things:

  1. they can enable behaviour that is otherwise impossible
  2. they impose constraints on the caller
  3. they impose constraints on the method implementation

The IsNullRef needs 1: its purpose is to check whether a reference is null, something that's only possible if we have some way of passing a reference. But before C# 12.0, the authors of this method were forced to choose between 2 and 3. If the parameter had been declared as in, that would have ensured that the method simply wasn't allowed to write through the reference, but this would have left it unable to require the caller to state explicitly that it knows a variable is being passed by reference. So the authors chose 2.

The new ref readonly annotation means we no longer have to choose. We can have a read-only reference where the caller is required to state clearly that they know it's being passed by reference. (Calling code can indicate this by using either ref or in at the call site.)

Although the online docs still seem to show IsNullRef as having a ref argument, you can see in the source code that it's ref readonly. This was changed as part of PR #89736, which took advantage of ref readonly in several places.)

Backwards compatibility

That PR changed quite a few existing public methods in the .NET runtime libraries. You might worry that this caused backwards compatibility problems. But as you would hope, the C# team thought about this.

Changing a ref method to be ref readonly is not a breaking change for callers. And for implementations that only ever read a ref parameter, changing the signature to ref readonly breaks nothing. The only situations in which replacing ref with ref readonly would be a breaking change is in a virtual method (i.e., any virtual or abstract method, or any interface method) or in a delegate type's signature: in those cases, code might be out there which defines its own implementation for the methods that does write through the reference. So the .NET team didn't add ref readonly to any virtual methods or delegate types in this PR.

More subtly, code for which ref readonly is well suited but which chose to use in (because ref readonly didn't exist when it was written) might also change to ref readonly. At a binary level this is not a breaking change, but it will cause errors when code is recompiled against the new declaration. (So existing compiled code will continue to work just fine.) This is similar to the situation that scoped was added to deal with: existing compiled code enjoys backwards compatibility but will require attention if you build it with the latest tool chain.

Summary

C# 12.0's new ref readonly annotation for method parameters does not provide any new capabilities, but it does let our code to be more expressive. It enables methods to declare that a parameter must be a reference, and that the method will never make any modifications to the value that reference points to, just like in, but inlike with in, callers must explicitly acknowledged the by-reference nature of the argument.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and 17 times Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 12.0, and has written Pluralsight courses on WPF fundamentals (WPF advanced topics WPF v4) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Ian has given over 20 talks while at endjin. Technology brings him joy.