-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Background and motivation
Currently, the .NET ecosystem suffers from API dichotomy: developers are forced to create and maintain duplicate methods — one set for IEnumerable<T> and another for ReadOnlySpan<T>.
In C# 13 (.NET 9), ref struct types gained the ability to implement interfaces. However, due to fundamental limitations of ref struct (the inability to be boxed on the heap), Span<T> cannot support the IEnumerable<T> interface. The IEnumerable<T>.GetEnumerator() method returns IEnumerator<T> (a reference type). Since the Span enumerator (Span<T>.Enumerator) is itself a ref struct, it cannot be returned as an interface without boxing.
Because of this limitation, we cannot use Span<T> in a vast number of existing methods that accept IEnumerable<T>, ICollection<T>, and IList<T>, even though Span<T> logically supports these interfaces.
API Proposal
I propose introducing support for IEnumerable<T> in Span<T> (and ReadOnlySpan<T>) using a compiler "hint" mechanism (e.g., via an attribute).
The core of the proposal:
- Interface Implementation: Allow
Span<T>to explicitly implementIEnumerable<T>, but mark the implementation ofIEnumerable<T>.GetEnumeratorwith a special attribute. This signals the compiler to substitute calls to this method with a call to the analogous method that returns theref structSpan<T>.Enumerator. - Call Specialization (Lowering): Introduce a mechanism where the compiler, upon detecting a
GetEnumerator()call forSpan<T>, substitutes it with a direct call to theGetEnumerator()method of the structure itself. - Enumerator Handling: Most importantly, the returned result of
GetEnumerator()(which is aref struct) must not be cast toIEnumerator<T>. The compiler must continue to treat it as a struct, similar to how it currently handles struct enumerators inforeachloops.
Essentially, this proposes extending the "Compiler Call Site Substitution" pattern to attribute-marked methods by invoking the struct method directly without casting the result to the IEnumerator<T> interface.
Expected Result
Methods accepting IEnumerable<T>, ICollection<T>, and IList<T> will be able to transparently accept Span<T> without memory allocations or data copying. This will allow for the reuse of a massive amount of existing code with the Span<T> type without needing to rewrite libraries, while achieving significant gains in both code performance and memory efficiency.
Code for Span<T> that support IEnumerable<T> interface
public readonly ref struct Span<T>: IEnumerable<T> // add IEnumerable<T> implementation
// Also it can support ICollection<T>, IList<T> and other interfaces for generic collections
{
// Inform the compiler that Span<T>.GetEnumerator() implements IEnumerable<T>.GetEnumerator() via specialization
[SpecializedImplementation(IEnumerable<T>.GetEnumerator())]
public Enumerator GetEnumerator() => new Enumerator(this);
public ref struct Enumerator: IEnumerator<T> // add IEnumerator<T> implementation
{
// Explicit interface implementations required by contract
object IEnumerator.Current => Current!;
void IEnumerator.Reset() => throw new NotSupportedException();
void IDisposable.Dispose() { }
}
}Explanation of the proposal
I propose introducing compiler support for a [SpecializedImplementation] attribute (naming is tentative). This attribute signals to the compiler that the marked method or property serves as a specialized implementation of a specific interface member.
Key Requirement: The method's input arguments and return type must be signature-compatible with the interface member.
In this specific case:
- The
GetEnumeratormethod has no input arguments (matching the interface signature). - The return type
Span<T>.EnumeratorimplementsIEnumerator<T>, which satisfies the requirement of theIEnumerable<T>interface method.
This enables the compiler to perform a safe call site substitution: instead of emitting a standard interface dispatch (or boxing), it emits a direct call to the ref struct method. Crucially, the return value is not boxed into IEnumerator<T>; instead, the compiler continues to treat it as a ref struct which effectively satisfies the interface contract.
Note: I am open to discussing the specific syntax. Perhaps, instead of an attribute, a more elegant approach could be proposed, such as new keywords or an extension of the implements syntax.
API Usage
Using HashSet<T> as an example, we can see how adding the allows ref struct constraint, combined with the proposed specialization mechanism, allows existing generic methods to accept Span<T> natively.
// Changes to allow ref struct for generic methods relying on IEnumerable<T>
public class HashSet<T>
{
// Note: Since instance constructors in C# cannot be generic, creation from Span
// would still require a specific overload or a static generic factory method.
public HashSet(ReadOnlySpan<T>)
// However, existing instance methods can be retrofitted simply by adding 'allows ref struct'
// allowing them to consume Span<T> via the specialized generic path.
public void ExceptWith<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public void IntersectWith<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool IsProperSubsetOf<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool IsProperSupersetOf<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool IsSubsetOf<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool IsSupersetOf<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool Overlaps<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public bool SetEquals<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public void SymmetricExceptWith<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
public void UnionWith<TEnumerable>(TEnumerable other)
where TEnumerable : IEnumerable<T>, allows ref struct;
}
// Usage Example:
Span<int> span1 = stackalloc int[] { 1, 2, 3 };
Span<int> span2 = stackalloc int[] { 4, 5, 6 };
// No memory allocation, no boxing, and no additional code needed in HashSet to support Span specifically
var hashSet = new HashSet<int>(span1);
// The compiler specializes this call to use the struct enumerator of Span<T>
hashSet.UnionWith(span2);
### Alternative Designs
_No response_
### Risks
_No response_