Una domanda così ampia e aperta, ma morderò! Ecco un esempio davvero stupido.
Considera le seguenti due interfacce:
interface IAdder
{
int Combine(int a, int b);
}
interface IXorer
{
int Combine(int a, int b);
}
Queste due interfacce sembrano avere gli stessi metodi, con lo stesso nome e gli stessi argomenti. Tuttavia sono trattati come completamente separati. Non esiste una relazione di ereditarietà. Non puoi usare accidentalmente IAdder
quando vuoi usare IXorer
(a meno che tu non faccia qualcosa di straordinariamente strano). Sei protetto da una sostituzione errata per tipo sicurezza.
Puoi memorizzare un riferimento a una di queste interfacce insieme al tipo di interfaccia, assicurando così che non sarai mai XOR quando desideri AGGIUNGERE, o viceversa.
public Client(IAdder adder)
{
_adder = adder;
}
public int Add(int a, int b)
{
return _adder.Combine(a, b); //Pretty darned sure this will add a and b
}
Non esattamente la scienza missilistica finora, spero.
Ora diciamo che non vuoi memorizzare un puntatore all'interfaccia. Invece, vuoi memorizzare un puntatore alla funzione stessa, come nel tuo secondo esempio. Potrebbe assomigliare a questo:
private readonly Func<int, int, int> _combiner;
public Client(IAdder adder)
{
_combiner = adder.Combine;
}
ma potresti anche fare questo:
public Client(IXorer xorer)
{
_combiner = xorer.Combine;
}
Quindi, visto quanto sopra, dimmi ... cosa fa questo codice?
return _combiner(a, b); // Um..... ????
Aggiunge o xor? Oops, non posso dirlo!
Ora non puoi più sapere che cosa fa _combiner
dal suo tipo. È solo una funzione che prende due interi e ne restituisce una terza. Non hai idea di cosa faccia, oltre a quello che puoi capire nominando le convenzioni e / o tracciando il codice. Non sei protetto da una sostituzione impropria di tipo safety perché il tipo di delegato non è legato a una particolare interfaccia.
Forse è questo che intendevi. Se vuoi solo Client
per poter inviare alcuni dati ad una funzione con una firma particolare, il tuo secondo esempio lo comunica abbastanza bene (torneremo su quello in un secondo). Ma se l'intenzione è di legare l'elaborazione a un particolare metodo di una particolare interfaccia, l'indirezione causata dal delegato ti fa perdere quell'informazione e rende il codice più difficile da leggere e più soggetto a errori.
E immagina di essere uno sviluppatore e stai decodificando questo codice pochi mesi dopo ... trovi la chiamata al delegato e premi F12 per scoprire dove va, e tutto quello che ti dice è il metodo firma. Ora devi fare una ricerca del codice per capire chi imposta quella variabile. E se è impostato più di una volta, chi lo ha impostato quando. Che incubo! Perché, è quasi come lavorare con BASIC procedurale con un gruppo di variabili globali. Se avessi memorizzato l'interfaccia, l'ingegnere dovrebbe almeno sapere dove cercare per scoprire cosa dovrebbe fare.
Sulla base di questo ragionamento, la mia preferenza è tipicamente quella di iniettare, immagazzinare e usare l'interfaccia, al fine di sfruttare il tipo di sicurezza come mezzo per garantire la correttezza semantica.
Ora, cosa succede se vuoi essere in grado di cambiare adder
e xorer
in fase di esecuzione? Certamente memorizzare solo il metodo ti consentirà di farlo senza troppi sforzi. Ma potresti anche inserire accidentalmente una funzione che prende due interi e ne emette un terzo, e immagino che ce ne siano parecchi nell'universo. Quindi un modo migliore per farlo sarebbe quello di rimanere con le interfacce:
interface ICombiner
{
int Combine(int a, int b);
}
interface IAdder : ICombiner {}
interface IXorer : ICombiner {}
Ora come programmatore hai la capacità di controllare completamente ciò che può essere sostituito e ciò che non può, che è esattamente il modo in cui le interfacce devono essere utilizzate. Non puoi farlo con i delegati.
TLDR: usa il primo metodo, a meno che tu non abbia un valido motivo per usare il secondo metodo.