struct vs readonly struct vs ref struct vs record struct

C# knows a various span of declarations of struct "types". In this article I will show what are the differences between a struct, readonly struct and ref struct. Furthermore what are the limitations between those types.

struct

Everyone used this in some way or another. A structure is a value type, which should ideally represent a single value. Prominent examples would be DateTime, int, double or TimeSpan but also things like a Point2D which would consist out of a x and y coordinate would be a valid example. On the Microsoft documentation we'll find a hint when and when not to use structs:

Avoid ... It will not have to be boxed frequently.

The last part is important and this is where a lot of this article will go to. We should not box our struct often. Normally value types such as a struct live on the stack. The stack is a nice way of modelling the liftetime of a variable. In contrast to that we have the heap. I wrote a complete article about the differencs between stack, heap, boxing and unboxing: Have a read here. It is vital to understand those concepts.

Important to understand is that normally a struct or any value type will live on that stack. So if you declare a int in a function than it lives exactly in that scope and that is all:

public void MyFunc()
{
    int a = 2;
}

a does only exist inside MyFunc and therefore only on the stack. But if we use any value type for example as a field in a class then this variable has to live on the heap as the parent scope / lifetime lives on the scope. structs allow a lot of things a class would allow with some key differences:

  • struct can't inherit from other classes or structs (interfaces are possibile)
  • struct can't have a destructor
  • ...

An example of a struct:

public struct Color
{
	public byte Red;
	public byte Green;
	public byte Blue;
	
	public Color() { Red = 0; Green = 0; Blue = 0; }
	
	public void ShiftToGrayscale()
	{
		var avg = (byte)((Red + Green + Blue) / 3);
		Red = Green = Blue = avg;
	}
}

readonly struct

The readonly struct was introduced in C# 7.2. The basic idea is to clarify your struct as immutable therefore only allowed to read data, but not to mutate any state. Sure you can do this without the readonly modifier (like DateTime exists since the beginning of the framework itself), but the compiler will help you when you violate that readonly manner.

Our Color example from above will throw multiple compiler errors when we declare the struct as readonly without adopting any of the code: public readonly struct Color will give you something like that:

Compilation error (line 16, col 14): Instance fields of readonly structs must be readonly.
Compilation error (line 17, col 14): Instance fields of readonly structs must be readonly.
Compilation error (line 18, col 14): Instance fields of readonly structs must be readonly.
Compilation error (line 25, col 3): Cannot assign to 'Red' because it is read-only
Compilation error (line 25, col 9): Cannot assign to 'Green' because it is read-only
Compilation error (line 25, col 17): Cannot assign to 'Blue' because it is read-only

The reason is simple: First we have to declare every field as readonly. That makes sense, otherwise any caller could just mutate the colors. But that is not all: The ShiftToGrayscale method is still mutating the internal state, so to achieve immutability you would return a new color with the given values:

public readonly struct Color
{
	public readonly byte Red;
	public readonly byte Green;
	public readonly byte Blue;
	
	public Color() { Red = 0; Green = 0; Blue = 0; }
	
	public Color(byte red, byte green, byte blue)
	{
		Red = red; Green = green; Blue = blue;
	}
	
	public Color ShiftToGrayscale()
	{
		var avg = (byte)((Red + Green + Blue) / 3);
		return new Color (avg, avg, avg);
	}
}

That is a huge step and immutability brings a lot of advantages:

  • Increased testability
  • Thread safety.
  • Atomicity of failure.
  • Absence of hidden side-effects

Bottom-line: The compiler tries to help you with immutability. But: It can't guarantee that either. If you have mutable lists or classes like the following example, you will be still able to modify them.

public readonly struct MyStruct
{
	public readonly List<object> myStuff = new List<object>() {1, 2};
	
	public MyStruct()
	{
	}
	
	public void Mutate()
	{
		myStuff[0] = 3;
	}
}

ref struct

