Skip to content

Foreach processing for types implementing IEnumerable<T> is problematic #1188

@svick

Description

@svick

§13.9.5 The foreach statement of the current draft-v9 says that when processing foreach, and after not finding a GetEnumerable method (in practice, this happens with explicit interface implementation), we do the following:

If among all the types Tᵢ for which there is an implicit conversion from X to IEnumerable<Tᵢ>, there is a unique type T such that T is not dynamic and for all the other Tᵢ there is an implicit conversion from IEnumerable<T> to IEnumerable<Tᵢ>, then the collection type is the interface IEnumerable<T>, the enumerator type is the interface IEnumerator<T>, and the iteration type is T.

I think relying on implicit conversions here is problematic. Also, it's not what Roslyn actually does, resulting in differences between the specification and the implementations.

In particular:

  1. The specification allows implementing IEnumerable<T> multiple times and directly using such type in foreach, when one of the type parameters inherits from the other:

    foreach (var x in new MyCollection())
    {
    }
    
    class MyCollection : IEnumerable<string>, IEnumerable<object>
    {
        IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
    
        IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw new NotImplementedException();
    
        IEnumerator<object> IEnumerable<object>.GetEnumerator() => throw new NotImplementedException();
    }

    According to the spec, the unique type T here should be string, since there is an implicit covariant conversion from IEnumerable<string> to IEnumerable<object>. The compiler does not allow this code:

    error CS1640:: foreach statement cannot operate on variables of type 'MyCollection' because it implements multiple instantiations of 'IEnumerable'; try casting to a specific interface instantiation

  2. The specification results in the wrong iteration type for a type that implements IEnumerable<dynamic>:

     foreach (var x in new MyCollection<dynamic>())
     {
     }
    
     class MyCollection<T> : IEnumerable<T>
     {
         IEnumerator<T> IEnumerable<T>.GetEnumerator() => throw new NotImplementedException();
    
         IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException();
     }

    According to the spec, the unique type T here should be object, since the type is implicitly convertible to both IEnumerable<object> and IEnumerable<dynamic>, but dynamic is explicitly forbidden. The compiler determines the iteration type to be dynamic, as expected.

It seems to me that this could be resolved by following the compiler, and specifying this in terms of the interfaces actually implemented by the type in question, not based on implicit conversions. But I don't know whether that's viable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions