Readonly ref variables, in parameters and readonly structs

During our trip around managed pointers and structs, we get the last topic to discuss – readonly semantics. Thus, we touch today topics like readonly structs and readonly paremeters.

Readonly ref variables

Ref types are quite powerful, because we may change its target. Thus, readonly refs were introduced in C# 7.2 that controls the ability to mutate the storage of a ref variable.
Please note a subtle difference in such context between a managed pointer to a value type versus a reference type:

  • for value type target – it guarantees that the value will not be modified. As the value here is the whole object (memory region), in other words, it guarantees that all fields will not be changed.
  • for reference type target – it guarantees that the reference value will not be changed. As the value here is the reference itself (pointing to another object), it guarantees that we will not change it to point to another object. But we can still modify the properties of the referenced object.

Let’s use an example returning a readonly ref:

BookCollection may illustrate the difference between readonly ref in case of both value type and reference type.

If Book is a class, it is guaranteed that we will not change the reference value, like trying to change it to a new object in commented Line 1 above. However, it is perfectly fine to modify fields of the target referenced instance so Line 2 would compile without a problem.

However, if Book is a struct, it is guaranteed that we will not be able to change its value, like trying to change the author in Line 2 (and for the same reason, it is not possible to assign to it a new value in Line 1).

These seemingly difficult nuances are easy to remember if we keep in mind what is a protected value – the whole object (for value type) or reference (for reference type).

Defensive copies

There is one important aspect to be mentioned in this context. Let’s assume that our Book is a struct and has a method that modifies its field:

What happens if we call it on a returned readonly ref? Like:

Even in such case ,it is guaranteed that the original value will not be changed, so the above sample would still print “Charles Dickens” to the console. It is implemented by so-called defensive copy approach – before executing ModifyAuthor method, a copy of the returned value type (a Book struct in our case) is being made and its method is called on it. It is perfectly visible in the corresponding IL code:

Line IL_00f contains ldobj instruction – it is being described as doing “Copy the value stored at address src to the stack“. Such copying does not occur if we use not readonly ref variable:

Compiler does not analyze whether a method called on readonly indeed modifies state, as it really difficult (assuming a lot of possible conditions inside a method, maybe even depending on external data). Thus, any method called on such struct will be treated that way. So in fact, ModifyAuthor method is still executed but only on a temporary instance that becomes unused soon. Any changes applied to such a defensive copy obviously are not performed on the original value.

Such defensive copy may be both surprising and costly – one may expect the field to be modified if ModifyAuthor method executed successfully. Creating a defensive copy of
a struct also is an obvious performance overhead (it required memory copying).

Note. Please note in case of a Book being a class, the expected behavior remains – ModifyAuthor would modify the object state even if readonly reference was returned to it. Remember, readonly reference disables reference mutation, not the reference target values.

Please note that readonly ref returns do not have to be used only in the context of collections. There is a good example of using readonly refs in MSDN to return static value type representing some global, commonly used value:

Without readonly ref returned the Origin value would be exposed to modification, which is obviously unacceptable because Origin should be treated as a constant. Before
introducing ref returns, such value could be exposed as a regular value type, but it could introduce copying of such structure many times.

A form of readonly refs is also available in the form of in parameters. This is a small yet very important addition to passing by reference feature added in C# 7.2. While
passing by reference using ref parameter, the argument may be changed inside such method – exposing the same problems as ref returning. Thus, the in modifier on
parameters was added, to specify that an argument is passed by reference but should not be modified by the called method:

Please note the same rules apply here as in readonly refs explained before: only a value of the parameter is guaranteed to be not modified. So, in case of in parameter
being a reference type, only the reference value is not modifiable – the target reference instance may be changed.

Thus, the same defensive copy approach is used when a method is called on in value type parameter:

You may also avoid defensive copies by making such struct readonly (if it is applicable) – they are explained just below. Because readonly structs disable any possible modifications on its fields, the compiler may safely omit creating defensive copy and call methods on passed value type arguments directly.

Defensive copies are created for all readonly structs, not necessarily represented by ref variables. For example, what is the output of the following example (taken from the Github issue listed below):

It will be 0 and 0 because UpdateValue method operates on the defensive copy of the readonly struct. Such behavior may sometimes lead to unexpected behavior, like mentioned in Readonly Structs vs Classes have dangerous inconsistency – failed spin lock Github issue.

The problem is that some types are structs and we may be not aware of that. Common example is SpinLock. Thus, when used as a readonly field (or ref variable) it will operate on a defensive copy:

In the above example we operate on copies, so each Enter and Exit is, in fact, no-op, meaningless operation. What’s worse, such code does not generate any compiler warnings. As it turns out, it is really demanding to introduce such change:

 

Readonly structs

We have already seen readonly ref and in parameters that disable modification of the argument in specified context. It may be very helpful in controlling that ref variable
used for value types will not allow the programmer to modify its value. One may, however, go even further and create immutable struct – the one that cannot be
modified once created. I hope you already see possible C# compiler and JIT compiler optimizations that comes from that fact – like the possibility to safely get rid of defensive copies while methods are called.

We define a readonly struct by adding a readonly modifier to a struct declaration:

Note. C# compiler enforces that every field of such struct is also defined as readonly.

If your type is (or can be) immutable from business and/or logic requirements point of view, it is always worth to consider using a readonly struct passed by reference (with
the help of in keyword) in high-performance pieces of code. As MSDN says:

“You can use the in modifier at every location where a readonly struct is an argument. In addition, you can return a readonly struct as a ref return when you are returning an object whose lifetime extends beyond the scope of the method returning the object.”

Thus, using a readonly struct is a very convenient way of manipulating immutable types both in safe and performance-aware manner.

For example, let’s modify BookCollection class to contain internally an array of readonly structs instead of regular structs:

It is fine that our readonly structs will be heap allocated inside such an array, because ReadOnlyBookCollection instances are heap-allocated reference types. However, all
immutability guarantees remains. Thus, the compiler will omit defensive copy creation in the CheckBook method.:

While if ReadonlyBook was not marked as readonly, such copy would be created:

Summary

This post was solely dedicated to various readonly usages in C#. The three main takeaways for you are:

  • structs are a powerful way of optimizing your code – due to avoiding heap allocations and possible JIT optimizations
  • ref variables are a convenient way of operating on value types – they may prevent copying them, if used wisely
  • readonly is a powerful way to semantically guard type instances from modification – but we should be aware of defensive copies that may hurt us both from performance and correctness perspective

Having said that, I wish you the best possible usage of structs and managed pointers!

Leave a Reply

Your email address will not be published. Required fields are marked *