proposals/rejected/collection-expressions-in-foreach.md
foreach[!INCLUDESpecletdisclaimer]
Closed in favor of immediately enumerated collection expressions.
Champion issue: https://github.com/dotnet/csharplang/issues/9739
Collection Expressions
introduced a terse syntax [e1, e2, e3, etc] to create common collection values.
This proposal extends their usage to foreach statements, where they can be used directly as the iteration
source without requiring an explicit target type.
It is common and reasonable for developers to want to iterate over a known set of values. This pattern appears frequently in real-world code:
// Today, developers must write:
foreach (var toggle in new[] { true, false })
{
RunTestWithFeatureFlag(toggle);
}
// With this proposal, they can write:
foreach (var toggle in [true, false])
{
RunTestWithFeatureFlag(toggle);
}
Another common scenario is iterating through a fixed set of stages or phases:
// Today:
foreach (var phase in new[] { Phase.Parsing, Phase.Binding, Phase.Lowering, Phase.Emit })
{
ExecuteCompilerPhase(phase);
}
// With this proposal:
foreach (var phase in [Phase.Parsing, Phase.Binding, Phase.Lowering, Phase.Emit])
{
ExecuteCompilerPhase(phase);
}
Requests for this capability have been heard internally and throughout the ecosystem. This feature was originally part of the collection expressions work but was extracted to keep the initial scope minimal. Additionally, implementing this in the general case would require giving collection expressions a "natural type," which proved to be too large and complex a design space to tackle at that time.
However, for foreach statements specifically, the problem space is much simpler. The collection is
created and immediately consumed—user code cannot introspect the collection itself—giving the language
and compiler broad flexibility in implementation without the complexities of determining a universal
natural type. This flexibility follows the design principles of collection-expressions themselves,
allowing optimal performance, with minimal syntax.
No grammar changes are required. Collection expressions are already valid expressions syntactically; this proposal only extends where they can be used semantically.
For an explicitly typed foreach statement of the form:
foreach (T v in [e1, e2, ..s1, etc.])
{
// ...
}
This is interpreted as:
foreach (T v in (T[])[e1, e2, ..s1, etc.])
{
// ...
}
In other words, the collection expression is target-typed to an array of the explicitly provided iteration type T.
For an implicitly typed foreach statement of the form:
foreach (var v in [e1, e2, ..s1, etc.])
{
// ...
}
The element type T_e is computed using the best common type
e1, e2, etc...s1, etc.The statement is then interpreted as:
foreach (T_e v in (T_e[])[e1, e2, ..s1, etc.])
{
// ...
}
If no best common type can be determined, a compile-time error occurs.
Note: this is also akin to having a method void M<T>(T[] values); and running type inference on M([e1, e2, ..s1, etc.]) to see
what type argument T is inferred to be. That type for T is then the element type for the foreach and the rules for an
explicitly typed foreach statement (above) apply.
While the semantics are defined in terms of array creation, a compliant implementation is free to optimize the collection expression however it deems appropriate, provided the observable behavior remains the same. For example:
ReadOnlySpan<T> for the elements when the element count is known and there are no intervening await expressions or yield statements.For example, foreach (var i in [0, 1, 2, 3]) could be translated to:
for (var i = 0; i <= 3; i++)
{
// ...
}
This follows the same implementation flexibility principle established in the base collection expressions translation specification.
The explicitly typed case uses the provided type T rather than computing best common type. This ensures the
iteration type properly informs the collection expression elements. For example:
foreach (byte b in [1, 2, 3]) // Values treated as bytes
{
// ...
}
If best common type was used in both cases, the values 1, 2, 3 would be typed as int, which would then fail to convert to byte.
The empty collection expression [] is:
foreach (int i in []) { } - the target type is known (int[])foreach (var v in []) { } - no element type can be inferredThis is not a special rule but follows naturally from the semantic translations above.
Because the semantics are defined in terms of arrays, pointer types are supported:
unsafe
{
int* p = null;
foreach (var v in [p, p]) // Legal - creates int*[]
{
// ...
}
}
This would not be possible if the target type were Span<T> or ReadOnlySpan<T>, which cannot contain pointer types.
// Implicitly typed with naturally typed literals
foreach (var value in [1, 2, 3, 4, 5]) // Element type is int
// Explicitly typed with strings
foreach (string s in ["hello", "world"]) // Element type is string
// With spread elements
int[] existing = { 1, 2, 3 };
foreach (var n in [0, ..existing, 4]) // Element type is int
// Type inference with mixed elements
IEnumerable<int> enumerable = GetNumbers();
foreach (var n in [1, 2, ..enumerable, 3]) // Element type is int
// With lambda expressions (requires explicit typing)
foreach (Func<int, int> f in [null, i => i, i => i * i])
// Empty collection with explicit type
foreach (string s in [])
// Boolean values
foreach (var b in [true, false]) // Element type is bool
// Null with reference types infers nullable
foreach (var s in [null, "hello", "world"]) // Element type is string?
// Dynamic elements
dynamic d = GetDynamic();
foreach (var x in [1, d, 3]) // Element type is dynamic
// Await expressions (not await foreach)
foreach (var result in [await GetValueAsync(), await GetOtherValueAsync()]) // Element type is BCT of the expressions
// Anonymous types
foreach (var item in [new { A = 1 }, new { A = 2 }]) // Element type is the anonymous type.
// Mixed arrays with collection expression
int[] arr = { 1, 2 };
foreach (var a in [arr, [3, 4]]) // Legal. Inner collection expression target typed to int[]. Outer to int[][]
// Error: Cannot infer element type from empty collection
foreach (var x in [])
// Error: No best common type between incompatible types
foreach (var x in [SyntaxKind.IfKeyword, "string"])
// Error: Lambda expressions need target type
foreach (var transform in [node => node.WithoutTrivia()])
// Error: No best common type when all elements are collection expressions
foreach (var tokens in [[SyntaxKind.Public, SyntaxKind.Private],
[SyntaxKind.Static, SyntaxKind.Async]])
// Error: await foreach doesn't work with collection expressions
// (IAsyncEnumerable cannot be created from collection expression)
await foreach (var compilation in [comp1, comp2, comp3])
{
// ...
}
This feature deliberately avoids giving collection expressions a general "natural type." While there is
clear user demand for expressions like var x = [1, 2, 3], determining what type x should be (array, List<T>,
ImmutableArray<T>, etc.) involves complex trade-offs around mutability, performance, and API design.
The foreach scenario sidesteps these issues because:
This allows us to provide value to users now while leaving the door open for a future "natural types" feature.
Implementations are encouraged to aggressively optimize these patterns. Since the collection's lifetime is limited
to the foreach statement itself, compilers can:
for loopsThe only requirement is that the iteration order and values match what would be produced by creating and iterating an array. These optimizations are best-effort, consistent with the approach taken in the base collection expressions specification.
When null literals appear in a collection expression with reference types, the best common type computation will produce a
nullable reference type. For example, [null, "hello"] has an element type of string?.
When any element in the collection expression is of type dynamic, the computed element type becomes dynamic. This
follows the standard best common type rules where dynamic acts as a "top type" for type inference purposes.
Collection expressions cannot be nested when using implicit typing if all elements are collection expressions, as collection
expressions have no natural type. However, mixing arrays or other collections with collection expressions works:
[existingArray, [1, 2, 3]] is valid because existingArray provides a concrete type for best common type computation.
This feature does not support await foreach with collection expressions, as IAsyncEnumerable<T> cannot be created
from a collection expression. However, await expressions can appear as elements:
foreach (var x in [await GetValueAsync(), await GetOtherAsync()]) is valid and the awaits are evaluated before iteration begins.
None at this time.
[TBD: Links to relevant LDM notes]