Dotnet logo

.NET Tools

Essential productivity kit for .NET and game developers

How-To's

Readonly structs, ref readonly and conditional ref expressions – C# 7.2 in Rider and ReSharper

Today, we continue our blog series about C# 7.2 language support in ReSharper and Rider.

In this series:

Last time we indicated that there is a remaining pitfall with in parameters. Let’s see why!

Readonly structs

There is one downside to using in parameters: for regular structs, they impose the compiler to generate copies whenever we call an instance member on it. But why do we need a copy? Didn’t we learn that in parameters will only allow us to read from that variable, and not to modify it? This is only partially true, because it only prohibits reassignments of the parameter. Instance methods of the struct are still allowed to mutate the reference by setting the this parameter – in other words, changing themselves – or reassigning fields or properties. So in order to guarantee our expectations on the call-site – that in parameters will not allow modifications – copies are being created:

struct S
{
    public void InstanceM() { this = new S(); }
}

class C
{
    readonly S s;
    
    void M() {
        F1(s); // copy
        F2(s); // no copy
    }
    
    void F1(S x) {
        x.InstanceM(); // no copy
        x.InstanceM(); // no copy
    }
    
    void F2(in S x) {
        x.InstanceM(); // copy!
        x.InstanceM(); // copy!
    }
}

The concept of readonly structs introduced with C# 7.2 can solve this issue. Adding the readonly modifier will ensure that all instance members as well as the value itself (this reference) will be completely immutable. We can’t change anything, neither from outside nor from inside the struct:

readonly struct S
{
    public readonly int B;

    public void InstanceM()
    {
        this = new S(); // error
    }
}

class C
{
    readonly S s;

    void M() {
        s.B = 5; // error
        F2(s); // no copy
    }

    void F2(in S x) { }
}


Making a struct read-only of course requires us to implement all instance members as read-only. This can easily be achieved by using the corresponding quick-fix. Instance fields will get the readonly modifier, while auto-properties will have their setter removed:
Making all members read-only

Ref readonly returns and locals

Similar to in parameters, we can make use of ref readonly returns and locals to enforce immutability and non-copying behavior on call-site: Values returned from a ref readonly member cannot be reassigned. And just like with in parameters, copies will still be created when calling instance members on non-readonly structs:

readonly S s;

void M()
{
    ref readonly var a = ref GetValue();
    ref readonly var b = ref GetValue();

    a = new S();   // error: a is readonly
    b.InstanceM(); // will copy unless S is readonly struct

    var c = GetValue(); // will always copy
}

ref readonly S GetValue() { return ref s; }

Note that when initializing a simple variable (not ref readonly) from a ref readonly member invocation, the returned value is also simply copied.

Conditional ref expressions

Sometimes we may need to get a reference to a value based on a certain condition. Prior to C# 7.2 this has been quite cumbersome, and we had to use if-else statements:

void Update()
{
    // Before C# 7.2
    if (player1.GetDistance(ref position) < player2.GetDistance(ref position))
        Update(ref player1);
    else
        Update(ref player2);

    // With C# 7.2
    Update(ref player1.GetDistance(ref position) < player2.GetDistance(ref position)
        ? ref player1
        : ref player2);
}

void Update(ref Player closest) { /* ... */ }

A common workaround has been to use a Choice method, passing the condition along with the consequence and alternative. However, this implied the potential danger that both the consequence and the alternative will actually be evaluated, resulting in potential errors:

void M([CanBeNull] string[] array, string[] other)
{
    // Before C# 7.2 with Choice method
    ref var safeArray = ref Choice(array != null, ref array, ref other);
    ref var firstElement = ref Choice(array != null, ref array[0] /* possible NRE! */, ref other[0]);

    // With C# 7.2
    ref var safeArray = ref array != null ? ref array : ref other; // Okay
    ref var firstElement = ref array != null ? ref array[0] : ref other[0]; // Okay: truly conditional
}

ref T Choice(bool condition, ref T consequence, ref T alternative)
{
    if (condition)
        return ref consequence;
    else
        return ref alternative;
}

So overall, conditional ref expressions can help us to declare, pass and modify values by-reference much easier.

We hope that you’ve enjoyed this blog series and that your code bases gain a lot of profit from the new C# 7.2 language features.

Download ReSharper 2018.1 now! Or give Rider 2018.1 a try. We’d love to hear your feedback!

image description