Delegații personalizați (declarați cu delegate la nivel de namespace) sunt uneori necesari și justificați, în special atunci când numele tipului adaugă claritate semantică unui API public. Există, însă, o problemă care apare rapid în proiecte reale: multe semnături de metode se repetă structural, chiar dacă contextele sunt diferite.
Considerați următoarele declarații care ar putea exista în același proiect:
Toate trei descriu exact aceeași semnătură: două string-uri ca parametri, fără valoare returnată. Cu toate acestea, din perspectiva compilatorului, sunt tipuri distincte și incompatibile. O variabilă de tip NotificareClient nu poate primi o metodă atribuită printr-o variabilă de tip LogHandler, chiar dacă semnăturile sunt identice. Această incompatibilitate structurală este o sursă de fricțiune inutilă.
.NET rezolvă această problemă prin trei familii de tipuri delegat generice și predefinite: Action, Func și Predicate. Acoperă aproape toate cazurile uzuale fără declarații personalizate și elimină redundanța descrisă mai sus.
Action - metode fără valoare de retur
Action reprezintă orice metodă care returnează void. Este un tip generic cu variante pentru orice număr de parametri, de la zero la șaisprezece:
Action// void fara parametriAction<T>// void cu un parametru de tip TAction<T1, T2>// void cu doi parametriAction<T1, T2, T3>// void cu trei parametri// ... pana la Action<T1, ..., T16>
Tipul T, T1, T2 etc. sunt parametri de tip generici. Compilatorul îi substituie cu tipurile concrete specificate la utilizare. Prin urmare, Action<string, string> este un tip delegat care descrie orice metodă care primește doi parametri de tip string și returnează void.
Tipul NotificareClient definit anterior (delegate void NotificareClient(string, string)) este echivalent funcțional cu Action<string, string>. Cele două sunt interschimbabile din perspectiva comportamentului, dar nu din perspectiva tipului: compilatorul le consideră tipuri diferite și nu permite atribuirea directă între ele fără conversie.
Utilizarea Action în locul unui tip personalizat:
// In loc de: NotificareClient notificator = NotificareEmail;Action<string,string>notificator=NotificareEmail;notificator+=NotificareSMS;notificator+=(nr,msg)=>Console.WriteLine("[PUSH] "+nr+": "+msg);notificator("CMD-001","Comanda expediata.");
Rezultatul este identic cu varianta bazată pe tipul personalizat. Avantajul este că nu mai este nevoie de o declarație delegate separată.
Func - metode cu valoare de retur
Func reprezintă orice metodă care returnează o valoare. Prin convenție, ultimul parametru de tip din lista generică este întotdeauna tipul returnat:
Tipul StrategieLivrare definit anterior (delegate double StrategieLivrare(Comanda)) este echivalent cu Func<Comanda, double>: o metodă care primește un obiect Comanda și returnează un double.
Utilizarea Func pentru strategia de livrare:
Variabila calculeazaLivrare poate fi reatribuită oricând cu o altă implementare. Codul care utilizează delegatul pentru a calcula costul (calculeazaLivrare(comanda)) rămâne neschimbat. Aceasta este esența design pattern-ului Strategy: comportamentul unui algoritm este substituit la runtime fără a modifica codul care îl consumă.
Predicate - condiții
Predicate<T> este un tip delegat predefinit pentru metode care testează o condiție: primesc un parametru de tip T și returnează bool. Este echivalent structural cu Func<T, bool>, dar cu un nume mai expresiv în contextul operațiilor de filtrare.
Tipul Predicate<T> apare în metodele clasice ale lui List<T>: FindAll, Find, Exists, RemoveAll. Aceste metode acceptă un Predicate<T> și operează pe elementele listei care satisfac condiția:
Diferența dintre Predicate<T> și Func<T, bool>
Deși Predicate<T> și Func<T, bool> descriu aceeași semnătură, compilatorul le tratează ca tipuri distincte. Nu există o conversie implicită între ele, iar API-urile care cer unul nu acceptă celălalt:
Această asimetrie este o consecință istorică: Predicate<T> a fost introdus în .NET 2.0, odată cu genericele, ca parte a API-ului lui List<T>. LINQ a apărut în .NET 3.5 și a ales în mod consecvent Func<T, bool> pentru consistență cu restul funcțiilor de ordin superior. Ambele variante produc același comportament la runtime; alegerea depinde de API-ul pe care îl utilizezi.
Când să declari un tip delegat personalizat
Cu Action, Func și Predicate disponibile, declararea unui tip delegat personalizat rămâne justificată în situații specifice.
Când numele tipului adaugă claritate semantică. Un parametru de tip NotificareClient comunică intenția mai clar decât Action<string, string>. Când tipul delegat apare în semnăturile publice ale unor clase sau interfețe, un nume semantic îmbunătățește lizibilitatea și documentarea automată a API-ului.
Când tipul este utilizat în contextul evenimentelor. Evenimentele folosesc convențional tipuri delegat cu semnătura (object sender, TEventArgs e). Deși EventHandler<T> acoperă acest caz, există situații în care un tip personalizat este mai potrivit.
Când tipul apare repetat în API-ul public al unui proiect. Dacă același tip de comportament apare în zeci de locuri, un tip cu nume propriu reduce riscul de confuzie și facilitează refactorizarea.
Ca regulă practică pentru cod nou: utilizează Action pentru metode fără retur, Func pentru metode cu retur, Predicate<T> când lucrezi cu metodele clasice ale lui List<T>, și declară un tip personalizat numai când numele său adaugă valoare semantică vizibilă.