C# 12 updates - ref readonly
This image is generated using Dall-EPrompt: Generate an image of a computer screen with an IDE open and someone trying out the new ref readonly modifier from C# in a minimalistic flat style
If we take a look at the release notes, Microsoft states that the ref readonly
modifier more clarity allows where the ref
or in
modifiers where used before (Microsoft, 2023).
When taking a look at the documentation for the ref readonly
modifier, it states that the modifier can be used to force the call site to pass in a reference and the callee cannot change the reference (Microsoft, 2023).
My expectations for this is, that when combined with the primary constructor it can be used to declare readonly fields in a class that are injected through the constructor. But let’s find out if that’s the case.
Implementation
Before we can start with playing around with the ref readonly
modifier, let’s create a new project inside our csharp-12-features
solution.
1
2
$ dotnet new console -n RefReadonlyModifier
$ dotnet sln add RefReadonlyModifier
Let’s re-use our PeopleRepository
and DbContext
from our previous post about the primary constructor.
DbContext
1
2
3
4
5
6
7
8
9
10
11
namespace RefReadonlyModifier;
public class DbContext
{
public IEnumerable<string> GetPeople() =>
new List<string>
{
"John Doe",
"Jane Doe"
};
}
PersonRepository
1
2
3
4
5
6
7
namespace RefReadonlyModifier;
public class PersonRepository(DbContext context)
{
public string GetName(int index) =>
context.GetPeople().ElementAt(index);
}
This is the exact same code as before, so let’s add the ref readonly
modifier to the DbContext
datatype in our PersonRepository
constructor.
1
2
3
4
5
public class PersonRepository(ref readonly DbContext context)
{
public string GetName(int index) =>
context.GetPeople().ElementAt(index);
}
This gives us the following error-message when we try to access the context
parameter from the GetName
method.
1
cannot use 'ref readonly' primary constructor parameter 'context' inside an instance member
When looking back at the documentation it’s pretty obvious because it clearly states ‘…must be present in the method declaration. …‘. So we can only use it for methods. Let’s update our PersonRepository
to pass the DbContext
as a readonly reference to the GetName
method.
1
2
3
4
5
public class PersonRepository()
{
public string GetName(ref readonly DbContext context, int index) =>
context.GetPeople().ElementAt(index);
}
This will make the context
variable read only as expected. If we take our PersonRepository
and put it into a tool like sharplab.io we get the generated C# code which will desugar the ref readonly
modifier.
1
2
3
4
5
6
7
8
public class PersonRepository
{
[System.Runtime.CompilerServices.NullableContext(1)]
public string GetName([In][RequiresLocation] ref DbContext context, int index)
{
return Enumerable.ElementAt(context.GetPeople(), index);
}
}
We see that two attributes In
and RequiresLocation
are added. If we remove the readonly
modifier both attributes are gone. So let’s find out what those attributes exactly are.
In attribute
This attribute makes sure that the context
is passed as a reference (Microsoft, 2021).
The
in
modifier allows the compiler to create a temporary variable for the argument and pass a readonly reference to that argument.
This is the attribute that makes sure we cannot change to location of the pointer at the call-site of our method.
RequiresLocation attribute
Looking at the official documentation, there’s not much to find except for this little bit of text.
Reserved for use by a compiler for tracking metadata. This attribute should not be used by developers in source code.
Let’s take a look at the spec and see if we can find more information. So my guess is, that this attribute requires the given argument to be declared before being passed into the method which required the ref readonly
parameter.
If we update our previous example and change the call to GetPerson(ref new DbContext(), 1)
and initialize a new DbContext
object during the call instead of passing an already allocated object. This will give us the warning Argument 1 should be a variable because it is passed to a 'ref readonly' parameter
. If we now remove the readonly
modifier in the GetPerson
signature, this error messages disappears and changes into Argument is 'value' while parameter is declared as 'ref'
. My expectation was that this was allowed, but I think that because the ref readonly
modifier is desugared into a ref
modifier with the [In]
and [RequiresLocation]
attributes, it allows for initializing an object during the call of the method.
How does it work
When using the ref readonly
modifier, it turns the parameter into a variable with an [In]
and [RequiresLocation]
attribute. This allows us to pass in an object that is not allowed to be changed in the method by assigning a new variable to the parameter with the ref readonly
modifier. This modifier is more or less the same as the already existing in
modifier, but the major difference is that the call site doesn’t have to be updated when you change existing API’s from ref
to ref readonly
. As with the in
modifier, the call site should also add the in
modifier, whereas the already present ref
modifier can stay the same for ref readonly
.
Use cases
As stated above, I think the best use case for this modifier are API’s that already use the ref
modifier but want to make sure that the passed argument isn’t allowed to change. To me, ref readonly
is more readable than the in
modifier because it clearly states what it is: a reference that I can’t change in this method. Whereas the in
modifier has this implicit guarantee.
Categories
Related articles