← Open in the full interactive course (progress tracking, search & more)

A lambda parameter that needs ref used to force you to spell out the type too — even when the compiler already knew it. C# 14 finally lets the modifier stand on its own.

This Part opened with a tour of what's new in C# 14 (323), then walked through extension members (324), field-backed properties (325), null-conditional assignment (326), and enhanced span conversions (327) — four features that each quietly remove a small piece of ceremony from code you already write. This lesson continues that same tour with a fifth, narrower one: a fix specifically for lambda expressions that need a parameter modifier like ref, out, in, or scoped.

You already know lambda syntax from lesson 101, and how lambdas compile down to delegates from lesson 189. This lesson assumes all of that and focuses narrowly on what's new: before C# 14, adding a modifier to a lambda parameter forced you to also write out that parameter's full type. C# 14 lifts that restriction, letting the compiler infer the type the same way it already could for an ordinary, unmodified implicit parameter.

What Is It?

The Simple Explanation

Starting in C# 14, you can write (ref x) => x *= 2 instead of being forced to write (ref int x) => x *= 2. The modifier — ref, out, in, or scoped — no longer drags an explicit type along with it. The compiler figures the type out from context, exactly like it already does for a plain implicit parameter.

The Technical Definition

Prior to C# 14, an implicitly-typed lambda parameter (one with no type written before it) could not carry a parameter modifier — the moment you needed ref/out/in/scoped, the compiler required you to switch that parameter to explicitly-typed form as well. C# 14 removes that coupling: a modifier-carrying lambda parameter can now remain implicitly typed, and the compiler infers its type from the target delegate type's corresponding parameter — the same target-typing mechanism ordinary implicit lambda parameters have always relied on.

Before C# 14

C# 14

Why Does It Exist?

The Problem — a Restriction With No Real Justification

Lesson 228 covered ref, in, and out on ordinary method parameters, and lesson 229 covered ref struct types, where the scoped modifier controls how long a by-ref value is allowed to live. None of that changed in C# 14 — what changed is that lambdas consuming those modifiers used to pay an unrelated tax. The compiler was already perfectly capable of inferring an implicit lambda parameter's type from the delegate it was being assigned to; it simply refused to do that inference once any modifier appeared on the parameter. That refusal wasn't protecting you from an ambiguity — the type was still fully determined by the target delegate — it was just an unimplemented case in the type-inference rules that nobody had gotten around to lifting.

The Solution — Extend the Same Inference the Compiler Already Does

C# 14 closes the gap directly: the same target-typing that infers an ordinary implicit parameter's type from the delegate's Invoke signature now also applies when that parameter carries ref, out, in, or scoped. No new capability was added — ref lambda parameters already worked before C# 14 — the fix is purely about no longer forcing you to restate a type the compiler already knows.

Big Picture — the Four Modifiers Affected

ref
Parameter is passed by reference; the lambda body can read AND write back to the caller's variable
out
Parameter is passed by reference and must be assigned inside the lambda before it returns
in
Parameter is passed by reference but read-only inside the lambda — no copy, no mutation
scoped
Restricts how long a by-ref or ref struct parameter is allowed to escape — relevant with types like Span<T>

All four already worked on explicitly-typed lambda parameters before C# 14. This lesson is exclusively about no longer needing the explicit type alongside them.

How It Works

FROM A MODIFIER-ONLY LAMBDA TO A COMPILED DELEGATE
1. A DELEGATE TYPE DECLARES A ref (OR out/in/scoped) PARAMETER
2. A LAMBDA IS ASSIGNED TO A VARIABLE OR PARAMETER OF THAT DELEGATE TYPE
3. THE COMPILER READS THE MODIFIER FROM THE LAMBDA AND THE TYPE FROM THE TARGET DELEGATE
4. THE COMPILED LAMBDA IS INDISTINGUISHABLE FROM ONE WRITTEN WITH AN EXPLICIT TYPE

Simple Example

delegate void RefAction<T>(ref T value); // Before C# 14 — modifier forces an explicit type RefAction<int> doubleItOld = (ref int x) => x *= 2; // C# 14 — modifier alone is enough; int is inferred from RefAction<int> RefAction<int> doubleIt = (ref x) => x *= 2; int number = 21; doubleIt(ref number); Console.WriteLine(number); // 42 // The same relief applies to out, in, and scoped: delegate bool TryParseAction<T>(string text, out T result); TryParseAction<int> tryParse = (text, out result) => int.TryParse(text, out result);

Meaning: Nothing about what the lambda does changed — doubleIt and doubleItOld behave identically. The only difference is that the second, C# 14 version stopped repeating a type the compiler was always going to look up anyway.

Real-World Example — Mutating a Span In Place

Code that works directly with Span<T> for performance reasons — the world lesson 229's ref struct types and lesson 327's span conversions both live in — often wants to apply a small transformation to each element without allocating a new array. A helper that accepts a ref-based callback is a natural fit, and it's exactly the kind of call site that used to force full lambda parameter types on every caller:

delegate void RefAction<T>(ref T item); static void ForEachRef<T>(Span<T> span, RefAction<T> action) { foreach (ref var item in span) action(ref item); } Span<int> prices = stackalloc int[] { 10, 20, 30 }; // C# 14 — no need to write "(ref int p)"; the Span<int> target already says int ForEachRef(prices, (ref p) => p += p / 10); // apply a 10% markup, in place foreach (var p in prices) Console.WriteLine(p); // 11, 22, 33

The callback here mutates each element directly, with no intermediate array and no allocation — a natural companion to the zero-allocation style of coding this Advanced tier's Memory & Performance material builds toward. C# 14's contribution is small but genuinely felt at this call site: one less type to type, on every single call like this in a codebase that leans on the pattern.

Analogy

A Pre-Addressed Envelope With a Special Handling Stamp

Imagine a pre-addressed, pre-stamped envelope — the recipient's address is already printed on it, so you never write it out yourself; you just drop your letter in and mail it. Now imagine that whenever you wanted to add a "Certified Mail" stamp, the post office's old rule forced you to also hand-write the full address again on a sticker, even though it was already printed right there on the envelope — as if adding one small instruction meant redoing work that was already done for you. C# 14's change is the post office finally dropping that rule: stamp it "Certified" if you need to, and the already-known address is still just the already-known address. The modifier is the stamp; the inferred type is the address that was never actually in question.

Under the Hood

A lambda expression, as lesson 189 covered, does not have a fixed type of its own in the way a variable does — it only gets a concrete type once it's assigned to (or passed as) something with a known delegate type, its target type. When the compiler sees an implicitly-typed parameter, it looks at that target delegate's Invoke method to learn the parameter's type. Before C# 14, the moment the lambda parameter carried a modifier, this lookup was simply skipped — the compiler bailed out and demanded an explicit type instead of consulting the target delegate for that parameter. C# 14's change is narrowly scoped to that lookup: it now also inspects the target delegate's modifier (ref/out/in) and type together, and cross-checks that what you wrote as a modifier on the lambda parameter is compatible with what the target delegate declares. Nothing changed about how modifier-carrying parameters are represented at the IL level — a compiled lambda with an inferred ref parameter is identical to one written the old, fully explicit way. This is purely a front-end, source-level relaxation of an inference rule.

