CodeGym /Courses /C# SELF /Passing lambda expressions ( ...

Passing lambda expressions ( =>) as a parameter

C# SELF
Level 50 , Lesson 2
Available

1. Introduction

So far you've seen calls to LINQ methods that took a lambda. But how does a C# method know that you can even pass a function to it?

Put simply, delegates are like an interface for functions: you describe the signature (parameter types and return type), and any method (or lambda!) matching that signature can be passed where that delegate is expected.

Remember how you passed a string as a parameter? It's pretty much the same with "logic" — the parameter type is just a delegate.

Delegates: basic theory in plain English

In C# a delegate is a type that describes "a function with this signature".

// Delegate that takes an int and returns a bool
public delegate bool IntPredicate(int x);

Any function compatible with that signature can be assigned to a variable of that type:

bool IsEven(int n) => n % 2 == 0;

IntPredicate pred = IsEven;

And a lambda works too:

IntPredicate pred = x => x % 2 == 0;

Generic delegates: Func, Action, Predicate

  • Func<T1, ..., TResult> — a function that takes parameters T1, ... and returns TResult.
  • Action<T1, ...> — a function that takes parameters and doesn't return a value (void).
  • Predicate<T> — a function that takes T and returns a bool.

2. Passing a lambda into your method

Imagine we're evolving our small teaching app — a console project that works with a list of users. Previously we filtered collections with LINQ, and now we'll write our own method that accepts a lambda-condition.

Creating a method with a lambda parameter

// Define the User class for the example (add to our app)
public class User
{
    public string Name { get; set; }
    public bool IsActive { get; set; }
}

// Method that accepts a list and a delegate-condition (a lambda)
public static List<User> FilterUsers(List<User> users, Predicate<User> predicate)
{
    var result = new List<User>();
    foreach (var user in users)
    {
        if (predicate(user)) // We call the lambda!
            result.Add(user);
    }
    return result;
}

Now you can pass any lambda:

var users = new List<User>
{
    new User { Name = "John", IsActive = true },
    new User { Name = "Peter", IsActive = false },
    new User { Name = "Mary", IsActive = true }
};

// Filter only active users
var activeUsers = FilterUsers(users, user => user.IsActive);

foreach (var user in activeUsers)
    Console.WriteLine(user.Name); // John, Mary

That's it! We passed a piece of logic — a tiny function — as a normal parameter, simply because the FilterUsers method expects a Predicate<User>, and we gave it a matching lambda.

Variant with Func<T, TResult>

Predicate<T> is good when you need a condition (returns a bool). But what if we want to "compute" something for each user?

// Method that applies a function to each element and collects results
public static List<TResult> MapUsers<TResult>(List<User> users, Func<User, TResult> selector)
{
    var result = new List<TResult>();
    foreach (var user in users)
    {
        result.Add(selector(user));
    }
    return result;
}

Usage:

var names = MapUsers(users, user => user.Name.ToUpper());

foreach (var name in names)
    Console.WriteLine(name); // VASYA, PETYA, MASHA

3. Useful nuances

Different ways to pass

You can pass not only a lambda but also a regular method — the signature just needs to match.

// Regular method
static bool NameHasS(User user) => user.Name.Contains("s");

// Passing a regular method:
var usersWithS = FilterUsers(users, NameHasS);
// Passing a lambda
var usersWithA = FilterUsers(users, u => u.Name.Contains("a"));

Or an old-style anonymous method (don't do this):

var usersWithM = FilterUsers(users, delegate(User u) { return u.Name.Contains("m"); });

Modern style — use lambdas!

Passing lambdas to LINQ: what really happens

var result = users.Where(u => u.IsActive).ToList();

Under the hood Where accepts a Func<User, bool>. That means any method that takes a Func<...> can be used the same way!

What if you want two parameters?

// Method that takes two lambdas for filtering
public static List<User> FilterUsersCustom(
    List<User> users,
    Func<User, bool> include,
    Func<User, bool> exclude)
{
    var result = new List<User>();
    foreach (var user in users)
    {
        if (include(user) && !exclude(user))
            result.Add(user);
    }
    return result;
}

Usage:

var customFiltered = FilterUsersCustom(
    users,
    u => u.Name.StartsWith("V"),
    u => u.IsActive == false
);
// Will take only users whose names start with "V" and who are active

Scenario: Filter factory

Console.WriteLine("Enter minimum name length:");
int minLength = int.Parse(Console.ReadLine());

Predicate<User> lengthFilter = user => user.Name.Length >= minLength;
var filteredUsers = FilterUsers(users, lengthFilter);

// Quite interactive and lively!

4. Common mistakes and nuances

Sometimes the compiler can't "infer" the lambda parameter types — especially in complex overload scenarios or when a method expects a delegate with multiple parameters/a concrete return type. In that case you can specify the lambda types explicitly:

FilterUsers(users, (User u) => u.Name.Length > 3);

or even:

MapUsers(users, (User u) => u.Name.ToUpper());

Error: lambda doesn't match the signature

FilterUsers(users, user => Console.WriteLine(user.Name)); // error!  Expected bool, got void

Because a function returning bool is expected, and the lambda returns void (actually nothing explicitly). Pay attention to the return type!

Error: abusing lambdas

If you start passing 10-line lambdas, it's better to extract them into a separate method. It's more readable and easier to debug.

2
Task
C# SELF, level 50, lesson 2
Locked
Advanced Filtering and Transformation of Users
Advanced Filtering and Transformation of Users
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION