C# Lambda Return Value: Syntax and Behavior
c# lambda return value: Learn how C# lambda expressions return values, how the compiler infers return types, and when to use expression or statement lambdas.
When a C# lambda expression returns a value, the compiler determines the return type from the expression body or the delegate target type. A lambda like (int x) => x * 2 returns an int, while (string s) => s.Length > 0 returns a bool. Understanding how the C# lambda return value is produced starts with the distinction between expression lambdas and statement lambdas.
Lambda Return Value Syntax in C#
An expression lambda has a single expression on the right side of the => operator, and that expression's value becomes the return value:
Func<int, int> square = x => x * x;
The expression x * x evaluates to an int, so the lambda returns an int. No explicit return keyword is needed because the expression itself is the return value.
A statement lambda uses a block body and requires an explicit return statement:
Func<int, int> square = x => { return x * x; };
Both forms produce the same result, but the expression lambda is more concise and is the preferred form when the logic fits in a single expression.
How C# Infers the Lambda Return Type
When you assign a lambda to a delegate type like Func<T, TResult>, the compiler infers the return type from the last expression or the return statements in the body. The delegate's TResult type parameter must match the inferred return type.
Func<int, string> format = x => $"Value: {x}";
Here the interpolated string expression produces a string, so TResult is string. If the inferred type does not match the delegate's TResult, the compiler reports an error.
When the lambda body contains conditional logic, the compiler must verify that all code paths return a compatible type:
Func<int, string> classify = x => { if (x > 0) { return "positive"; } return "non-positive"; };
Both return statements produce a string, so the lambda is valid. If one branch returned an int and the other a string, the compiler would reject the lambda because no implicit conversion exists between the two.
Using Lambda Return Values with Func Delegates
The Func family of delegates is the standard way to represent a lambda that returns a value. Func<TResult> takes no parameters and returns TResult; Func<T, TResult> takes one parameter, and so on up to Func<T1, T2, T3, T4, TResult>.
Func<int, int, int> add = (a, b) => a + b; Func<double, double> half = x => x / 2.0;
The delegate type determines how the lambda is invoked and what the caller receives. When you call the delegate, the returned value is whatever the lambda body produced:
int result = add(3, 4); // result is 7
Lambdas that return values are commonly passed to LINQ methods. Select, Where, and Aggregate all accept delegates that produce return values:
var squares = numbers.Select(x => x * x);
The lambda passed to Select returns the transformed value for each element, and the result is an IEnumerable<int>.
Common Mistakes with Lambda Return Values
One frequent error is using a statement lambda when an expression lambda would work, which adds unnecessary return keywords and braces. Another is mixing return types in a statement lambda body:
Func<int, object> bad = x => { if (x > 0) { return x; // int } return "negative"; // string };
This fails because the compiler cannot find a common type for int and string. The fix is to cast one branch explicitly or change the delegate's TResult to a common base type.
A related mistake is forgetting that void-returning delegates like Action cannot be assigned a lambda that returns a value:
Action<int> action = x => x * 2; // error: cannot convert lambda expression
The expression x * 2 produces a value, but Action<int> expects a void return. Use Func<int, int> instead when the lambda must return a value.
Runtime and Allocation Considerations
Expression lambdas that capture no outer variables are compiled to static methods and cached by the compiler. This means repeated calls do not allocate new delegate instances. Statement lambdas follow the same rule when they capture nothing.
When a lambda captures local variables or instance fields, the compiler generates a closure class to hold the captured state. Each invocation that creates a new closure allocates a new instance:
int factor = 2; Func<int, int> multiply = x => x * factor;
The lambda captures factor, so the compiler creates a closure object. If this code runs inside a loop, a new closure is allocated on each iteration. Moving the lambda creation outside the loop avoids repeated allocation when the captured value does not change.
For LINQ queries over large collections, the allocation cost of closures is usually small compared to the iteration work. But in hot paths where a lambda is created millions of times, hoisting the delegate creation can reduce garbage collection pressure.
When Statement Lambdas Are Necessary
Statement lambdas are required when the logic cannot fit in a single expression. This includes loops, multiple statements, or switch statements inside the body:
Func<int, int> factorial = n => { int result = 1; for (int i = 2; i <= n; i++) { result *= i; } return result; };
The for loop makes a statement lambda necessary. Expression lambdas cannot contain loops or multiple statements.
Statement lambdas also allow early returns, which can make certain algorithms clearer:
Func<string, bool> isValid = input => { if (string.IsNullOrWhiteSpace(input)) { return false; } return input.Length <= 20; };
The early return false avoids nesting the length check inside an if block. This is a readability choice; the same logic could be written as a single expression with the conditional operator, but the statement form is often easier to follow.
Return Value Conversion and Target Typing
C# 10 introduced natural type for lambdas, which means the compiler can infer a delegate type when the lambda is not assigned to an explicit delegate type. This affects how return values are typed in var declarations:
var lambda = (int x) => x * 2;
The compiler infers Func<int, int> for lambda. Before C# 10, this code required an explicit delegate type. The return value type is still inferred from the expression body, but the delegate type itself is now inferred rather than declared.
Target typing also matters when a lambda is passed directly to a method parameter. The method's parameter type determines the delegate type, and the lambda's return value must be implicitly convertible to the delegate's TResult. This is why a lambda returning int can be passed to a parameter of type Func<int, long>: the int result is implicitly converted to long at the call site. The same conversion rules apply inside the lambda body, so the compiler checks each return statement against the expected delegate return type.