Skip to content

foreach expansion spec doesn't match Microsoft C# compiler behavior #84307

Description

@GeeLaw

Consider

A a = default;
Console.WriteLine(a.Value); // 0, as expected
((A)(a)).GetEnumerator();
Console.WriteLine(a.Value); // 0, as expected
foreach (var i in a)
  ;
Console.WriteLine(a.Value); // 1, unexpected per spec

public struct A
{
  public int Value;
  public IEnumerator<int> GetEnumerator()
  {
    ++Value;
    return ((IEnumerable<int>)new int[1]).GetEnumerator();
  }
}

Per 13.9.5.2 Synchronous foreach, the correct expansion uses ((C)(x)).GetEnumerator() to acquire the enumerator. In this example, C is A and x is a.

Casting a struct variable to its own type creates a copy, so according to the spec, the correct behavior is to mutate a copy of a.


I think the spec is wrong for the following reasons:

  • I do expect foreach (var i in a) to call GetEnumerator on a without cast.
  • When the resolved GetEnumerator method is an extension method taking this ref A, the expression ((A)(a)).GetEnumerator() (as required by the current spec) doesn't compile. Since extension GetEnumerator is added in or after C# 7.2 (which is when taking receiver by ref is allowed in extension methods), it's intended to work. (Update: It seems foreach by extension method is added in commit 0372a1384efd01df4ed1f7ef6bf2798999c86967 for C# 9.)

In fact, the spec has more incorrect wordings. Suppose struct A implements IEnumerable<int> explicitly without a public GetEnumerator method, the current behavior is to use constrained callvirt on a (hence potentially mutating, and also inside GetEnumerator the struct can check its identity) instead of casting it to the interface then invoking GetEnumerator (which boxes hence only mutates a copy).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions