Skip to content

[API Proposal]: Allow Span<T> to implement IEnumerable<T> via Compiler Call Site Substitution / Specialization #123705

@AlexRadch

Description

@AlexRadch

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:

  1. Interface Implementation: Allow Span<T> to explicitly implement IEnumerable<T>, but mark the implementation of IEnumerable<T>.GetEnumerator with a special attribute. This signals the compiler to substitute calls to this method with a call to the analogous method that returns the ref struct Span<T>.Enumerator.
  2. Call Specialization (Lowering): Introduce a mechanism where the compiler, upon detecting a GetEnumerator() call for Span<T>, substitutes it with a direct call to the GetEnumerator() method of the structure itself.
  3. Enumerator Handling: Most importantly, the returned result of GetEnumerator() (which is a ref struct) must not be cast to IEnumerator<T>. The compiler must continue to treat it as a struct, similar to how it currently handles struct enumerators in foreach loops.

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:

  1. The GetEnumerator method has no input arguments (matching the interface signature).
  2. The return type Span<T>.Enumerator implements IEnumerator<T>, which satisfies the requirement of the IEnumerable<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_

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationneeds-area-labelAn area label is needed to ensure this gets routed to the appropriate area owners

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions