C# 7 Series, Part 9: ref structs

Part 1: Value Tuples
Part 2: Async Main
Part 3: Default Literals
Part 4: Discards
Part 5: Private Protected
Part 6: Read-only structs
Part 7: Ref Returns
Part 8: “in” Parameters
Part 9: (This post) ref structs

Background

In the previous posts, I have explained many new C# features, each of the feature is introduced to either enhance the language, or solve issues. Specifically, I explained value types and reference types, pass parameters by value, pass parameters by reference, ref locals and ref returns and in parameters. Many of these features are designed for high performance scenarios.

ref and in parameters can help to avoid copying values so that it reduces the allocation of memory. This is efficient when you have stack allocated local variables to pass as the actual parameters of a method, in this case, all the allocations are on the stack; no heap allocation is necessary.

For high performance and native development scenarios, you may want “stack-only”  types that always stay on execution stack, so operations against objects of this type can only happen on the stack, any external references to this type in the scopes that may expose to managed heap, should be disallowed.

Ref Struct

ref struct is the stack-only value type that:

  • Represents a sequential struct layout;
  • Can be used stack-only. i.e. method parameters and local variables;
  • Cannot be static or instance members of a class or normal struct;
  • Cannot be method parameter of async methods or lambda expressions;
  • Cannot be dynamic binding, boxing, unboxing, wrapping or converting.

ref struct is also called embedded reference.

Examples

The following code defines a ref struct.

 public ref struct MyRefStruct
{
    public int MyIntValue1;
    public int MyIntValue2;

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override bool Equals(object obj) => throw new NotSupportedException();

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override int GetHashCode() => throw new NotSupportedException();

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override string ToString() => throw new NotSupportedException();
}

Please note, I have overridden the Equals, GetHashCode and ToString methods that are inherited from System.Object.  Because boxing is not allowed for ref structs, you will have no way to call these two base methods.

You can use MyRefStruct as regular value type in method parameters or local variables, but you will not be able to use it elsewhere.

image

image

You can also make readonly ref structs, simply add readonly directive to the ref struct declaration.

 public readonly ref struct MyRefStruct
{
    public readonly int MyIntValue1;
    public readonly int MyIntValue2;

    public MyRefStruct(int value1, int value2)
    {
        this.MyIntValue1 = value1;
        this.MyIntValue2 = value2;
    }

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override bool Equals(object obj) => throw new NotSupportedException();

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override int GetHashCode() => throw new NotSupportedException();

    [EditorBrowsable(EditorBrowsableState.Never)]
    public override string ToString() => throw new NotSupportedException();
}

Like regular readonly structs, you need to make all instance fields/properties read-only.

Metadata

Ref struct is available in C# 7.2. This feature needs compiler level changes to work, to be backward compatible with previously  compiler generated assemblies,. C# 7.2 compiler emits [Obsolete] and [IsByRefLike] attributes for ref struct declarations.

If any old assemblies references library that contains ref struct types, the [Obsolete] attribute will effect and block the code from compiling.

Here is the generated IL for the above ref struct declaration.

 .class public sequential ansi sealed beforefieldinit Demo.MyRefStruct
extends [System.Runtime]System.ValueType
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = (
        01 00 00 00
    )
.custom instance void [System.Runtime]System.ObsoleteAttribute::.ctor(string, bool) = (
        01 00 52 54 79 70 65 73 20 77 69 74 68 20 65 6d
        62 65 64 64 65 64 20 72 65 66 65 72 65 6e 63 65
        73 20 61 72 65 20 6e 6f 74 20 73 75 70 70 6f 72
        74 65 64 20 69 6e 20 74 68 69 73 20 76 65 72 73
        69 6f 6e 20 6f 66 20 79 6f 75 72 20 63 6f 6d 70
        69 6c 65 72 2e 01 00 00
    )
.custom instance void [System.Runtime]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = (
        01 00 00 00
    )
// Fields
.field public initonly int32 MyIntValue1
.field public initonly int32 MyIntValue2
// Methods
.method public hidebysig specialname rtspecialname
instance void .ctor (
int32 value1,
int32 value2
        ) cil managed
    {
// Method begins at RVA 0x2090
// Code size 16 (0x10)
.maxstack 8
// (no C# code)
        IL_0000: nop
// this.MyIntValue1 = value1;
        IL_0001: ldarg.0
        IL_0002: ldarg.1
        IL_0003: stfld int32 Demo.MyRefStruct::MyIntValue1
// this.MyIntValue2 = value2;
        IL_0008: ldarg.0
        IL_0009: ldarg.2
        IL_000a: stfld int32 Demo.MyRefStruct::MyIntValue2
// (no C# code)
        IL_000f: ret
    } // end of method MyRefStruct::.ctor
.method public hidebysig virtual
instance bool Equals (
object obj
        ) cil managed
    {
// Method begins at RVA 0x20a1
// Code size 6 (0x6)
.maxstack 8
// throw new NotSupportedException();
        IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
// (no C# code)
        IL_0005: throw
    } // end of method MyRefStruct::Equals
.method public hidebysig virtual
instance int32 GetHashCode () cil managed
    {
.custom instance void [System.Runtime]System.ComponentModel.EditorBrowsableAttribute::.ctor(valuetype [System.Runtime]System.ComponentModel.EditorBrowsableState) = (
            01 00 01 00 00 00 00 00
        )
// Method begins at RVA 0x20a8
// Code size 6 (0x6)
.maxstack 8
// throw new NotSupportedException();
        IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
// (no C# code)
        IL_0005: throw
    } // end of method MyRefStruct::GetHashCode
.method public hidebysig virtual
instance string ToString () cil managed
    {
.custom instance void [System.Runtime]System.ComponentModel.EditorBrowsableAttribute::.ctor(valuetype [System.Runtime]System.ComponentModel.EditorBrowsableState) = (
            01 00 01 00 00 00 00 00
        )
// Method begins at RVA 0x20af
// Code size 6 (0x6)
.maxstack 8
// throw new NotSupportedException();
        IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor()
// (no C# code)
        IL_0005: throw
    } // end of method MyRefStruct::ToString
} // end of class Demo.MyRefStruct

Span<T> and Memory<T>

With the support of ref-like types, it is now possible to have a consolidated type for all contiguous memory access. The System.Span<T>, which represents a sequential space of the memory, can be used for universal memory operations for execution stacks, managed heaps and unmanaged heaps.

Here is a simple usage of ReadOnlySpan<T> to trim the start spaces of a string.

 internal class Program
{
    private static void Main(string[] args)
    {
        string text = "  I am using C# 7.2 Span<T>!";
        Console.WriteLine(TrimStart(text).ToArray());
    }

    private static ReadOnlySpan<char> TrimStart(ReadOnlySpan<char> text)
    {
        if (text.IsEmpty)
        {
            return text;
        }

        int i = 0;
        char c;

        while ((c = text[i]) == ' ')
        {
             i++;
        }

        return text.Slice(i);
    }
}

System.Memory<T> wraps the System.Span<T> so it can be used for async methods, which enables asynchronous scenarios for I/O bound and CPU bound operations. My next post will have more details.

Conclusion

C# 7.2 adds language features for high performance scenarios and enables efficiencies for lower native development and interoperability scenarios. Ref struct can also be used together with stackalloc, Span<T>, fixed buffers and Ranges (C# 7.3) for productivity.

NOTE: To use this feature, use Visual Studio 2017 15.5+.