.NET Tools
Essential productivity kit for .NET and game developers
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:
- Leading numeric separators, ref structs and in parameters
- Readonly structs, ref readonly and conditional ref expressions
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:
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!