One consequence worth being precise about: this still requires a target type. A modifier-carrying lambda still cannot be assigned to var on its own — there is no natural delegate type for a lambda with a ref parameter the way there sort of is for a plain one using Func<>/Action<>. You still need an explicit delegate type in the picture (a custom delegate like RefAction<T> above, since Func<> and Action<> don't declare ref/out/in parameters at all) — C# 14 just stops making you restate that delegate's parameter type a second time once you've already named it once.

Common Confusion

1. "C# 14 added ref/out/in/scoped support to lambdas" — no, that already existed

Lambdas have been able to take ref, out, and in parameters for a long time, provided you spelled out the type. Nothing about what a lambda can do changed in C# 14 — only how much you're forced to type when doing it.

2. "Now the type can be inferred from thin air" — no, a target delegate type is still required

The inference isn't magic — it's still reading the type off a known target delegate, exactly like ordinary implicit lambda parameters always have. If there's no delegate type in the picture (nothing for the lambda to be assigned to or passed as), there's still nothing for the compiler to infer the type from.

Common Mistakes

Mistake 1 — Expecting this to work with Func<> or Action<>

Trying Func<int, int> f = (ref x) => x; and being confused it doesn't compile. Func<> and Action<> don't declare any ref/out/in parameters in the first place — a modifier-carrying lambda still needs a custom delegate type (or one from a library) whose signature actually has that modifier on the matching parameter.

Mistake 2 — Assuming this makes modifier-carrying lambdas simpler to reason about than they were

Treating the shorter syntax as license to reach for ref lambdas more casually, without the same care lesson 228 taught for ref/out/in on ordinary methods. The semantics — aliasing, definite assignment for out, read-only enforcement for in — are exactly as strict as before; only the amount of typing changed, not the amount of care the modifier still demands.

When Should I Use It?

Rule of thumb: If you'd already omit the type on a plain lambda parameter in this position, omit it on the modifier-carrying one too — as of C# 14, the two follow exactly the same rule.

Mental Model

Before C# 14: modifier on a lambda parameter ⇒ explicit type required
C# 14: modifier on a lambda parameter ⇒ type still inferred from the target delegate, same as any other implicit parameter

Remember: nothing about what ref/out/in/scoped DO changed — only whether you're forced to also spell out a type the compiler could already see.

Key Takeaway


Check Your Understanding

You've seen exactly what changed about lambda parameter modifiers in C# 14 — and, just as importantly, what didn't. Let's confirm it clicked.

1. Prior to C# 14, why did (ref x) => x *= 2; fail to compile when assigned to a RefAction<int> delegate?

Show answer

Correct: B

Why B is correct: The type was always determinable from the target delegate — the restriction was specifically that the compiler's inference logic bailed out the moment a modifier appeared, not that the type was genuinely ambiguous.

Why A is incorrect: Lambdas with ref parameters worked long before C# 14, provided the type was written explicitly — the capability existed, only the implicit-typing shortcut was missing.

Why C is incorrect: Custom delegates with ref parameters, like RefAction<T>, are ordinary, valid C# and have been for a long time.

Why D is incorrect: Compound assignment inside a lambda body is ordinary, valid C# with no connection to this restriction.

Reinforcement: The old restriction was about inference being skipped, not about a genuine ambiguity or a missing capability.

2. Which statement about C# 14's lambda parameter modifier change is accurate?

Show answer

Correct: B

Why B is correct: This is precisely the mechanism the lesson walked through — target-typed inference, extended to also cover parameters that carry a modifier, still anchored to a known target delegate type.

Why A is incorrect: A modifier-carrying lambda still has no natural type of its own — it still requires an explicit target delegate type somewhere in the assignment or call.

Why C is incorrect: The four modifiers already existed; C# 14 didn't introduce a new one, it only changed whether the existing ones require an explicit type alongside them.

Why D is incorrect: Func<> and Action<> still don't declare any ref/out/in parameters — this feature only helps once you're already using a delegate type that does.

Reinforcement: Same inference mechanism as always, just no longer blocked by the presence of a modifier.

3. A developer writes Func<int, int> f = (ref x) => x; and it fails to compile even on C# 14. Why?

Show answer

Correct: B

Why B is correct: The feature still needs a target delegate whose corresponding parameter actually declares the modifier being used — Func<> never declares ref parameters, so there's nothing to match against, regardless of C# version.

Why A is incorrect: This is a standard language feature in C# 14, not an opt-in one requiring separate configuration.

Why C is incorrect: The opposite is true — C# 14 made ref lambda parameters easier to write, not unavailable.

Why D is incorrect: Single-letter lambda parameter names are ordinary and unrelated to this restriction.

Reinforcement: The modifier still has to match something the target delegate type actually declares — this feature doesn't invent capability the delegate type doesn't have.

4. True or false: C# 14's lambda parameter modifier change alters the runtime semantics of how ref parameters behave inside a lambda.

Show answer

Correct: B

Why B is correct: As the Under the Hood section explained, this is a front-end, compile-time-only change to how the type is inferred — the resulting compiled lambda is indistinguishable from one written the old, fully explicit way.

Why A is incorrect: Aliasing behavior for ref parameters is unchanged — only how much typing is required to declare one changed.

Why C is incorrect: ref parameters remain passed by reference exactly as before; the feature never touches passing convention.

Why D is incorrect: The feature is a real, active part of C# 14 — it's just scoped narrowly to inference, not to a deprecated no-op.

Reinforcement: A shorter way to write the same thing, compiling to the same result — not a behavioral change.

Next in the C# 14 tour: partial constructors (329) — a small extension of a pattern you already recognize from source generators.


dotnetmadeeasy.com — Learn C# and .NET, the right way.