`foreach` expansion spec doesn't match Microsoft C# compiler behavior
- Dominant language
- C#
- Stars
- 20.7k
- Forks
- 4.3k
- PR merge metrics
- PR metrics pending
Description
Consider
```csharp
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 GetEnumerator()
{
++Value;
return ((IEnumerable)new int[1]).GetEnumerator();
}
}
```
Per [13.9.5.2 Synchronous foreach](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#13952-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`](https://github.com/dotnet/csharpstandard/commit/0372a1384efd01df4ed1f7ef6bf2798999c86967) for C# 9.)
In fact, the spec has more incorrect wordings. Suppose `struct A` implements `IEnumerable` 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).
Contributor guide
Research direction
Start with the foreach expansion in section 13.9.5.2 of the C# specification and reproduce the struct example from the issue. Compare the compiler's behavior for a struct instance, a ref extension GetEnumerator, and explicit IEnumerable implementation; done means the specification and compiler behavior agree for these cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100