Consuming Read-only Structs in C#
Mar 5, 2020 • 6 Minute Read
Introduction
With the Roslyn update, Microsoft has changed the way versioning works and allowed minor version updates in their release schedule. Version 7.2 of C# was one such update, and its main goal was to introduce the concept of read-only structs. There are advantages and disadvantages to this new feature, and this guide will discuss those in detail. First, we will clarify what structs are, and then we will check out the difference between read-only and standard structs.
Structs
Structs share a very similar syntax to classes. There is a restriction that tells you that the name must be a valid C# identifier name. The restriction says the name can start with either a letter or the underscore (_) character.
The limitations of structs include:
- Only static or const fields can be initialized during declaration.
- You cannot declare a finalizer, a.k.a. parameterless constructor.
- Structs are copied on assignment.
- Structs are value types, while classes are reference types.
- Structs can have constructors that accept parameters.
- Structs cannot inherit from another struct or class. They inherit from value types, which inherit from object types.
- Structs can have interfaces.
- Structs cannot be null, meaning they cannot assign any of their variables the null value unless they are declared as nullable.
Let's see an example of a struct.
public struct Device
{
public string DeviceType { get; set; }
public string Name { get; set; }
public int Serial { get; set; }
public Device(string devicetype, string name, int serial)
{
DeviceType = devicetype;
Name = name;
Serial = serial;
}
}
The first difference we note is that the struct keyword is used at declaration. As a general rule of thumb, you need to remember that structs are basically data containers, and while classes can be data containers too, they are designed for something else. When you are in doubt, you need to decide whether you want to use value semantics or reference semantics in your application. The first comes from structs, the second comes from classes.
This type of struct can be freely modified. You can change the Name, DeviceType and the Serial after instantiation. More tech-savvy people are also aware that you can even modify the content this and change it to another instance. In short, this struct is a mutable datastructure.
Read-only Structs
In order to transform our above struct to read-only, we need to do two things. First, we need to add the readonly keyword. But that alone is not enough. We also need to remove the set from the properties. If we do not, we get the following error:
CS8341 Auto-implemented instance properties in readonly structs must be readonly.
The final look of the struct needs to be like this:
public readonly struct Device
{
public string DeviceType { get;}
public string Name { get;}
public int Serial { get; }
public Device(string devicetype, string name, int serial)
{
DeviceType = devicetype;
Name = name;
Serial = serial;
}
}
You need to be aware that this works because in this readonly case, the modification of the values is only allowed from the call of the constructor and only if the constructor has parameters.
For example, if you want a parameterless constructor, you might try this:
public Device()
{
DeviceType = "Server";
Name = "DC1";
Serial = 112233;
}
//OR
public Device
{
DeviceType = "Server";
Name = "DC1";
Serial = 112233;
}
You will face two errors:
- Structs cannot contain explicit parameterless constructors.
- A list of properties that need to be assigned before the control is returned to the caller.
Now that we have clarified this, we can start consuming the read-only struct.
Consuming Read-only Structs
There is a keyword we need to introduce, which is in. This allows us to pass an argument via a read-only reference. In C#, there are two ways to pass an argument: you either pass it by value or by reference. There are two more ways that are variants of the first, known as in and out. The out is actually a reference-based argument passing where the compiler additionally checks for a definite assignment. The in is also passed by reference with a very special attribute.
System.Runtime.CompilerServices.IsReadOnlyAttribute
This is set to true when using in to pass an argument, and the compiler will ensure that no assignment happens to the struct in that function definition where this argument is passed.
This change in the language has two consequences:
- You cannot use in with iterator blocks or async methods.
- You cannot create overloads for your methods that are only different by in,ref or out.
There is one bottleneck you need to be aware of. There is a defense mechanism that creates a copy of the parameter every time it is used. If the passed parameter is a struct and it's also read-only, this protection is removed. This means that you should never pass a read-only struct this way. It will degrade performance and remove protection as well.
Conclusion
In this guide we have learned about read-only structs. We have seen that they provide us with better design possibilities and improved performance. We have walked through how to build this type of datastructure and seen the nifty details that allow us to use this feature efficiently. I hope this guide has been informative to you and I would like to thank you for reading it!