Discount for my course: High Performance Coding with .NET Core and C#

Gergely Kalapos


ref structs in C# 7.2 - .NET Concept of the Week - Episode 16

Posted on August 03, 2018



In this episode we talk about 'ref structs', which was introduced in C# 7.2. You will learn what it is, where it is used in the framework and when you should use it in your own code.


Source code

The written form of the video:

First of all, at the time I create this post 7.2 isn’t the default C# version, so the first thing you need is to make sure that your project targets at least C# 7.2.
You can do this on the Build tab:


Here you have to click to advanced and with this drop down you can make the necessary change. So instead of "latest major version" you have to select "latest minor version". If you skip this and use a C# 7.2 feature then Visual Studio will explicitly tell you to switch to 7.2.

By the way this change is saved in the csproj file (spot the LangVersion tag!), and of course you can also do it directly within this file, so you don’t need Visual Studio for switching to the latest compiler.
<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.1</TargetFramework>
    </PropertyGroup>
    
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
        <LangVersion>7.2</LangVersion>
    </PropertyGroup>
    
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
        <LangVersion>7.2</LangVersion>
    </PropertyGroup>
</Project>

And with this I can go back to my C# file and create a struct and add the ref keyword to it.

ref struct MyRefStruct
{

}

Basically, that’s it, this is what the syntax looks like and in the remaining part of the post we will talk about what this exactly means.

What is a ref struct?

Well, a ref struct is basically a struct that can only live on the stack.

Now a common misconception is that since classes are reference types, those live on the heap and structs are value types and those live on the stack. This is wrong.
A normal struct can also be allocated on the heap.
A classic example is boxing.

On the left side you see a C# code that assigns a value type to System.sObject and on the right side you see the corresponding IL code with the box instruction. Boxing basically puts a value type onto the heap.

Another example is a struct within a class.


In this case, once we allocate an instance from MyClass than it will contain a MyStruct field, but since MyClass is allocated on the heap, the MyStruct field will be also placed onto the managed heap and the GC has to collect it when we don’t use the instance anymore.
So, the point is:
  • plain old structs can be allocated on the heap,
  • but ref struct can only exist on the execution stack.

Now what does this mean in practice?

Things we cannot do with ref structs

Well, this means that there are a couple of things -actually, lots of things- that you cannot do with a ref struct.

With a simple struct without the ref keyword you can do basically everything that you can do with a class with a very few exceptions. There are differences around inheritance, but you can create a normal struct basically everywhere; it can be a local variable, a field within a class or another struct, a generic parameter and so on.

Now this is not the case with ref structs.
In order to make sure that a ref struct only lives on the stack the C# compiler enforces a few rules when you declare ref struct variables.

For example a ref struct cannot be a static or an instance member of a class or a normal struct.


This means that the code on the image above does not compile since the compiler throws an error. Which kind of makes sense, since this would automatically place the ref struct onto the heap and that is exactly what we want to avoid.

A ref struct can’t be a method parameter of an async method or lambda either.


The reason for that is that the compiler creates a class or a normal struct from your lambdas and async methods, and with that we would again place the ref struct onto the heap, which is invalid.

Plus, we cannot do anything with a ref struct that would cause boxing.


So, this code is also invalid.

Furthermore, you can’t use it as a generic type argument and it cannot be a type of an array element either.

So, the point is: the compiler prevents us from putting a ref struct instance onto the heap which is kind of a major limitation.

Things we can do with ref structs

We can do basically 3 things with a ref struct:

  • it can be a method parameter
  • a return type of a method
  • and it can be a local variable.

And all those, kind of make sense, since in all of those cases the ref struct is allocated on the stack.
So, the obvious question is:

Why was this feature introduced?

The answer is Span<T>.

Now there are very good blog posts on Span, and I also talk about it in my "C# and .NET - Advanced topics" course, so I won’t go into too much details here.
In short: Span<T> is a value type that enables the representation of contiguous regions of arbitrary memory.


We have managed, unmanaged and stack allocated memory and we can wrap all 3 types of memory into a span and access it safely.

Of course there is much more to it and if this is new to you then I really suggest you to learn more about span.

All right, let’s get back to the original topic:

Why do we need ref struct to implement a span?

The first reason is that a span basically contains two fields:

  • the first field points to the data itself
  • the second field stores the length of the wrapped memory.

Now regardless of the implementation writes to such struct would not be atomic. This means if the span would live on the heap then we should always lock on them once we modify a span and with that we would lose the performance benefit that Span offers. Or we would skip the lock but with that we would risk out of range access and type safety. And this problem is by the way called struct tearing.

The second reason is that the implementaiton of span on CoreCLR contains a managed pointer in one of its fields. And those managed pointers cannot be fields of a heap object.

This means that all the limitations that we talked about with ref structs also apply to span.
Now I'd like to point out that if you cannot live with those limitations then there is another type called Memory<T>, which is very similar to Span<T>, but it is a plain, old struct without the ref keyword, so it can be a field of a type, it can be used in async methods, and so on.

The generated IL code

I have a ref struct here:

ref struct MyStruct {}
And here I have the corresponsing IL code:

The compiler basically emits two attributes to the struct:
  • The IsByRefLikeAttribute tells the CLR that this is a ref struct.
  • The second one is an obsolate attribute which ensures that such a reflike type is not used by an older compiler.
So compilers from 7.2 know this IsByRefLikeAttribute and this ObsolteAttribute with this specific string and those ignore this obsolate flag, but older compilers simply won't enable you to use this type, since they treat this obsolete flag really as obsolate. I'm not sure that this is a nice solution, but this is how it works.

Summary

To sum up: when should we use a ref struct?
The short answer is that we should use a ref struct each time when we want to make sure that our struct never ends up on the managed heap.

This means that ref structs don't put pressure to the GC.

I'd say there are two main use cases here:

  • First of all if you want to optimize your code and readuce preassure on the GC
  • And the second sceanrio is when you want to have a type that encapsulates other ref structs.

ref struct SampleRefStruct
{
    Span<int> intSpan;
    Span<double> doubleSpan;
}

For example if you want to encapsulate two spans, then the only way to do this is to create a ref structs, since Span<T> is a ref struct, and a ref struct can't be a field of a class or a normal struct.

And maybe a last advice to close this post: ref struct is a shiny new thing and in some specific cases it can be useful, but similarly to every new shiny thing: make sure that you do not overuse it.


;