December 2, 2024 4 minutes read
C# 13 will allow interfaces on ref struct
. Until now, without this possibility ref struct
missed out on abstraction. For example, while Span<T>
acts like a sequential list, it cannot be used with methods that accept IReadOnlyList<T>
or a IEnumerable<T>
. This requires separate methods for Span<T>
with nearly identical implementations. Allowing ref struct
to implement interfaces enables consistent abstraction and operation reuse across different types.
A Word on ref struct
C# 7.2 introduced ref struct
to get the benefit of pointers in a safe context and to prevent memory allocation in many common situations.
A ref struct
is a structure that can only exist on the thread stack. To enforce this, the C# compiler imposes several constraints, such as preventing the boxing of a ref struct
instance and ensuring that a field of a non ref struct
type cannot be typed with a ref struct
.
ref struct
s are designed to enhance performance. The most well-known ref struct
is Span<T>
. Internally, Span<T>
holds a managed pointer that can only exist on the thread stack. A managed pointer is a special .NET runtime feature that has been available since .NET 1.0. Before the introduction of Span<T>
, managed pointers were only used for parameter passing by reference through the ref
and out
keywords.
Managed pointers can point to various types of memory, such as a character within a string, an item in an array, a field within an object or even unmanaged memory. They are tracked by the Garbage Collector and are faster than regular object references due to their stack-only nature. These advantages motivated Microsoft to create Span<T>
and ref struct
to leverage the power of managed pointers.
More explanations in details in this article: Managed pointers, Span, ref struct, C#11 ref fields and the scoped keyword
Wait, interface means boxing?
Usually, interface on structure leads to boxing. This program…
|
MyStruct val = new(); IDisposable d = val; struct MyStruct : IDisposable { public void Dispose() { } } |
…compiles to this IL with the box
instruction:
|
IL_0000: ldloca.s 0 IL_0002: initobj MyStruct IL_0008: ldloc.0 IL_0009: box MyStruct IL_000e: stloc.1 IL_000f: ret |
Boxing a value on the heap is incompatible with a ref struct
that must reside on the thread stack. However, there is an exception where using an interface on a structure does not result in boxing: within a generic parameter context. The following program avoids any boxing:
|
MyStruct val = new(); DoDispose(val); static void DoDispose<T>(T obj) where T : IDisposable { obj.Dispose(); } struct MyStruct : IDisposable { public void Dispose() { } } |
The anti-constraint ‘allows ref struct’ on generic type
Until C# 13, it was not possible to use a ref struct as a generic type parameter:
This is why C# 13 introduces the anti-constraint allows ref struct
on generic type. In C# 13 this program compiles:
|
MyStruct val = new(); Method(val); static void Method<T>(T obj) where T: allows ref struct { // use obj } ref struct MyStruct { } |
allows ref struct
is the first C# anti-constraint on a generic type: instead of imposing a restriction, it allows a broader range of types to be used as generic parameters.
Putting it all together
In the .NET 9.0, Span<T>
and ReadOnlySpan<T>
do not implement any interface. This is likely due to potential breaking changes with their stack-only nature being incompatible with the enumerator pattern. Indeed, this pattern requires an IEnumerator<T>
object on the heap at some point. However, to implement an abstract algorithm that can work with both ReadOnlySpan<T>
and char[]
, we can still create a wrapper ref struct
that implements IReadOnlyList<T>
without an enumerator. Here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
using System.Collections; const string str = "Hello .NET 9 and C# 13"; ReadOnlySpan<char> span = str; // GetCustomHash() can be invoked on various types that implement IReadOnlyList<char> // including a ref struct with no heap allocation required int h1 = GetCustomHash(new ReadOnlySpanWrapper<char>(span)); char[] array = str.ToCharArray(); int h2 = GetCustomHash(array); Assert.IsTrue(h1 == h2); // Algorithm that can work with an abstracted span static int GetCustomHash<T>(T list) where T : IReadOnlyList<char>, allows ref struct { int hash = 0; int count = list.Count; for(int i = 0; i < count; i++) { // http://www.cse.yorku.ca/~oz/hash.html (hash * 33) + c // the magic of number 33 (why it works better than many other constants, prime or not) // has never been adequately explained. hash += (hash << 5) + list[i]; } return hash; } // ref struct wrapper ref struct ReadOnlySpanWrapper<T> : IReadOnlyList<T> { readonly ReadOnlySpan<T> _span; internal ReadOnlySpanWrapper(ReadOnlySpan<T> span) { _span = span; } public T this[int index] => _span[index]; public int Count => _span.Length; // No way to box a ref struct into an interface public IEnumerator<T> GetEnumerator() { throw new NotImplementedException();} IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } static class Assert { public static void IsTrue(bool b) { System.Diagnostics.Debug.Assert(b); } } |
allows ref struct and Alternate Lookup for Dictionary and HashSet in .NET 9
For an explanation of .NET 9 alternate lookup please refer to this article: Alternate Lookup for Dictionary and HashSet in .NET 9. It contains this code sample:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
var dico = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) { { "Paul", 11 }, { "John", 22 }, { "Jack", 33 } }; // .NET 9 : GetAlternateLookup() Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>> lookup = dico.GetAlternateLookup<ReadOnlySpan<char>>(); // https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.split?view=net-8.0 string names = "jack ; paul;john "; MemoryExtensions.SpanSplitEnumerator<char> ranges = names.AsSpan().Split(';'); foreach (Range range in ranges) { ReadOnlySpan<char> key = names.AsSpan(range).Trim(); int val = lookup[key]; Console.WriteLine(val); } |
We can see that ReadOnlySpan<T>
is used as a generic type parameter both at method GetAlternateLookup<ReadOnlySpan<char>>()
call site, and in the nested type Dictionary<string, int>.AlternateLookup<ReadOnlySpan<char>>
. This is because both declarations rely on allows ref struct
:
|
public System.Collections.Generic.Dictionary<TKey, TValue>.AlternateLookup<TAlternateKey> GetAlternateLookup<TAlternateKey>() where TAlternateKey : notnull, allows ref struct; public readonly partial struct AlternateLookup<TAlternateKey> where TAlternateKey : notnull, allows ref struct |
Conclusion
With ref struct
interfaces, Microsoft provides a new tool to enhance the performance of C# programs with no design sacrifice thanks to abstraction. Since 2017, there has been ongoing discussion about the possibility of ref struct
implementing interfaces. There, some developers exposed real-world examples where this language feature could shine.
In our last example, we had to use a wrapper to use a span as an IReadOnlyList<char>
. I wonder if anything will be done to prevent this glitch.