1. Introduzione
Sai cosa è interessante? L'espressione lambda in sé è come una ricetta senza pentola. Descrive cosa fare, ma ha bisogno di un "contenitore" per vivere nel codice. In C# per questo ci sono i delegati generici pronti: Func, Action e Predicate.
Immaginali come stampi pronti per le tue lambda — prendi quello giusto e versa la tua logica. Niente bici fatte in casa o tipi di delegati personalizzati, quando hai già tutto a portata di mano.
Un po' di storia
All'inizio, quando serviva passare una funzione come parametro, bisognava dichiarare un tipo di delegato personalizzato. Era lungo, noioso e sembrava così:
delegate int Calculate(int x, int y);
Calculate adder = (a, b) => a + b;
Quando in C# sono arrivati Func, Action e Predicate, si poteva dimenticare la dichiarazione manuale dei delegati nel 90% dei casi. Ora è molto più semplice e universale:
Func<int, int, int> adder = (a, b) => a + b;
2. Func<T, TResult> — funzione con ritorno
Sintassi e scopo
Func è un delegato generico (generic delegate) che prende da 0 a 16 parametri e restituisce un valore.
Func<int, int, int> sum = (x, y) => x + y;
Func<tipo1, tipo2, ..., tipoN, TResult> — tutti i parametri fino all'ultimo sono argomenti, l'ultimo è il tipo restituito.
Esempi
1. Somma di due numeri
Func<int, int, int> sum = (x, y) => x + y;
Console.WriteLine(sum(3, 5)); // 8
2. Elevare al quadrato
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // 16
3. Senza parametri
Func<string> greet = () => "Privet, lambda!";
Console.WriteLine(greet());
Schema visivo
| Firma | Esempio | Descrizione |
|---|---|---|
|
|
Accetta int, restituisce int |
|
|
Due int, restituisce int |
|
|
Non prende nulla, restituisce una stringa |
Come appare nella tua applicazione
Supponiamo che nella nostra mini-applicazione (il finto "Rubrica utenti"), abbiamo una lista di numeri — vogliamo applicare diverse operazioni. Per esempio elevare al quadrato o sommare con un numero fisso:
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> — azione senza ritorno
Action è il delegato generico per metodi che fanno qualcosa (per esempio stampano a schermo), ma non restituiscono nulla.
Può accettare da 0 a 16 parametri, ma restituisce sempre void.
Esempi
1. Stampare a schermo
Action<string> print = text => Console.WriteLine("Dannye: " + text);
print("Privet, mir!");
2. Azione senza parametri
Action greet = () => Console.WriteLine("Dobro pozhalovat'!");
greet();
3. Azione con più parametri
Action<int, int> showSum = (a, b) => Console.WriteLine($"Summa: {a + b}");
showSum(2, 3); // Summa: 5
Schema visivo
| Firma | Esempio | Descrizione |
|---|---|---|
|
|
Senza parametri, senza ritorno |
|
|
Un parametro |
|
|
Più parametri |
Nella nostra applicazione
Aggiungiamo alla rubrica utenti un metodo per stampare tutti i nomi:
List<string> names = new() { "Anna", "Boris", "Vika" };
Action<string> printName = name => Console.WriteLine("Pol'zovatel': " + name);
names.ForEach(printName);
// oppure: names.ForEach(name => Console.WriteLine(name));
4. Predicate<T> — sì o no?
Quando serve una funzione che restituisca solo true o false per un parametro, usa Predicate<T>. È semplicemente un delegato che prende un parametro e restituisce bool.
Predicate è la "wrapper per quando ci serve un controllo booleano".
Esempi
1. Controllare se un numero è maggiore di 5
Predicate<int> isGreaterThanFive = x => x > 5;
Console.WriteLine(isGreaterThanFive(3)); // false
Console.WriteLine(isGreaterThanFive(7)); // true
2. Uso con il metodo List<T>.Find
List<int> values = new() { 2, 4, 7, 10 };
int found = values.Find(isGreaterThanFive); // usa Predicate<int>
Console.WriteLine(found); // 7
3. Tutti sono adulti?
List<int> ages = new() { 12, 19, 34 };
bool allAdults = ages.TrueForAll(age => age >= 18);
// TrueForAll accetta Predicate<int>
Qual è la differenza rispetto a Func<T, bool>?
Di fatto, sono completamente intercambiabili. Anche la documentazione Microsoft dice: "Predicate<T> è semplicemente un Func<T, bool> per API speciali". Però a volte i metodi della libreria standard richiedono proprio Predicate.
5. Come le lambda "si inseriscono" in Func, Action, Predicate
Quando scrivi una lambda, C# analizza: "Oh, la sua forma corrisponde al delegato richiesto — posso sostituirla!"
Func<int, int> f1 = x => x * 2;
Action<string> a1 = text => Console.WriteLine(text);
Predicate<int> p1 = x => x < 10;
Ovunque lambda! Ma sotto il cofano — tre delegati diversi con firme diverse.
Applicazione con codice "reale"
List<User> users = new() {
new User("Anna", 24),
new User("Boris", 17),
new User("Vika", 31),
};
// Funzione che restituisce solo gli utenti adulti (Predicate<User>)
List<User> adults = users.FindAll(user => user.Age >= 18);
Console.WriteLine("Spisok vzroslykh: " + string.Join(", ", adults.Select(u => u.Name)));
E se vogliamo stampare i nomi di tutti gli utenti con Action<User>:
users.ForEach(user => Console.WriteLine(user.Name));
Ottenere i loro nomi (Func<User, string>):
IEnumerable<string> names = users.Select(user => user.Name);
Tabella per chiarezza
| Delegato | Firma | Esempio di lambda | Dove si usa |
|---|---|---|---|
|
T → U | |
Select, qualsiasi trasformazione |
|
T → void | |
ForEach, metodi-azione |
|
T → bool | |
Find, Exists, filtri |
6. Esempi con l'app interna: passo dopo passo
Estendiamo la nostra piccola app "Rubrica utenti". Abbiamo una classe User:
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> — controlliamo se l'utente è maggiorenne
Func<User, bool> isAdult = user => user.Age >= 18;
Usiamo in LINQ:
var adults = users.Where(isAdult);
2. Predicate<User> — cerchiamo un utente attivo
Predicate<User> isActive = user => user.IsActive;
User found = users.Find(isActive);
3. Action<User> — disattiviamo un utente
Action<User> deactivate = user => user.IsActive = false;
users.ForEach(deactivate);
4. Func<User, string> — otteniamo una breve descrizione
Func<User, string> describe = user => $"{user.Name} ({user.Age})";
var descriptions = users.Select(describe);
Tutte queste lambda sono codice come dati: si possono passare ai metodi, conservare in variabili, combinare.
7. Sfumature non ovvie ed errori tipici
1. La lambda deve corrispondere alla firma del delegato. Se la firma non coincide, avrai un errore di compilazione.
Func<int, string> wrong = x => x * 2; // errore: aspettato string, ricevuto int
// Corretto:
Func<int, string> right = x => (x * 2).ToString();
2. Non dimenticare void vs return. Action non restituisce valori — provare a scrivere tipo Action<int> a = x => x * x; non funzionerà, perché ritorna un valore quando non dovrebbe.
3. Predicate<T> e Func<T, bool> sono spesso intercambiabili, ma non sempre. A volte i metodi delle collection si aspettano proprio Predicate<T>, altre volte Func<T, bool>. L'assegnazione diretta non funziona senza wrapping esplicito.
Predicate<int> pred = x => x > 0;
Func<int, bool> func = pred; // errore
// Ma:
Func<int, bool> func2 = x => x > 0;
Predicate<int> pred2 = new Predicate<int>(func2); // si può convertire tramite il costruttore
GO TO FULL VERSION