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.
GO TO FULL VERSION