C# 2 anonymous methods: syntax, captured variables, omitting the parameter list, static anonymous methods, the lack of expression-tree conversion, and how they compare with lambdas.
Anonymous methods are the C# 2 syntax for inline delegates: delegate (int x) { return x * 2; }. They compile the same way as lambdas, into a method plus a closure class if they capture variables. Lambdas replaced them in C# 3 because they are shorter, can have expression bodies and can become expression trees, which anonymous methods cannot. The one thing anonymous methods still do is omit the parameter list entirely, delegate { ... }, to ignore all parameters. In new code I use lambdas, with discards for unused parameters.
Anonymous methods (C# 2.0) are inline, unnamed methods written with the delegate keyword: delegate (int x) { return x * 2; }. Lambdas (C# 3.0) replaced them for almost every purpose. Both compile to the same thing (a compiler-generated method, plus a closure class if they capture). I'd explain the differences, the one trick anonymous methods still have, static support, and whether to use them today.
// Anonymous method (C# 2)
Func<int, int> doubleIt = delegate (int x) { return x * 2; };
button.Click += delegate (object? s, EventArgs e) { Save(); };
// Lambda (C# 3+)
Func<int, int> doubleIt2 = x => x * 2;
button.Click += (s, e) => Save();
| Anonymous method | Lambda | |
|---|---|---|
| Syntax | delegate (params) { body } |
(params) => expr or => { body } |
| Expression body | No (block only) | Yes |
| Omitting the parameter list | Yes: delegate { ... } converts to any delegate type whose parameters you don't use |
No (must declare parameters or discards) |
| Parameter type inference | No — types must be written if parameters are declared | Yes |
Expression trees (Expression<Func<...>>) |
No | Yes — essential for LINQ providers/EF Core |
Natural type (var f = ...) |
No | Yes (C# 10) |
| Attributes, explicit return type, default parameters | No | Yes (C# 10/12) |
async |
Yes (async delegate { ... }) |
Yes |
static |
Yes (C# 9) | Yes (C# 9) |
| Discard parameters | Yes (C# 9: delegate (int _, int _) { }) |
Yes |
| Captures (closures) | Same mechanism | Same mechanism |
The parameterless anonymous method converts to a delegate of any parameter list (as long as there are no out parameters), because you omit the list entirely:
EventHandler<OrderPlacedEventArgs> h = delegate { _counter.Increment(); }; // ignores (sender, e)
Action<int, string, Guid> ignoreAll = delegate { Console.WriteLine("called"); };
With lambdas you must write the parameters (or discards): (_, _) => _counter.Increment(). Since C# 9 lambda discards make this almost as concise, so the advantage is marginal. It's mainly handy when the parameter list is long or awkward to spell.
(Historically there was also a quirk: a parameterless anonymous method could be convertible to multiple delegate types, sometimes causing overload ambiguity.)
Yes. Since C# 9, both static lambdas and static anonymous methods are allowed:
Func<int, int> square = static delegate (int x) { return x * x; };
static forbids capturing locals, parameters or this, guaranteeing no closure allocation and no accidental captures.
Almost never. Lambdas are shorter, support type inference, expression bodies, expression trees, natural types, attributes and default parameters, and every C# developer reads them fluently. I'd only consider delegate { ... } in rare cases where ignoring a long parameter list improves readability — and even then (_, _) => is usually clear enough. In legacy code, I'd leave existing anonymous methods alone unless touching that code anyway; converting is a safe, IDE-assisted refactoring.
In a legacy WinForms codebase migrated to .NET 8, a code-analysis pass flagged hundreds of anonymous methods. Rather than a risky mass rewrite, we let the IDE "convert to lambda" fixer run file by file during normal feature work, and added an .editorconfig rule preferring lambdas. One conversion revealed a bug: an anonymous method captured a loop variable in a for loop — same capture semantics as lambdas — which we fixed by copying to a local.
Checks depth of language knowledge and ability to work with legacy code.
Green flags: knows the syntax and that it compiles like a lambda; names the omitted-parameter-list feature; knows they cannot be expression trees; prefers lambdas in new code.
Red flags: confuses anonymous methods with anonymous types; thinks they do not capture variables.
Both anonymous methods and lambdas compile to:
<>c class with a cached delegate in a static field, if they capture nothing,this.The IL is the same whichever syntax you used.
delegate { ... } (no parameter list) is compatible with any delegate type where:
out parameters,return statements (or void).This made them useful in C# 2 for events and callbacks where parameters were irrelevant. With overloaded methods taking different delegate types, a parameterless anonymous method can be ambiguous — the compiler may report CS0121.
When a lambda targets Expression<TDelegate>, the compiler emits code that builds an expression tree (calls to Expression.Lambda, Expression.Call, etc.) instead of compiling a method. The C# design team only implemented this for lambdas with expression bodies (and limited statement forms). Anonymous methods always have block bodies with statements — not representable in LINQ expression trees, which are expression-oriented.
| Version | Feature |
|---|---|
| C# 3 | Lambdas, expression trees |
| C# 5 | async lambdas |
| C# 9 | static lambdas/anonymous methods, discard parameters |
| C# 10 | Natural types, attributes on lambdas, explicit return types |
| C# 12 | Default parameter values, params in lambdas |
| C# 14 | Parameter modifiers (ref, out, scoped) without explicit types |
Anonymous methods received only static and discards — they're effectively frozen.
Both capture variables, not values. A for loop variable captured by either syntax is shared across iterations; foreach variables are fresh per iteration since C# 5. Closures over this keep the object alive as long as the delegate lives.
delegate { } to Action<int, string> and to EventHandler.Expression<Func<int, int>> and read the error.static to an anonymous method that captures a local and read CS8820.namespace Delegates.AnonymousMethods;
public static class Program
{
public static void Main()
{
// Classic anonymous method
Func<int, int> doubleIt = delegate (int x) { return x * 2; };
// Parameter list omitted: converts to any delegate without out parameters
Action noArgs = delegate { Console.WriteLine("no args"); };
Action<int, string> ignoresBoth = delegate { Console.WriteLine("ignored both parameters"); };
EventHandler onTick = delegate { Console.WriteLine("tick"); };
// Captures like a lambda
var threshold = 10;
Predicate<int> isBig = delegate (int x) { return x > threshold; };
threshold = 3; // closure sees the variable, not a copy
Console.WriteLine(isBig(5)); // True
// Static anonymous method (C# 9): capturing would be a compile error
Func<int, int> square = static delegate (int x) { return x * x; };
// Modern equivalents
Func<int, int> doubleLambda = x => x * 2;
EventHandler onTickLambda = (_, _) => Console.WriteLine("tick (lambda)");
// Expression<Func<int, int>> expr = delegate (int x) { return x; }; // CS1946: not convertible to expression tree
noArgs();
ignoresBoth(1, "a");
onTick(null, EventArgs.Empty);
onTickLambda(null, EventArgs.Empty);
Console.WriteLine(doubleIt(4) + doubleLambda(4) + square(3));
}
}
What happens: delegate (x) { ... } style used out of habit.
Why it is wrong: more verbose, no inference or expression trees.
Do instead: use lambdas.
What happens: converting all anonymous methods in one big change. Why it is wrong: churn and risk for no behaviour change; may hide subtle capture issues. Do instead: convert opportunistically with IDE fixes.
What happens: believing anonymous methods capture by value.
Why it is wrong: both capture variables; loop-variable bugs apply equally.
Do instead: copy loop variables to locals before capturing in for loops.
What happens: passing an anonymous method to EF Core's Where.
Why it is wrong: can't convert to Expression<Func<...>>.
Do instead: use expression-bodied lambdas for queryable APIs.
What happens: delegate { } passed to overloaded methods.
Why it is wrong: CS0121 ambiguity.
Do instead: use a lambda with explicit parameters.
this in long-lived subscriptionsWhat happens: anonymous methods subscribed to static events. Why it is wrong: keeps the object alive and can't be unsubscribed without a stored reference. Do instead: store the delegate and unsubscribe, or avoid capturing.
static for non-capturing callbacksWhat happens: callbacks accidentally capture locals in hot paths.
Why it is wrong: closure allocations.
Do instead: mark them static.
async voidWhat happens: async delegate { ... } assigned to Action.
Why it is wrong: exceptions crash the process; completion isn't observable.
Do instead: target Func<Task>.
This is a history-and-maintenance question. Seniors explain that anonymous methods and lambdas share the same compilation model, name the one remaining unique feature, and show they can modernise legacy code safely. I treat migrating to lambdas as a low-risk cleanup that also opens the door to static lambdas and expression trees.
A lambda is an anonymous function, either an expression like x => x * 2 or a statement block. It has no type until it is converted, either to a delegate, which is compiled code, or to an Expression<Func<...>>, which is a data structure EF Core can turn into SQL. Since C# 10 a lambda can have a natural type, so var f = (int x) => x * 2 is a Func<int,int>. When it captures variables, the compiler hoists them into a closure class, which allocates. static lambdas forbid capture. Newer additions are attributes and return types (C# 10), default parameters (C# 12), and C# 14 parameter modifiers like out without writing types.
A local function is a named method declared inside another method. Unlike a lambda it can be recursive, generic, an iterator or async, and called before it is declared. The big practical difference is allocation: if a local function captures variables and is only called directly, the compiler passes them in a struct by reference, so there is no heap allocation, while a capturing lambda always allocates a closure object and a delegate. A static local function cannot capture anything, which guarantees no hidden allocations. I use them for helpers used in one method and for the eager-validation pattern in iterators and async methods.
Last updated 30 Oct 2026.