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.

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.
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:
- they can enable behaviour that is otherwise impossible
- they impose constraints on the caller
- 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.