String.Create with Spans

One new C# feature with .NET 9 is the ability to define allows ref struct as a generic type constraint. It sounds contradictory to refer to this as a constraint, because it allows more types to be used with the given method. However, it can also limit how those generic type parameters maybe used in methods. Allowing ref structs allows ref struct types to be used as generic type parameters. The most common ref struct types are the built-in Span<T> and ReadOnlySpan<T> but this constraint allows for any custom ref structs as well. In C# ref struct types are special as they must live on the stack, and must not escape to the managed heap.

The C# compiler makes sure that ref struct types are used correctly, and in this sense, it poses as a constraint. For example, in the code snippet below MyType compiles without the constraint:

public class MyType<T> // where T : allows ref struct
{
    private T _field;
    public T MyCreate(Func<T> myfunc)
    {
        var value = myfunc();
        _field = value;
        return value;
    }
}

However, if we uncomment the where T : allows ref struct constraint, the compiler issues the following error for _field: Field or auto-implemented property cannot be of type 'T' unless it is an instance member of a ref struct.

Note, that this is not a new limitation, as if the type was not generic, we would get the same error when using a ref struct (such as Span<T>) in place of the T.

Usage

Overall, this new language feature allows writing generic code, that allows the usage of Span<T and ReadOnlySpan<T> types. The class library in .NET 9 already uses this feature in two particularly useful places.

Alternate Dictionary Lookup

The first is IAlternateEqualityComparer, which is an interface that is implemented by many built-in comparers (OrdinalComparer, OrdinalIgnoreCaseComparer, etc.) and it allows for collections like Dictionary<K,V to fetch the values of entries with a string key, but based on a ReadOnlySpan<char> input argument. I explored an early preview version of this feature in a previous post, but please note that the signature has changed by the time of the final release of .NET 9.

String Create

A second impactful place is allowing ref struct parameters with the string.Create method. This method has 3 parameters:

  • the size of the desired string

  • TState that can be any custom type to hold supporting values of the string creation

  • a SpanAction<char, TState>, that is a callback to initialize the string

For the SpanAction parameter a developer can pass a callback that formats TState into a preallocated buffer by Create method. This is quite handy for high-performance scenarios where one prefers to create a string while avoiding unnecessary allocations and copies. There is a detailed blog post about the internals by Steve Gordon.

Note that knowing the size of the string upfront avoids the need for underlying buffer to 'grow', hence it can be more efficient to string interpolation.

One limitation of this solution was that it could not use a value of ReadOnlySpan<char> or Span<char> for the TState parameter. In this case it had to allocate a reference type, such as string or a byte[].

However, in .NET 9 this feature also allows ref structs as type parameters. A ReadOnlySpan<T> can be passed now as an input argument:

ReadOnlySpan<char> data = "hello";
string.Create(data.Length, data, static (destination, p) => p.CopyTo(destination));

Custom Code

This feature is not limited to the BCL, developers can also use it in their application code. Generic types and methods may define this constraint:

public int MyCreate<T>(T data, Func<T, int> func) where T : allows ref struct
{
    return func(data) * 10;
}

However, notice that defining the constraint in-itself does not help to work with Span<T> and ReadOnlySpan<T> types, as the constraint applies to all ref structs, that are not necessarily spans. The above example uses a function (Func<> callback) to delegate the responsibility to the caller of this method to handle the actual ref structs, in a similar way as string.Create does:

ReadOnlySpan<char> data = "hello";
var result = test.MyCreate(data, static input =>
{
    if (int.TryParse(input, out int result))
        return result;
    return -1;
});

In the example above, input is typed as ReadOnlySpan<char>. Note, that for this to work, Func<> delegates also allow ref struct type parameters.

Another approach is allowing ref struct types on interfaces. For example, one can create a parser interface that allows ref struct input types:

public interface IParser<T> where T : allows ref struct
{
    public int Parse(T data);
}

For this post, this interface can be implemented by two types, SpanParser and StringParser. Note that handling the 'span'-ness of the input is completely done by the type implementing the interface. The interface above, cannot provide span specific information for the input parameter, as the constraint applies to all ref struct types.

public class SpanParser : IParser<ReadOnlySpan<char>>
{
    public int Parse(ReadOnlySpan<char> data)
    {
        if (int.TryParse(data, out int result))
            return result;
        return -1;
    }
}

public class StringParser : IParser<string>
{
    public int Parse(string data)
    {
        if (int.TryParse(data, out int result))
            return result;
        return -1;
    }

If someone would prefer to handle generic input types of ReadOnlySpan<>, a type like below could be implemented. This type can be instantiated as new Counter<char>() or new Counter<byte>(), and the implementation may iterate the input. A further constraint on T (for example IBinaryInteger) can allow even further processing of the input.

public class Counter<T> : IParser<ReadOnlySpan<T>>
{
    public int Parse(ReadOnlySpan<T> data) => data.Length;
}

Finally, the above IParser<T> interface could be used by a method as shown below. This method accepts a generic input, parses it, applies some custom logic (here the parsed results are multiplied by 10) and returns a result to the caller:

int MyCreate<T>(T data, IParser<T> parser) where T : allows ref struct
{
    return parser.Parse(data) * 10;
}

// Use the MyCreate method 👇
ReadOnlySpan<char> data = "hello";
MyCreate(data, new SpanParser());

To use this method with a ReadOnlySpan<char> a SpanParser must be provided as the second argument of this method, because the compiler makes sure that type of T matches for both the input arguments.

Conclusion

This post explains a new C# language feature in .NET 9 that allows ref structs as a constraint for generic type parameters. It shows two examples where this reduces allocation for existing types and methods built into .NET library. Finally, it shows how someone can utilize this feature on custom types and methods, so that ReadOnlySpan<T> and Span<T> types can become generic type arguments.