Also with C# 7.2 Microsoft introduced ref struct. The bottom line is: ref structs are guaranteed to live on the stack and can never escape to the heap. With that you can achieve low allocation scenarios where you don't put extra pressure on the garbage collector. Now who does guarantee that? The compiler does that for you. You might ask how does he do that? And the answer is simple: He will prohibit every scenario where a value type is moved to the managed heap:

  • A ref struct can't be the element type of an array.
    That means no list, no myrefStructs = new MyRefStructs[] but the next is fine as it will live on the stack: myrefStructs = stackalloc MyRefStructs[10];
  • A ref struct can't be a declared type of a field of a class or a non-ref struct.
    The reason is simple: The compiler can't guarantee that your struct is not going to the heap itself for all the reasons explained above. As a class always lives on the heap it is clear that it is forbidden to have it there as a field.
  • A ref struct variable can't be captured by a lambda expression or a local function.
    Lamda expression as well as local functions are outside of the parent scope and would have therefore to be boxed
  • A ref struct variable can't be used in an async method.
    The reason is how the C# compiler generates code when it encounters an async or better an await statement. Basically it builds a statemachine, which itself is a class. If you want to know more head over to my presentation including slides here.
  • A ref struct variable can't be used in iterators.
    The same reason as async: We create a state machine which would extend the scope/lifetime and therefore the ref struct would end up on the heap. If you want to know more here you go.
  • A ref struct can't implement interfaces
    Well they can't do that because that would mean that there is a chance that they get boxed.

The usage is very constrained, but still it has it uses. One of the most prominent examples is Span<T> introduced in .NET Core 2.1. It enables zero allocation scenarios. And yes Span<T> is internally a ref struct. I also covered that in greater detail here.

Bonus: readonly ref struct

Both modifiers can also be put together. The semantics would mean that the struct should be immutable and thanks to the ref keyword is only allowed to live on the stack.

record struct

C# 10 introduced them. And much like the record which was introduced in C# 9.0 and "enhances" the class, the record struct enhances a struct. It is a nice syntactic sugar from the compiler to give you "pre-defined" implementations of Equals, ToString() and GetHashcode

You can define a struct super easy like that:

public record struct Color(byte Red, byte Green, byte Blue)
{
    public Color ShiftToGrayscale()
	{
		var avg = (byte)((Red + Green + Blue) / 3);
		return new Color (avg, avg, avg);
	}
}

Now where is the ToString() and other methods? All that does the compiler for you in the background. The result will look something like that:

public struct Color : IEquatable<Color>
{
    public byte Red{ ... }

    public byte Green { ... }

    public byte Blue { ... }

    public Color(byte Red, byte Green, byte Blue)
    {
        ....
    }

    public Color ShiftToGrayscale()
    {
        byte num = (byte)((Red + Green + Blue) / 3);
        return new Color(num, num, num);
    }

    [IsReadOnly]
    [CompilerGenerated]
    public override string ToString()
    {
        ....
    }

    [IsReadOnly]
    [CompilerGenerated]
    private bool PrintMembers(StringBuilder builder)
    {
        ....
    }

    [CompilerGenerated]
    public static bool operator !=(Color left, Color right)
    {
        ...
    }

    [CompilerGenerated]
    public static bool operator ==(Color left, Color right)
    {
       ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public override int GetHashCode()
    {
        ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public override bool Equals(object obj)
    {
        ...
    }

    [IsReadOnly]
    [CompilerGenerated]
    public bool Equals(Color other)
    {
        if (EqualityComparer<byte>.Default.Equals(<Red>k__BackingField, other.<Red>k__BackingField) && EqualityComparer<byte>.Default.Equals(<Green>k__BackingField, other.<Green>k__BackingField))
        {
            return EqualityComparer<byte>.Default.Equals(<Blue>k__BackingField, other.<Blue>k__BackingField);
        }
        return false;
    }

    [IsReadOnly]
    [CompilerGenerated]
    public void Deconstruct(out byte Red, out byte Green, out byte Blue)
    {
        Red = this.Red;
        Green = this.Green;
        Blue = this.Blue;
    }
}

First thing which is important the record automatically implement IEquatable<T>. For the full implementation have a look at the generated code on Sharplab.io. One of the best websites to understand what your code and syntactic sugar actually does! Nowadays more and more developers are using record and record structs as they are super convenient to use.

Bonus: readonly record struct

Sure you can also apply the readonly keyword to a record struct it will do the same as a "normal" struct as a record is translated to a normal one, meaning the compiler will enfore certain restrictions to enforce more immutability.

Bonus readonly ref record struct

This one does not exist. Also ref record struct for that matter. And the reason is very simple: record struct implements IEquatable<T> and we saw earlier that ref structs are not allowed to implement any interface.

Conclusion

I hope I could give you an overview over the different keywords used in combination with struct, what they do and where to use them (or even better, when to avoid them).

Resources

19
An error has occurred. This application may no longer respond until reloaded. Reload x