CodeGym /Courses /C# SELF /Combining lambda expressions and delegates

Combining lambda expressions and delegates

C# SELF
Level 50, Lesson 4
Available

1. Introduction

Delegate is a type that describes a function "signature": what parameters it takes, what it returns. It's like a passport for a function or a club pass for code: if the signature matches — come in, if not — sorry, you won't be allowed.

Example:

public delegate int Operation(int a, int b);

This delegate represents a function that takes two int and returns an int. This is how programming looked "before lambdas" (fresh, but not very convenient):

Operation add = delegate (int x, int y)
{
    return x + y;
};

With lambdas the code became more concise:

Operation add = (x, y) => x + y;

Fun fact: In C# a lambda expression is just a way to create an "anonymous function" that the compiler automatically turns into an instance of a delegate of the right type. And that makes the relationship between them almost indistinguishable!

Built-in delegates: Func, Action, Predicate

To avoid creating endless custom delegates for every case, C# has "generic" function types:

  • Func<T1, T2, ..., TResult> — a generic delegate that takes parameters (T1, T2, ...) and returns a value of type TResult.
  • Action<T1, T2, ...> — almost the same, but returns nothing (void).
  • Predicate<T> — a generic delegate that takes one parameter and returns bool (a favorite for filters).
Delegate Return type Syntax example
Func<int, int>
int
Func<int, int> f = x => x + 1;
Action<string>
void
Action<string> a = s => ...;
Predicate<double>
bool
Predicate<double> p = d => ...;

Example:

Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;

2. Lambda expressions "inside" a delegate

Passing a lambda to a method that expects a delegate

Any method that accepts a delegate can also "eat" a lambda, as long as its signature matches what's expected.

public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
    int result = operation(a, b);
    Console.WriteLine($"Result: {result}");
}

// Call with a lambda
CalculateAndShow(5, 7, (x, y) => x * y); // Result: 35

Example with Action

void ProcessString(string message, Action<string> processor)
{
    processor(message);
}

// Use a lambda
ProcessString("Hello, world!", s => Console.WriteLine(s.ToUpper()));

Example with Predicate

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);

That's when you realize: why invent custom delegates if there's a proven "wheel" from Microsoft.

How delegates and lambdas work together

flowchart TD
    A[Lambda expression] -->|Creates| B((Delegate))
    B -->|Passed as argument| C[Method]
    B -->|Stored| D[Property/Field/Collection]
    C -->|Invokes| E[Lambda code]

3. Passing a lambda as a delegate in your own methods

If your own method uses delegates, lambdas make usage super flexible.

Example: our mini-calculator

// Define a method to work with operations
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
    return operation(a, b);
}

// Apply different operations (lambdas in action!)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);

Console.WriteLine(sum);      // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max);      // 4

See how elegantly the logic changes, instead of three separate methods?

4. Lambdas + delegates in LINQ (and beyond)

In LINQ methods expect a "small logical function", i.e. a delegate. Almost all standard LINQ methods accept these types:

  • Func<T, bool> for filtering (Where)
  • Func<T, TResult> for transformations (Select)
  • Func<T, TKey> for sorting (OrderBy)
List<string> names = new List<string> { "Anna", "Oleg", "Viktor", "Yana" };

// Get only names longer than 3 characters
var longNames = names.Where(name => name.Length > 3);

// Transform names to uppercase
var upperNames = names.Select(name => name.ToUpper());

LINQ is "lambdas on steroids" because anything you can pass as a delegate you can pass as a lambda!

5. Combining: storing and passing delegate-lambdas

List of functions

A lambda is not just a temporary anonymous method. You can store a set of lambdas in a collection and use them dynamically.

List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
    (x, y) => x + y,
    (x, y) => x - y,
    (x, y) => x * y,
    (x, y) => x / y
};

foreach (var op in operations)
{
    Console.WriteLine(op(10, 2));
}

This pattern often appears in tests, handlers, and plugins.

Dictionary of delegates

And now, if you're a fan of plug-and-play architecture:

var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
    { "plus", (x, y) => x + y },
    { "minus", (x, y) => x - y },
    { "pow", (x, y) => (int)Math.Pow(x, y) }
};

string command = "pow"; // Simulate user input

if (mathFuncs.TryGetValue(command, out var operation))
{
    Console.WriteLine(operation(2, 5)); // 32
}
else
{
    Console.WriteLine("Command not found");
}

6. Useful nuances

Lambda-delegates: returning a delegate from a method

You can return delegates (and therefore lambdas) from methods — it's basically a function factory!

Func<int, int> GetMultiplier(int factor)
{
    // Return a lambda-delegate that multiplies by factor
    return x => x * factor;
}

var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15

var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20

That's how a closure becomes a real tool, not a toy!

Composing functions: Combine, Delegate.Combine and multicast delegates

Sometimes you want one "invoke" to run multiple functions. For example, event handling in a GUI: click a button — multiple handlers respond.

Action and delegates allow you to "chain" multiple functions:

Action<string> pipeline = s => Console.WriteLine("Step 1: " + s);
pipeline += s => Console.WriteLine("Step 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("Step 3: " + s.Length);

pipeline("test");
// Prints:
// Step 1: test
// Step 2: TEST
// Step 3: 4

Convenient for "pipeline" scenarios or event handling.

Important: For functions that return values, the result will be only from the last call ("the tail of the function"). For Action — all are executed, since void.

Using lambda-delegates as callback parameters

Classic scenario — "call me when you finish".

void DownloadFile(string url, Action<string> onFinish)
{
    // Simulate downloading...
    System.Threading.Thread.Sleep(500);// (Simulate long work, don't use in real GUI apps)
    onFinish($"Download '{url}' finished!");
}

DownloadFile("http://example.com", msg => Console.WriteLine(msg));

In real apps (e.g. when working with APIs, DBs, files) — this is a standard pattern.

Higher-order function: example filter by predicate

Lambda expressions and delegates let you write generic functions that accept other functions as parameters. This is the basis of the "higher-order function" pattern.

List<int> Filter(List<int> source, Predicate<int> condition)
{
    List<int> result = new List<int>();
    foreach (var item in source)
        if (condition(item)) result.Add(item);
    return result;
}

// Use different lambdas:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);

You can even create "lambda factories":

Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
    x => x >= min && x <= max;

Predicate<int> inRange = GetRangeChecker(1, 10);

var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10

7. Common mistakes and nuances of combining

If parameter signatures don't match, the compiler will grumble (CS1660: "Cannot convert lambda expression").

A lambda can unexpectedly "remember" variables that later change (closure) — be careful with captured variables.

Multicast delegates with return values return only the result of the last handler — often a surprise for beginners.

In places where Expression<Func<int,bool>> is required, you can't substitute a statement-lambda — you need an expression-lambda.

2
Task
C# SELF, level 50, lesson 4
Locked
Using Func and Action
Using Func and Action
1
Survey/quiz
Delegates - a Type for Lambda Expressions, level 50, lesson 4
Unavailable
Delegates - a Type for Lambda Expressions
The Connection Between Lambda Expressions and Delegates
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION