# C# Language Design Meeting for April 15th, 2026

## Agenda

- [Deconstruction in lambda parameters](#deconstruction-in-lambda-parameters)
- [Labeled `break` and `continue`](#labeled-break-and-continue)

## Quote(s) of the Day

- "I love this feature, but it really feels like we're packing in meaning to every combination of tokens" "You gotta maximize token value"

## Discussion

### Deconstruction in lambda parameters

Champion issue: https://github.com/dotnet/csharplang/issues/9848  
Spec: https://github.com/dotnet/csharplang/blob/c4ec6fb60c2e174b1abb6c019f22bb15b9b13f6c/proposals/deconstruction-in-lambda-parameters.md

We opened with an overview of the proposal and a call for a go/no-go decision on the feature as a whole.
The motivating scenario is a lambda that receives a tuple (or any deconstructible value) and whose very
first act is to deconstruct that parameter. Today this forces the author to either invent a throwaway
name for the parameter and chain member accesses through it (for tuples, something like `pair.a` and
`pair.b`; for the source generator API, long trails of `tuple.Left.Left.Right`), or to expand into a
block-bodied lambda solely to fit a deconstruction statement in before the real work begins. LINQ-style
pipelines, `Zip`, and Roslyn's incremental generator API were all called out as places where this friction
shows up repeatedly.

The proposal lets the author write the deconstruction directly in the parameter list:

```csharp
Action<(int x, int y)> act = ((a, b)) => WriteLine(a, b);
```

The intuition is that a single anonymous parameter still exists with the original tuple or other
deconstructible type, but it has no name the body can refer to; the names inside the parentheses are
declared variables in scope for the lambda body, exactly as if the lambda body started with
`var (a, b) = <anonymous parameter>;`. The form extends naturally to explicitly typed parameters
(`((int a, int b)) => ...`) and nests for multi-level deconstruction, so patterns like
`(((syntaxTree, syntaxNodes), compilation), cancellationToken) => ...` become expressible without any
intermediate variable. Because the underlying notion is deconstruction rather than tupling, the same shape
also works for any type with a `Deconstruct` method, not just `ValueTuple`.

A substantial part of the discussion centered on the choice of syntax. We considered a "splat" style where
`(a, b) => ...` would bind to a single-parameter delegate whose parameter is a tuple, by implicitly
distributing that parameter across the tuple elements, and briefly entertained spread-like spellings such
as `(..(x, y)) => ...`. The splat form was rejected: it collides directly with the existing reading of
`(a, b) => ...` as a two-parameter lambda, and at a call site with overloads of both `Action<T1, T2>` and
`Action<(T1, T2)>` there would be no way to tell the two intents apart. Keeping the extra pair of
parentheses preserves the invariant that the outer parenthesized list always describes the lambda's
parameters, with each deconstruction living inside its own parameter position.

We also looked at the rules that keep this model simple. Deconstruction patterns in the parameter list must
be either fully explicitly typed or fully implicitly typed, matching whether the lambda itself is explicitly
or implicitly typed; mixed forms like `((a, b), int c) =>` or `((int a, int b), c) =>` are disallowed.
Parameter modifiers such as `ref`, `out`, and `params` cannot be applied to a deconstructed parameter.
`_` is always a discard at every nesting level, sidestepping the older single-versus-multiple
underscore distinction from deconstruction statements. Tuples still require at least two elements, as
elsewhere in the language.

Type inference was the most technically involved topic. A deconstructed lambda parameter without explicit
types does contribute information: the parameter must ultimately be a tuple (or deconstructible) shape, and
that shape flows back into inference. For a method like `void M<T1, T2>(T1 t1, T2 t2, Action<R<T1, T2>>
action)`, if `T1` and `T2` are pinned by the earlier arguments, then a deconstructed lambda
`((a, b)) => ...` should bind successfully even though the parameter's shape only becomes known after
inference.
We agreed this is the behavior we want, and that making it work cleanly requires careful integration with
the existing input/output type-inference machinery. The spec will need to be written with that integration
in mind rather than treated as a bolt-on.

Overload resolution raised related questions. When a deconstructed lambda has a natural type, that natural
type participates in overload resolution in the usual way. We discussed whether to design special tiebreakers for
cases like `M(Action<Point>)` versus `M(Action<(int, int)>)` where both targets are deconstructible into
the same shape. When one overload can be passed a tuple type and the rest cannot, there was openness to
allowing the "natural type" of the deconstruction to prefer the overload that can receive a tuple value.

We spent some time on the question of whether this feature should extend beyond lambdas to named methods
and local functions. For lambdas, the absence of a parameter name is unobservable to callers, and the
deconstructed names are purely a convenience for the body. For a named method, the parameter name is part
of the visible signature (named arguments, documentation, tooling), and an anonymous deconstructed
parameter would leave an open design question about what that public-facing name is. The metadata format
considers it valid for a method parameter to have no name, and there was openness to exploring this. A range of
syntactic sketches was floated for how one might thread a name through a deconstructed parameter (placing
an identifier before or after the pattern, leading with `var`, borrowing Swift-style outer/inner names,
and so on), and none were obviously right. We did not want to hold up the lambda feature on that design,
but we also did not want to close the door: if a reasonable answer to the naming problem emerges, we
would want the named-function form to be available, even if the only spelling initially is an anonymous
deconstructed parameter.

A general concern was raised that parameter lists are already dense pieces of syntax, with parentheses,
optional types, optional modifiers, and names all layered together, and that this proposal adds yet
another shape that readers have to recognize in the same position. The counter-argument, which carried
the room, is that under these rules the deconstruction pattern is unambiguous wherever it is allowed,
that it matches how readers already mentally process lambdas that start by deconstructing their argument,
and that the alternative is the continued proliferation of throwaway parameter names that exist only to
be immediately dotted or deconstructed.

#### Conclusion

We want to proceed with the general feature for lambdas. We also want to be able to deconstruct in the
parameter list of a named function, even if that means the parameter has no accessible name, once a
workable design is in hand. Detailed semantics, type inference integration, and overload resolution
behavior remain to be fully specified.

### Labeled `break` and `continue`

Champion issue: https://github.com/dotnet/csharplang/issues/9875  
Spec: https://github.com/dotnet/csharplang/blob/c4ec6fb60c2e174b1abb6c019f22bb15b9b13f6c/proposals/labeled-break-continue.md

The proposal is to allow `break` and `continue` to take an optional label identifying a lexically
enclosing loop or, for `break`, a `switch`. The target construct is marked using C#'s existing label
syntax, and `break label;` or `continue label;` behaves as if the statement were written directly inside
the targeted construct.

The motivation is the familiar pattern of nested loops where the interesting control-flow action belongs
to an outer loop rather than the immediately enclosing one. Two recent examples came up in the same week:
a Roslyn search that walks instance constructors in an outer loop and members in an inner loop and needs
to stop the outer walk as soon as a match is found, and a thread-pool routine where an inner spin-wait
needs to succeed and then have the outer work-distribution loop `continue` to fetch the next unit of work.
Today the workarounds are boolean flags threaded through the loops, early method extraction, or `goto` to
a label placed at a somewhat arbitrary point in the code. Labeled `break` and `continue` address all of
these directly.

We noted that the feature is a composition of three things the language already has: labels, `break`, and
`continue`. The scoping rules that keep `goto` from jumping across `finally`, into the middle of a `try`,
and so on apply unchanged, so there is no new validation burden. Labels cannot be crossed through a
lambda body, just as `goto` cannot. `continue` is still only valid against loops, not `switch`. Most of
our mainstream peer languages — Java, JavaScript, Kotlin, Rust, and Go — already have this facility, and
the design space is well understood.

The discussion was brief because the room saw no points of controversy. A few observations were made in
passing: labels have always sat a little awkwardly in C# as things that attach to statements rather than
existing as independent constructs, and this feature finally gives that shape a second, natural use
beyond `goto`. A potential future direction was floated where an iteration variable name (for example, the
`i` of a `for` loop) could itself serve as the target of `break`/`continue`, but we preferred to keep the
initial feature focused on explicit labels, which avoid any ambiguity with variable names in more complex
nested scopes.

#### Conclusion

We will allow labels on `break` and `continue` and proceed with the feature. It is small enough to fit in
opportunistically as scheduling allows.
