1. Introduction
You know what's interesting? A lambda expression by itself is like a recipe without a pot. It describes what to do, but it needs a "container" to live in the code. In C# there are ready-made generic delegate types for that: Func, Action and Predicate.
Think of them as ready-made molds for your lambdas — pick the right one and pour your logic in. No custom reinvented delegates when you've already got what you need.
Bit of history
At first, when you needed to pass a function as a parameter, you had to declare your own delegate type. That was long, annoying and looked like this:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
When Func, Action and Predicate landed in C#, you could forget about manual delegate declarations in 90% of cases. Now it looks way simpler and more universal:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — a function with a return
Syntax and purpose
Func is a generic delegate that takes from 0 to 16 parameters and returns a value.
Func<int, int, int> sum = (x, y) => x + y;
Func<type1, type2, ..., typeN, TResult> — all type parameters up to the last one are arguments, the last one is the return type.
Examples
1. Sum of two numbers
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Square a number
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. No parameters
Func<string> greet = () => "Hello, lambda!";
Console.WriteLine(greet());
Visual scheme
| Signature | Example | Description |
|---|---|---|
|
|
Takes a int, returns an int |
|
|
Two int, returns an int |
|
|
Takes nothing, returns a string |
How it looks in your app
Say in our mini-app (a mock "User directory") we have a list of numbers — we want to apply different processing to it. For example, square them or add with a fixed number:
List<int> numbers = new() { 1, 2, 3, 4, 5 };
Func<int, int> square = x => x * x;
var squares = numbers.Select(square);
Console.WriteLine(string.Join(", ", squares)); // 1, 4, 9, 16, 25
3. Action<T> — an action without a return
Action is a generic delegate for methods that do something (for example, print to the screen) but don't return anything.
It can take from 0 to 16 parameters, but always returns void.
Examples
1. Print to the screen
Action<string> print = text => Console.WriteLine("Data: " + text);
print("Hello, world!");
2. Action with no parameters
Action greet = () => Console.WriteLine("Welcome'!");
greet();
3. Action with multiple parameters
Action<int, int> showSum = (a, b) => Console.WriteLine($"Sum: {a + b}");
showSum(2, 3); // Sum: 5
Visual scheme
| Signature | Example | Description |
|---|---|---|
|
|
No parameters, no return |
|
|
One parameter |
|
|
Multiple parameters |
In our app
Add a method to the user directory to print all names:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("User': " + name);
names.ForEach(printName);
// or like this: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — yes or no?
When you need a function that returns only true or false for a single parameter, use Predicate<T>. It's just a delegate that takes one parameter and returns a bool.
Predicate is the official "we need a boolean check" wrapper for a lambda.
Examples
1. Check if a number is greater than 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Use with List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // uses Predicate<int>
Console.WriteLine(found); // 7
3. Are they all adults?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll accepts Predicate<int>
What's the difference from Func<T, bool>?
Practically, they're fully interchangeable. Even Microsoft docs say: "Predicate<T> is just Func<T, bool> for specific APIs". But some standard library methods ask specifically for Predicate.
5. How lambdas "fit" into Func, Action, Predicate
When you write a lambda, C# analyzes: "Oh, its shape matches the required delegate — we can substitute it!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Lambdas everywhere! But under the hood — three different delegates with different signatures.
Practical example in "real" code
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Function returning only adult users (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("List of adults: " + string.Join(", ", adults.Select(u => u.Name)));
And if we want to print all user names with an Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Get their names (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Table for clarity
| Delegate | Signature | Lambda example | Where used |
|---|---|---|---|
|
T → U | |
Select, any transformations |
|
T → void | |
ForEach, action methods |
|
T → bool | |
Find, Exists, filters |
6. Examples with the internal app: step by step
Let's extend our small "User directory" app. Suppose we have this User class:
public class User
{
public string Name { get; }
public int Age { get; }
public bool IsActive { get; set; }
public User(string name, int age)
{
Name = name;
Age = age;
IsActive = true;
}
}
1. Func<User, bool> — check if a user is an adult
Func<User, bool> isAdult = user => user.Age >= 18;
Use in LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — find an active user
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — deactivate a user
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — get a short description
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
All these lambdas are perfectly good "code-as-data" that you can pass to methods, store in variables, and compose.
7. Non-obvious nuances and common mistakes
1. A lambda must match the delegate signature. If the signature doesn't match, you'll get a compile error.
Func<int, string> wrong = x => x * 2; // error: string expected, got int
// Correct:
Func<int, string> right = x => (x * 2).ToString();
2. Remember void vs return. Action doesn't return a value — try assigning something like Action<int> a = x => x * x; and it won't work because a value is returned though it shouldn't.
3. Predicate<T> and Func<T, bool> are often interchangeable, but not always. Sometimes collection methods expect specifically Predicate<T>, sometimes Func<T, bool>. Direct assignment doesn't work without explicit wrapping.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // error
// But:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // can convert via constructor
GO TO FULL VERSION