Bart Kessels
Bart Kessels
Passionate open source software engineer who loves to go backpacking

C# 12 updates - ref readonly

C# 12 updates - ref readonly
This image is generated using Dall-E
  • Prompt: 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

    C# 12 updates - Introduction

    A couple of weeks ago Microsoft has released C# 12 which packed a lot of new features. In the upcoming week...

    C# 12 updates - Primary Constructor

    For those of you coming for Javascript or Kotlin, you're going to love this new feature. Set your member va...

    C# 12 updates - Default lambda parameters

    In C# 12, Microsoft has added the ability to specify default values for your lambda parameters.

    C# 12 updates - Collection expressions

    C# 12 finally introduces some new syntactical suger that I'm eager to use in production because it improves...