In the previous C# 13 article, you learned about features you’ll see in everyday code. In this final installment, we’ll take a look at the advanced features of the latest C# version, such as generic ref struct parameters, ref struct interfaces, the System.Threading.Lock
type, and OverloadResolutionPriorityAttribute
.
Generic ref struct parameters
Generic programming was introduced long before ref struct
. The type parameters are assumed to be heap allocatable, therefore ref struct
types could not be used.
C# 13 is changing this. A new constraint, allows ref struct
, enables you to indicate a generic type may be a ref struct. When this constraint is added, all operations performed on the type instances must be ref struct
permitted.
void Generic<T>()
{
var array = new T[2]; // allocates on heap
}
void GenericAllowsRefStruct<T>() where T : allows ref struct
{
// var array = new T[2]; // error: Array elements cannot be of type 'T'.
T instance = default;
}
While the method implementation is constrained to use only ref struct
compatible operations, it can be called with ref structs and non-ref structs. The following code shows calling this method with concrete types as well as generic parameters.
void UseWithNonRefStruct()
=> GenericAllowsRefStruct<int>();
void UseWithRefStruct()
=> GenericAllowsRefStruct<ReadOnlySpan<byte>>();
static void UseWithNonRefGeneric<T>()
{
var array = new T[2]; // allocates on heap
GenericAllowsRefStruct<T>();
}
void UseWithAllowsRefGeneric<T>() where T : allows ref struct
=> GenericAllowsRefStruct<T>();
As shown by the previous UseWithNonRefGeneric
example, when the method (or type) is used with a generic parameter, that parameter is not assumed to allow ref struct
unless it is declared to be. In other words, the user of the generic method (or type) decides whether it wants to maintain the constraint.
One use case for the allows ref struct
generic constraint is to pass state arguments to methods which will be passed back to a callback delegate. Thanks to the allows ref struct
, the state may now be ref struct
(like a ReadOnlySpan
). An example of such a method is string.Create
, which is used to construct a string from some state. This is its signature:
static string Create<TState>(int length, TState state, SpanAction<char, TState> action) where TState : allows ref struct
Many .NET generic delegates have been updated with the constraint to enable passing and returning ref structs
, including System.Action
and System.Func
.
IEnumerable<T>
and IAsyncEnumerable<T>
include the constraint as well. This allows implementing specialized enumerables where T
is a ref struct
. Base class collection types, like List<T>
, do not maintain the constraint when implementing the interface. This is expected since their storage is heap-based.
ref struct interfaces
As described in the previous section, ref struct
types can now be used as generic parameter types. This doesn’t allow you to do much with these types, as the only thing that is known about the type is that it may be a ref struct
. C# 13 also makes it possible for ref structs
to implement interfaces. The ref structs
can’t be casted to these interfaces (since that would require boxing them to the heap). The interfaces can be used as generic constraints, as shown in the next example.
UseFoo(new MyRefStruct());
UseFoo(new RegularStruct());
void UseFoo<T>(T instance) where T : IFoo, allows ref struct
=> instance.Foo();
interface IFoo
{
void Foo();
}
ref struct MyRefStruct : IFoo
{
public void Foo()
=> Console.WriteLine("MyRefStruct.Foo");
}
struct RegularStruct : IFoo
{
public void Foo()
=> Console.WriteLine("RegularStruct.Foo");
}
Lock type
Any reference type instance can be used to implement mutual exclusive access via the lock
keyword. This locking uses the .NET Monitor class under the hood. .NET 9 introduces a new dedicated lock type named System.Threading.Lock
. Thanks to its specialized implementation, this lock performs better than the monitor-based locking (Performance Improvements in .NET 9 - Threading). The Lock
type is recognized by the C# compiler and can be used with the C# lock
statement.
using System.Diagnostics;
using System.Threading;
Lock gate = new Lock();
lock (gate)
{
Debug.Assert(gate.IsHeldByCurrentThread);
Console.WriteLine("Called under lock");
}
The Lock
type is a reference type. For the compiler to use the specialized locking (and not use monitor-based locking), the compiler must be aware of the type. If we change the gate
declaration from Lock
to object
in the previous example, the compiler uses monitor-based locking. Fortunately, the compiler generates a CA9216 warning to inform us that we’re not using the specialized Lock
implementation.
Overload resolution priority
The compiler can be told what method overloads are preferred using the new OverloadResolutionPriority attribute. This is useful in libraries where the existing method can’t be removed due to backwards compatibility, and a new overload is introduced that is preferable or that may create ambiguity with an existing member. The attribute takes an integer value for which higher values indicate a higher priority.
The following example uses the OverloadResolutionPriority
to prefer a new overload over an existing one that is an exact match with the caller argument type.
using System.Runtime.CompilerServices;
int[] numbers = new int[10]; // type is int[]
int result = Calculator.Sum(numbers); // calls the ReadOnlySpan overload
static class Calculator
{
public static int Sum(int[] array) { ... }
[OverloadResolutionPriority(1)]
public static int Sum(ReadOnlySpan<int> span) { ... }
}
New and improved C# 13
In this final article of our two-part series, we looked at advanced C# 13 features, such as generic ref struct parameters, ref struct interfaces, the System.Threading.Lock
type and the OverloadResolutionPriorityAttribute
. These new advanced features improve C# for specific use cases.