Lambda Expressions, Func<..., T>
, Action
, and Action<...>
Introducing Lambda Functions
Lambda expressions are a concise way to represent anonymous methods (methods without a name) in C#. They are commonly used in scenarios involving delegates or expressions, where a quick and simple function is needed.
Lambda Functions Across Languages
Many programming languages offer a concept similar to lambda expressions. They may have names like:
- Anonymous functions (JavaScript)
- Arrow functions (JavaScript, TypeScript, Kotlin, etc.)
- Lambdas (Python, Ruby, etc.)
Lambda expressions in C# are functionally similar, allowing for concise inline function definitions.
Lambda Expression Syntax
The basic syntax of a lambda expression in C# uses the =>
operator, which is pronounced as "goes into." Here's a breakdown:
- The left side contains the input parameters (in parentheses).
- The right side contains the expression or the body of the function.
Example
(x, y) => x + y
This is functionally equivalent to a "regular function" that looks like this:
int Add(int x, int y)
{
return x + y;
}
Refresher: Action Methods vs Methods That Return a Value
- Action or void-returning methods perform an action but do not return a value (i.e., they have a void return type).
- Value-returning methods return a value of a specific type.
Func<...>
and Action<...>
Func<T1, T2, ..., TResult>
represents a method that takes input parameters and returns a value of type TResult
.
Each of the types in the Func<...>
delegate represents the type of each parameter, and the last type represents the type of the return value.
For example, a Func<int, int, int>
delegate represents a method that takes two int parameters and returns an int, like this "normal function":
int Add(int x, int y)
{
return x + y;
}
Action<T1, T2, ...>
represents a method that takes input parameters and returns void (i.e., does not return a value).
These delegates are frequently used with lambda expressions to create anonymous methods inline.
Action<string> writeStringToConsole = x => Console.WriteLine(x);
An Action delegate encapsulates a method that returns void. This Action delegate represents a method that takes an object parameter and prints it to the console. The lambda expression defines this action concisely.
Understanding closures
Closures occur when a lambda expression captures variables from its enclosing scope. In other words, the lambda expression "remembers" the variables that were in scope when the lambda was created, even after the scope ends. This can be particularly useful in scenarios where you need to keep track of values outside the function's direct scope.
For example:
int counter = 0;
Func<int, int> incrementCounter = num => {
counter = num + counter;
return counter;
};
Console.WriteLine(incrementCounter(5)); // Output: 5
Console.WriteLine(incrementCounter(7)); // Output: 12
Console.WriteLine(incrementCounter(8)); // Output: 20
In this example, the lambda expression { counter = num + counter; return counter; }
captures the variable counter from its surrounding scope. Each time incrementCounter
is invoked, it increments by the specified amount and returns the current value of counter, even though counter is outside the direct scope of the lambda.
How Closures Work
Closures work because C# handles the captured variables as references, not as values. Even though the function is anonymous and can be executed outside its initial context, the captured variable still exists in memory, and changes to it persist.