Back to Csharplang

Collection Expressions in `foreach`

proposals/rejected/collection-expressions-in-foreach.md

latest10.9 KB
Original Source

Collection Expressions in foreach

[!INCLUDESpecletdisclaimer]

Closed in favor of immediately enumerated collection expressions.

Champion issue: https://github.com/dotnet/csharplang/issues/9739

Summary

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.

Motivation

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:

csharp
// 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:

csharp
// 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.

Detailed design

Syntax

No grammar changes are required. Collection expressions are already valid expressions syntactically; this proposal only extends where they can be used semantically.

Semantics

Explicitly typed foreach

For an explicitly typed foreach statement of the form:

csharp
foreach (T v in [e1, e2, ..s1, etc.])
{
    // ...
}

This is interpreted as:

csharp
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.

Implicitly typed foreach

For an implicitly typed foreach statement of the form:

csharp
foreach (var v in [e1, e2, ..s1, etc.])
{
    // ...
}

The element type T_e is computed using the best common type

  • The types of all expression elements e1, e2, etc.
  • The element types of all spread elements ..s1, etc.

The statement is then interpreted as:

csharp
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.

Implementation flexibility

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:

  • Stored in the program's constant data segment when it contains only constant values.
  • Using a stack-allocated ReadOnlySpan<T> for the elements when the element count is known and there are no intervening await expressions or yield statements.
  • Complete elision of the collection entirely.

For example, foreach (var i in [0, 1, 2, 3]) could be translated to:

csharp
for (var i = 0; i <= 3; i++)
{
    // ...
}

This follows the same implementation flexibility principle established in the base collection expressions translation specification.

Design decisions

Using explicit type vs. best common type

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:

csharp
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.

Empty collection expression

The empty collection expression [] is:

  • Legal in the explicitly typed case: foreach (int i in []) { } - the target type is known (int[])
  • Illegal in the implicitly typed case: foreach (var v in []) { } - no element type can be inferred

This is not a special rule but follows naturally from the semantic translations above.

Pointer types

Because the semantics are defined in terms of arrays, pointer types are supported:

csharp
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.

Examples

Valid usage

csharp
// 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[][]

Invalid usage

csharp
// 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])
{
    // ...
}

Design notes

Relationship to natural types

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:

  1. The collection is immediately consumed and cannot be stored or passed elsewhere
  2. The compiler can choose the most efficient representation for each specific case
  3. User code cannot depend on the specific collection type chosen

This allows us to provide value to users now while leaving the door open for a future "natural types" feature.

Optimization opportunities

Implementations are encouraged to aggressively optimize these patterns. Since the collection's lifetime is limited to the foreach statement itself, compilers can:

  • Use stack allocation for small, known-size collections
  • Embed constant data directly in the assembly
  • Transform simple patterns into equivalent for loops
  • Use specialized enumeration patterns that avoid allocations

The 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.

Special cases

Nullability

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?.

Dynamic

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.

Nested collection expressions

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.

Async considerations

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.

Open questions

None at this time.

Design meetings

[TBD: Links to relevant LDM notes]