Low-level struct improvements in C# 11

Span<T> is one hell of a structure, which brought performance improvements in .NET on a different level. Simplified Span<T> represents a contiguous slice of memory. If that doesn't ring a big bell I would suggest reading my article "Create a low allocation and fast StringBuilder - Span in Action" where I describe this in great detail.


Info: The examples shown will be compilable with .NET 7 Preview 7 and above. Your IDE will most likely indicate errors but the build will work. Alternatively you can head over to https://sharplab.io and try it out there. In the dropdown on top you have to switch from "default" to "main" to make it work as "main" is the current branch of the developer team and therefore get the new stuff.


The unfortunate thing about Span<T> was that it had special handling by the runtime and the garbage collector. The trouble starts that this:

public readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

Is not valid syntax in C# 10 as we are not allowed to have ref fields in a struct. Now big surprise, we can do this with C# 11. The language team worked on making this available for us. A word of warning here: As the title suggests, we are talking about very low level stuff. That is not your everyday bread and butter for sure ... well besides if you do performance tuning on such a low level of course. The nature of low level stuff is, that it's most likely more complex and can do more harm than good. Keep that in mind.

So from now one we can create ref struct's with ref fields if we wish so. With that also some pitfalls might come into play. Let's have a look at the following code:

ref struct S 
{
    public ref int Value;
}

S local = default;

// What does happen here?
local.Value.ToString();

The last line will throw a NullReferenceException. Value is a reference which was never set to anything, hence the null. Now how about just adding a Value == null? Well that doesn't work out of two reasons. First Value is an integer even though we flagged it as reference. And more important second: C# defines that ref's can not be null. Under normal circumstances the languages forces you to initialize a ref object.

So we have to use special helpers to make that work:

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

What can I do with that?

This question is always interesting. And the scenarios are more in the realm of performance and or frugal objects. Frugal objects are small helper classes, which are extremely bound to your specific use-case. You can build for example a List, which always has 3 entries:

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

Or a linked list, which completely lives on the stack:

ref struct StackLinkedListNode<T>
{
    T _value;
    ref StackLinkedListNode<T> _next;

    public T Value => _value;

    public bool HasNext => !Unsafe.IsNullRef(ref _next);

    public ref StackLinkedListNode<T> Next 
    {
        get
        {
            if (!HasNext)
            {
                throw new InvalidOperationException("No next node");
            }

            return ref _next;
        }
    }

    public StackLinkedListNode(T value)
    {
        this = default;
        _value = value;
    }

    public StackLinkedListNode(T value, ref StackLinkedListNode<T> next)
    {
        _value = value;
        _next = ref next;
    }
}

As you can see the usage is quite limited for every day scenarios, but that is okay because it is not meant to replace something in your code. It just allows some new scenarios in a low allocation / high throughput environment.

scoped

Also a new keyword was introduced: scoped. Let's have a look what it does or what it tries to solve. For that I will take a small example from my ValueStringBuilder which basically builds upon those types.

public ref partial struct ValueStringBuilder
{
    private int bufferPosition;
    private Span<char> buffer;

    public void Append(ReadOnlySpan<char> str)
    { ... }

It doesn't matter what the function itself does only the usage is from bigger interest. Now let's call that thing:

var valueStringBuilder = new ValueStringBuilder();
Span<char> span = stackalloc char[2] { '1', '2' };
valueStringBuilder.Append(span);

That will result in the following error:

Cannot use local variable 'span' in this context because it may expose referenced variables outside of their declaration scope

The explanation is "simple":

the purpose of the scoped keyword for ref struct parameters is to allow stack allocated local variables to be passed to methods that do not capture the inbound ref struct or return the inbound ref struct from the method.

Taken from David L. stackoverflow

If we add the scoped modifier everything works as expected:

public void Append(scoped ReadOnlySpan<char> str)

This makes your code so much easier! Another common approach, where scoped helps is this:

scoped Span<char> buffer;
if (newSize < MaxSizeForStackalloc)
{
    buffer = stackalloc char[newSize];
}
else
{
    buffer = new byte[newSize]; // Or ArrayPool.Shared
}

Conclusion

If you are a low level C# developer these changes can really help you out!

Resources

7
Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x