Delegați
Delegații reprezintă obiecte care indică spre metode. Cu alte cuvinte, delegații sunt pointeri către metode și cu ajutorul delegaților putem apela aceste metode.
Definirea delegaților
Pentru a declara un delegat se folosește cuvântul cheie delegate, urmat de tipul returnat, numele și parametrii. De exemplu:
delegate void Message();
Delegatul Message are tipul de returnare void (adică nu returnează nimic) și nu acceptă niciun parametru. Acest lucru înseamnă că acest delegat poate indica orice metodă care nu acceptă parametri și nu returnează nimic.
Să vedem cum putem folosi acest delegat:
Message mes; // 2. Creăm o variabilă delegat
mes = Hello; // 3. Atribuim acestei variabile adresa metodei
mes(); // 4. Apelăm metoda
void Hello() => Console.WriteLine("Hello FDC.COM");
delegate void Message(); // 1. Declarăm delegatul
Primul pas este definirea delegatului:
delegate void Message(); // 1. Declarăm delegatul
Pentru a utiliza delegatul, declarăm o variabilă a acestui delegat:
Message mes; // 2. Creăm o variabilă delegat
Apoi, atribuim delegatului adresa unei metode (în cazul nostru, metoda Hello). Observați că această metodă are același tip de returnare și același set de parametri (în acest caz, lipsa parametrilor) ca și delegatul.
mes = Hello; // 3. Atribuim acestei variabile adresa metodei
Apoi, apelăm metoda prin intermediul delegatului:
mes(); // 4. Apelăm metoda
Apelul delegatului se face similar cu apelul unei metode.
Delegații nu trebuie neapărat să indice doar metode definite în aceeași clasă în care este definită variabila delegatului. Aceștia pot indica și metode din alte clase și structuri.
Message message1 = Welcome.Print;
Message message2 = new Hello().Display;
message1(); // Welcome
message2(); // Привет
delegate void Message();
class Welcome
{
public static void Print() => Console.WriteLine("Welcome");
}
class Hello
{
public void Display() => Console.WriteLine("Salut");
}
Locul definirii delegatului
Dacă definim un delegat într-un program de nivel superior (top-level program), care este reprezentat implicit de fișierul Program.cs începând cu versiunea C# 10, ca în exemplul de mai sus, atunci delegatul se definește la sfârșitul codului, la fel ca și alte tipuri. Totuși, delegatul poate fi definit și în interiorul unei clase:
class Program
{
delegate void Message(); // 1. Declarăm delegatul
static void Main()
{
Message mes; // 2. Creăm o variabilă delegat
mes = Hello; // 3. Atribuim acestei variabile adresa metodei
mes(); // 4. Apelăm metoda
void Hello() => Console.WriteLine("Hello FDC.COM");
}
}
Sau în afara clasei:
delegate void Message(); // Declarăm delegatul
class Program
{
static void Main()
{
Message mes; // Creăm o variabilă delegat
mes = Hello; // Atribuim acestei variabile adresa metodei
mes(); // Apelăm metoda
void Hello() => Console.WriteLine("Hello FDC.COM");
}
}
Parametrii și rezultatul delegatului
Să vedem cum putem defini și utiliza un delegat care acceptă parametri și returnează un rezultat:
Operation operation = Add; // delegatul indică spre metoda Add
int result = operation(4, 5); // efectiv Add(4, 5)
Console.WriteLine(result); // 9
operation = Multiply; // acum delegatul indică spre metoda Multiply
result = operation(4, 5); // efectiv Multiply(4, 5)
Console.WriteLine(result); // 20
int Add(int x, int y) => x + y;
int Multiply(int x, int y) => x * y;
delegate int Operation(int x, int y);
În acest caz, delegatul Operation returnează un rezultat de tip int și are doi parametri de tip int. Deci, orice metodă care returnează un int și acceptă doi parametri de tip int corespunde acestui delegat. În acest exemplu, sunt metodele Add și Multiply. Putem atribui oricare dintre aceste metode variabilei delegat și le putem apela.
Atribuirea unei referințe la metodă
Mai sus, variabilei delegat i s-a atribuit direct o metodă. Există și un alt mod - crearea unui obiect delegat folosind constructorul, în care se transmite metoda necesară:
Operation operation1 = Add;
Operation operation2 = new Operation(Add);
int Add(int x, int y) => x + y;
delegate int Operation(int x, int y);
Ambele metode sunt echivalente.
Conformitatea metodelor cu delegatul
După cum s-a menționat mai sus, metodele sunt conforme cu delegatul dacă au același tip de returnare și același set de parametri. Dar trebuie să ținem cont de modificatorii ref, in și out. De exemplu, să presupunem că avem un delegat:
delegate void SomeDel(int a, double b);
Acestui delegat îi corespunde următoarea metodă:
void SomeMethod1(int g, double n) { }
Următoarele metode NU corespund:
double SomeMethod2(int g, double n) { return g + n; }
void SomeMethod3(double n, int g) { }
void SomeMethod4(ref int g, double n) { }
void SomeMethod5(out int g, double n) { g = 6; }
Aici metoda SomeMethod2 are un alt tip de returnare, diferit de tipul delegatului. SomeMethod3 are un alt set de parametri. Parametrii SomeMethod4 și SomeMethod5 sunt, de asemenea, diferiți de parametrii delegatului, deoarece au modificatori ref și out.
Adăugarea metodelor la delegat
În exemplele de mai sus, variabila delegat indica spre o singură metodă. În realitate, delegatul poate indica spre mai multe metode care au aceeași semnătură și același tip de returnare. Toate metodele din delegat sunt plasate într-o listă specială - lista de apeluri (invocation list).
Și când apelăm delegatul, toate metodele din această listă sunt apelate secvențial. Putem adăuga mai multe metode în această listă. Pentru a adăuga metode la delegat, se folosește operatorul +=:
Message message = Hello;
message += HowAreYou; // acum message indică spre două metode
message(); // sunt apelate ambele metode - Hello și HowAreYou
void Hello() => Console.WriteLine("Hello");
void HowAreYou() => Console.WriteLine("How are you?");
delegate void Message();
În acest caz, lista de apeluri a delegatului message include două metode - Hello și HowAreYou. Și când apelăm message, sunt apelate ambele metode.
Trebuie menționat că în realitate se va crea un nou obiect delegat, care va include metodele din copia veche a delegatului și metoda nouă, iar acest nou obiect delegat va fi atribuit variabilei message.
La adăugarea delegaților, trebuie să ținem cont de faptul că putem adăuga o referință la aceeași metodă de mai multe ori, iar în lista de apeluri a delegatului vor fi mai multe referințe la aceeași metodă. Prin urmare, la apelarea delegatului, metoda adăugată va fi apelată de atâtea ori cât a fost adăugată:
Message message = Hello;
message += HowAreYou;
message += Hello;
message += Hello;
message();
Output-ul în consolă:
Hello
How are you?
Hello
Hello
Putem elimina metode din delegat folosind operatorul -=:
Message? message = Hello;
message += HowAreYou;
message(); // sunt apelate toate metodele din message
message -= HowAreYou; // eliminăm metoda HowAreYou
if (message != null) message(); // este apelată metoda Hello
Trebuie să menționăm că la eliminarea metodelor din delegat, se va crea un nou delegat, care va conține în lista de apeluri metodele rămase, mai puțin metoda eliminată.
La eliminarea unei metode poate apărea situația în care delegatul să nu mai conțină nicio metodă, și atunci variabila va avea valoarea null. De aceea, în acest caz, variabila este definită nu doar ca o variabilă de tip Message, ci anume Message?, adică un tip care poate reprezenta atât un delegat Message, cât și valoarea null.
În plus, înainte de al doilea apel verificăm variabila pentru valoarea null.
La eliminare, trebuie să ținem cont de faptul că, dacă delegatul conține mai multe referințe la aceeași metodă, operația -= începe căutarea de la sfârșitul listei de apeluri și elimină doar prima apariție găsită. Dacă o astfel de metodă nu există în lista de apeluri, operația -= nu are niciun efect.
Unirea delegaților
Delegații pot fi uniți în alți delegați. De exemplu:
Message mes1 = Hello;
Message mes2 = HowAreYou;
Message mes3 = mes1 + mes2; // Unim delegații
mes3(); // Sunt apelate toate metodele din mes1 și mes2
void Hello() => Console.WriteLine("Hello");
void HowAreYou() => Console.WriteLine("How are you?");
delegate void Message();
În acest caz, obiectul mes3 reprezintă unirea delegaților mes1 și mes2. Unirea delegaților înseamnă că lista de apeluri a delegatului mes3 va conține toate metodele din delegații mes1 și mes2. Și la apelarea delegatului mes3, toate aceste metode vor fi apelate simultan.
Apelarea delegatului
În exemplele de mai sus, delegatul a fost apelat ca o metodă obișnuită. Dacă delegatul primește parametri, atunci la apelare sunt transmise valorile necesare pentru acești parametri:
Message mes = Hello;
mes();
Operation op = Add;
int n = op(3, 4);
Console.WriteLine(n);
void Hello() => Console.WriteLine("Hello");
int Add(int x, int y) => x + y;
delegate int Operation(int x, int y);
delegate void Message();
O altă modalitate de a apela delegatul este utilizarea metodei Invoke():
Message mes = Hello;
mes.Invoke(); // Hello
Operation op = Add;
int n = op.Invoke(3, 4);
Console.WriteLine(n); // 7
void Hello() => Console.WriteLine("Hello");
int Add(int x, int y) => x + y;
delegate int Operation(int x, int y);
delegate void Message();
Dacă delegatul primește parametri, valorile pentru aceștia sunt transmise metodei Invoke.
Trebuie să ținem cont de faptul că, dacă delegatul este gol, adică nu are nicio referință la metode în lista sa de apeluri (deci delegatul este null), atunci la apelarea unui astfel de delegat vom primi o excepție, așa cum se întâmplă în exemplul următor:
Message? mes;
//mes(); // ! Eroare: delegatul este null
Operation? op = Add;
op -= Add; // delegatul op este gol
int n = op(3, 4); // !Eroare: delegatul este null
Prin urmare, la apelarea delegatului, este bine să verificăm dacă acesta nu este null. Sau putem folosi metoda Invoke și operatorul condițional null:
Message? mes = null;
mes?.Invoke(); // Nu apare nici o eroare, delegatul pur și simplu nu este apelat
Operation? op = Add;
op -= Add; // delegatul op este gol
int? n = op?.Invoke(3, 4); // Nu apare nici o eroare, delegatul pur și simplu nu este apelat, iar n = null
Dacă delegatul returnează o valoare, se va returna valoarea ultimei metode din lista de apeluri (dacă lista de apeluri conține mai multe metode). De exemplu:
Operation op = Subtract;
op += Multiply;
op += Add;
Console.WriteLine(op(7, 2)); // Add(7,2) = 9
int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x * y;
delegate int Operation(int x, int y);
Delegați generici
Delegații, la fel ca și alte tipuri, pot fi generici. De exemplu:
Operation<decimal, int> squareOperation = Square;
decimal result1 = squareOperation(5);
Console.WriteLine(result1); // 25
Operation<int, int> doubleOperation = Double;
int result2 = doubleOperation(5);
Console.WriteLine(result2); // 10
decimal Square(int n) => n * n;
int Double(int n) => n + n;
delegate T Operation<T, K>(K val);
Aici, delegatul Operation este tipizat cu doi parametri de tip. Parametrul T reprezintă tipul valorii returnate. Parametrul K reprezintă tipul parametrului transmis delegatului. Astfel, acest delegat corespunde unei metode care primește un parametru de orice tip și returnează o valoare de orice tip.
Delegați ca parametri ai metodelor
Delegații pot fi, de asemenea, parametri ai metodelor. Astfel, o metodă poate primi alte metode ca parametri. De exemplu:
DoOperation(5, 4, Add); // 9
DoOperation(5, 4, Subtract); // 1
DoOperation(5, 4, Multiply); // 20
void DoOperation(int a, int b, Operation op)
{
Console.WriteLine(op(a,b));
}
int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x + y;
delegate int Operation(int x, int y);
Aici, metoda DoOperation primește ca parametri două numere și o acțiune sub forma unui delegat Operation. În interiorul metodei, apelăm delegatul Operation, transmițându-i numerele din primii doi parametri.
La apelarea metodei DoOperation, putem transmite ca al treilea parametru o metodă care corespunde delegatului Operation.
Returnarea delegaților din metode
Delegații pot fi returnați din metode. De exemplu:
Operation operation = SelectOperation(OperationType.Add);
Console.WriteLine(operation(10, 4)); // 14
operation = SelectOperation(OperationType.Subtract);
Console.WriteLine(operation(10, 4)); // 6
operation = SelectOperation(OperationType.Multiply);
Console.WriteLine(operation(10, 4)); // 40
Operation SelectOperation(OperationType opType)
{
switch (opType)
{
case OperationType.Add: return Add;
case OperationType.Subtract: return Subtract;
default: return Multiply;
}
}
int Add(int x, int y) => x + y;
int Subtract(int x, int y) => x - y;
int Multiply(int x, int y) => x * y;
enum OperationType
{
Add, Subtract, Multiply
}
delegate int Operation(int x, int y);
În acest caz, metoda SelectOperation() primește ca parametru o enumerare de tip OperationType. Această enumerare conține trei constante, fiecare corespunzând unei operațiuni aritmetice. În metoda SelectOperation, în funcție de valoarea parametrului, se returnează o anumită metodă.
Deoarece tipul returnat al metodei este delegatul Operation, metoda trebuie să returneze o metodă care corespunde acestui delegat - în acest caz, metodele Add, Subtract și Multiply. De exemplu, dacă parametrul metodei SelectOperation este OperationType.Add, se returnează metoda Add, care efectuează adunarea a două numere:
case OperationType.Add: return Add;
Când apelăm metoda SelectOperation, putem obține acțiunea dorită în variabila operation:
Operation operation = SelectOperation(OperationType.Add);
Și când apelăm variabila operation, de fapt, se va apela metoda obținută din SelectOperation:
Operation operation = SelectOperation(OperationType.Add); // Aici operation = Add
Console.WriteLine(operation(10, 4)); // 14