31

Readonly ref variables, in parameters and readonly structs

 5 years ago
source link: https://www.tuicool.com/articles/hit/Uri6RzI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

During our trip aroundmanaged pointers andstructs, 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:

public class Book // or struct
{
	public string Title;
	public string Author;
}
 
public class BookCollection
{
	private Book[] books =
	{
	  new Book { Title = "Call of the Wild, The", Author = "Jack London" },
	  new Book { Title = "Tale of Two Cities, A", Author = "Charles Dickens" }
   };
	private Book nobook = default;
	public ref readonly Book GetBookByTitle(string title)
	{
		for (int ctr = 0; ctr < books.Length; ctr++)
		{
			if (title == books[ctr].Title)
				return ref books[ctr];
		}
		return ref nobook;
	}
}
 
public class Program
{
	static void Main(string[] args)
	{
		var collection = new BookCollection();
		ref readonly var book = ref collection.GetBookByTitle("Tale of Two Cities, A");
		//book = new Book(); // Line 1
		book.Author = "Konrad Kokosa"; // Line 2
		Console.WriteLine(book.Author);
	}
}

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:

public struct Book
{
   ...
   public void ModifyAuthor(string author)
   {
      this.Author = author;
   }
}

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

ref readonly var book = ref collection.GetBookByTitle("Tale of Two Cities, A");
book.ModifyAuthor("Konrad Kokosa");
Console.WriteLine(book.Author);

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:

.method public hidebysig static 
	void Main () cil managed 
{
	.locals init (
		[0] valuetype Book
	)
	IL_0000: newobj instance void BookCollection::.ctor()
	IL_0005: ldstr "Tale of Two Cities, A"
	IL_000a: callvirt instance valuetype Book& BookCollection::GetBookByTitle(string)
	IL_000f: ldobj Book
	IL_0014: stloc.0
	IL_0015: ldloca.s 0
	IL_0017: ldstr "Konrad Kokosa"
	IL_001c: call instance void Book::ModifyAuthor(string)
	IL_0021: ret
}

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:

.method public hidebysig static 
	void Main () cil managed 
{
	IL_0000: newobj instance void BookCollection::.ctor()
	IL_0005: ldstr "Tale of Two Cities, A"
	IL_000a: callvirt instance valuetype Book& BookCollection::GetBookByTitle(string)
	IL_000f: ldstr "Konrad Kokosa"
	IL_0014: call instance void Book::ModifyAuthor(string)
	IL_0019: ret
}

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:

struct Point3D
{
   private static Point3D origin = new Point3D();
   public static ref readonly Point3D Origin => ref origin;
   ...
}

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:

public class BookCollection
{
   ...
   public void CheckBook(in Book book)
   {
      book.Title = "XXX"; // Compilation error: Cannot assign to a member of variable 'in Book' because it is a readonly variable.
   }
}

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:

public class BookCollection
{
   ...
   public void CheckBook(in Book book)
   {
      book.ModifyAuthor(); // Called on book defensive copy, original book Author will not be changed.
   }
}

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):

class Program
{
	private static readonly TestStruct _Account = new TestStruct();
 
	static void Main(string[] args)
	{
		Console.WriteLine(_Account.MyMoney);
		_Account.UpdateValue(100);
		Console.WriteLine(_Account.MyMoney);
	}
 
	public struct TestStruct
	{
		private int _money;
		public int MyMoney => _money;
 
		public void UpdateValue(int moneyAmount)
		{
			_money += moneyAmount;
		}
	}
}

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:

class Program
{
	private static readonly SpinLock sl = new SpinLock();
	static void Main(string[] args)
	{
		// good luck with sl.Enter and sl.Exit
	}
}

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:

I recently asked to the Roslyn team if it could be possible to add a warning (that we’d turn into error) when a defensive copy happens on an in param/readonly field and it’s not possible because it would be considered as a breaking change. Yes, breaking changes go that deep! 

— Alexandre Mutel (@xoofx) February 6, 2019

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:

public readonly struct ReadonlyBook
{
   public readonly string Title;
   public readonly string Author;
   
   public ReadonlyBook(string title, string author)
   {
      this.Title = title;
      this.Author = author;
   }
   
   public void ModifyAuthor()
   {
      //this.Author = "XXX"; // Compilation error: A readonly field cannot be assigned to (except in a constructor or a variable initializer)
      Console.WriteLine(this.Author);
   }
}

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:

public class ReadOnlyBookCollection
{
   private ReadonlyBook[] books = {
      new ReadonlyBook("Call of the Wild, The", "Jack London" ),
      new ReadonlyBook("Tale of Two Cities, A", "Charles Dickens")
   };
   private ReadonlyBook nobook = default;
   
   public ref readonly ReadonlyBook GetBookByTitle(string title)
   {
      for (int ctr = 0; ctr < books.Length; ctr++)
      {
         if (title == books[ctr].Title)
            return ref books[ctr];
      }
      return ref nobook;
   }
 
   public void CheckBook(in ReadonlyBook book)
   {
      //book.Title = "XXX"; // Would generate compiler error.
      book.ModifyAuthor(); // It is guaranteed that DoSomething does not modify book's fields. No defensive copy created.
   }
}
 
public static void Main(string[] args)
{
   var coll = new ReadOnlyBookCollection();
   ref readonly var book = ref coll.GetBookByTitle("Call of the Wild, The");
   book.Author = "XXX"; // Compiler error: A readonly field cannot be assigned to (except in a constructor or a variable initializer)
}

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.:

.method public hidebysig 
    instance void CheckBook (
        [in] valuetype ReadonlyBook& book
    ) cil managed 
{
    IL_0000: ldarg.1
    IL_0001: call instance void ReadonlyBook::ModifyAuthor()
    IL_0006: ret
}

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

.method public hidebysig 
    instance void CheckBook (
       [in] valuetype ReadonlyBook& book
    ) cil managed 
{
    .locals init (
        [0] valuetype ReadonlyBook
    )
    IL_0000: ldarg.1
    IL_0001: ldobj ReadonlyBook
    IL_0006: stloc.0
    IL_0007: ldloca.s 0
    IL_0009: call instance void ReadonlyBook::ModifyAuthor()
    IL_000e: ret
}

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!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK