Delegato vs interfacce: ulteriori chiarimenti disponibili?


23

Dopo aver letto l'articolo: Quando usare i delegati invece delle interfacce (Guida per programmatori C #) , ho bisogno di aiuto per comprendere i punti indicati di seguito, che ho trovato non molto chiari (per me). Qualche esempio o spiegazione dettagliata disponibile per questi?

Usa un delegato quando:

  • Viene utilizzato un modello di progettazione di eventi.
  • È auspicabile incapsulare un metodo statico.
  • Si desidera una composizione semplice.
  • Una classe può richiedere più di una implementazione del metodo.

Utilizzare un'interfaccia quando:

  • Esistono un gruppo di metodi correlati che possono essere chiamati.
  • Una classe richiede solo un'implementazione del metodo.

Le mie domande sono:

  1. Cosa significano per un modello di progettazione di eventi?
  2. In che modo la composizione risulta semplice se viene utilizzato un delegato?
  3. se esiste un gruppo di metodi correlati che possono essere chiamati, utilizzare l'interfaccia-Quali vantaggi ha?
  4. se una classe necessita solo di un'implementazione del metodo, utilizzare l'interfaccia: come è giustificato in termini di vantaggi?

Risposte:


11

Cosa significano per un modello di progettazione di eventi?

Molto probabilmente si riferiscono a un'implementazione del modello di osservatore che è un costrutto di linguaggio di base in C #, esposto come " eventi ". L'ascolto di eventi è possibile agganciando un delegato a loro. Come ha sottolineato Yam Marcovic, EventHandlerè il tipo di delegato di base convenzionale per gli eventi, ma è possibile utilizzare qualsiasi tipo di delegato.

In che modo la composizione risulta semplice se viene utilizzato un delegato?

Questo probabilmente si riferisce solo all'offerta di flessibilità dei delegati. Puoi facilmente "comporre" determinati comportamenti. Con l'aiuto di lambdas , anche la sintassi per farlo è molto concisa. Considera il seguente esempio.

class Bunny
{
    Func<bool> _canHop;

    public Bunny( Func<bool> canHop )
    {
        _canHop = canHop;
    }

    public void Hop()
    {
        if ( _canHop() )  Console.WriteLine( "Hop!" );
    }
}

Bunny captiveBunny = new Bunny( () => IsBunnyReleased );
Bunny lazyBunny = new Bunny( () => !IsLazyDay );
Bunny captiveLazyBunny = new Bunny( () => IsBunnyReleased && !IsLazyDay );

Fare qualcosa di simile con le interfacce richiederebbe di usare il modello di strategia o usare una Bunnyclasse base (astratta) da cui estendere i coniglietti più specifici.

se esiste un gruppo di metodi correlati che possono essere chiamati, utilizzare l'interfaccia-Quali vantaggi ha?

Ancora una volta, userò i coniglietti per dimostrare come sarebbe più facile.

interface IAnimal
{
    void Jump();
    void Eat();
    void Poo();
}

class Bunny : IAnimal { ... }
class Chick : IAnimal { ... }

// Using the interface.
IAnimal bunny = new Bunny();
bunny.Jump();  bunny.Eat();  bunny.Poo();
IAnimal chick = new Chick();
chick.Jump();  chick.Eat();  chick.Poo();

// Without the interface.
Action bunnyJump = () => bunny.Jump();
Action bunnyEat = () => bunny.Eat();
Action bunnyPoo = () => bunny.Poo();
bunnyJump(); bunnyEat(); bunnyPoo();
Action chickJump = () => chick.Jump();
Action chickEat = () => chick.Eat();
...

se una classe necessita solo di un'implementazione del metodo, utilizzare l'interfaccia: come è giustificato in termini di vantaggi?

Per questo, considera di nuovo il primo esempio con il coniglietto. Se è mai necessaria una sola implementazione (non è mai richiesta una composizione personalizzata), è possibile esporre questo comportamento come interfaccia. Non dovrai mai costruire i lambda, puoi semplicemente usare l'interfaccia.

Conclusione

I delegati offrono molta più flessibilità, mentre le interfacce ti aiutano a stabilire contratti forti. Quindi trovo l'ultimo punto menzionato, "Una classe potrebbe aver bisogno di più di una implementazione del metodo". , di gran lunga il più rilevante.

Un ulteriore motivo per utilizzare i delegati è quando si desidera esporre solo parte di una classe da cui non è possibile regolare il file di origine.

Come esempio di tale scenario (massima flessibilità, nessuna necessità di modificare le fonti), prendere in considerazione questa implementazione dell'algoritmo di ricerca binaria per ogni possibile raccolta semplicemente passando due delegati.


12

Un delegato è qualcosa di simile a un'interfaccia per la firma di un singolo metodo, che non deve essere esplicitamente implementata come un'interfaccia normale. Puoi costruirlo in fuga.

Un'interfaccia è solo una struttura linguistica che rappresenta un contratto - "Con la presente garantisco la disponibilità dei seguenti metodi e proprietà".

Inoltre, non sono completamente d'accordo sul fatto che i delegati siano principalmente utili quando utilizzati come soluzione al modello di osservatore / abbonato. È, tuttavia, una soluzione elegante al "problema della verbosità java".

Per le tue domande:

1 e 2)

Se si desidera creare un sistema di eventi in Java, in genere si utilizza un'interfaccia per propagare l'evento, qualcosa del tipo:

interface KeyboardListener
{
    void KeyDown(int key);
    void KeyUp(int key)
    void KeyPress(int key);
    .... and so on
}

Questo significa che la tua classe dovrà implementare esplicitamente tutti questi metodi e fornire stub per tutti loro anche se vuoi semplicemente implementarli KeyPress(int key).

In C #, questi eventi sarebbero rappresentati come elenchi di delegati, nascosti dalla parola chiave "evento" in c #, uno per ogni singolo evento. Ciò significa che puoi abbonarti facilmente a ciò che desideri senza sovraccaricare la tua classe con metodi "chiave" pubblici ecc.

+1 per i punti 3-5.

Inoltre:

I delegati sono molto utili quando si desidera fornire ad esempio una funzione "map", che accetta un elenco e proietta ciascun elemento in un nuovo elenco, con la stessa quantità di elementi, ma in qualche modo diverso. In sostanza, IEnumerable.Select (...).

IEnumerable.Select accetta a Func<TSource, TDest>, ovvero un delegato che esegue il wrapping di una funzione che accetta un elemento TSource e lo trasforma in un elemento TDest.

In Java questo dovrebbe essere implementato usando un'interfaccia. Spesso non esiste un luogo naturale per implementare tale interfaccia. Se una classe contiene un elenco che vuole trasformare in qualche modo, non è molto naturale implementare l'interfaccia "ListTransformer", soprattutto perché potrebbero esserci due elenchi diversi che dovrebbero essere trasformati in modi diversi.

Certo, potresti usare classi anonime che sono un concetto simile (in Java).


Valuta la possibilità di modificare il tuo post per rispondere effettivamente alle sue domande
Yam Marcovic,

@YamMarcovic Hai ragione, ho vagato un po 'in forma libera invece di rispondere direttamente alle sue domande. Tuttavia, penso che spieghi un po 'i meccanici coinvolti. Inoltre, le sue domande erano un po 'meno ben formate quando ho risposto alla domanda. : p
Max

Naturalmente. Succede anche a me, e ha il suo valore in sé. Ecco perché l'ho solo suggerito , ma non me ne sono lamentato. :)
Yam Marcovic,

3

1) Pattern di eventi, beh il classico è il modello di Observer, ecco un buon collegamento Microsoft con Microsoft Talk Observer

L'articolo a cui ti sei collegato non è molto ben scritto e rende le cose più complicate del necessario. Il business statico è buon senso, non è possibile definire un'interfaccia con membri statici, quindi se si desidera un comportamento polimorfico su un metodo statico, si utilizzerà un delegato.

Il link sopra parla di delegati e perché ci sono buoni per la composizione, quindi ciò potrebbe aiutare con la tua query.


3

Prima di tutto, bella domanda. Ammiro la tua attenzione all'utilità piuttosto che accettare ciecamente le "migliori pratiche". +1 per quello.

Ho letto quella guida prima. Devi ricordare qualcosa al riguardo: è solo una guida, principalmente per i nuovi arrivati ​​di C # che sanno programmare ma non hanno molta familiarità con il modo di fare C #. Non è tanto una pagina di regole quanto una pagina che descrive come le cose sono già fatte di solito. E dato che sono già stati fatti così ovunque, potrebbe essere una buona idea rimanere coerenti.

Arriverò al punto, rispondendo alle tue domande.

Prima di tutto, suppongo che tu sappia già cos'è un'interfaccia. Per quanto riguarda un delegato, è sufficiente dire che è una struttura che contiene un puntatore tipizzato a un metodo, insieme a un puntatore facoltativo all'oggetto che rappresenta l' thisargomento per quel metodo. Nel caso di metodi statici, quest'ultimo puntatore è nullo.
Esistono anche delegati multicast, che sono esattamente come i delegati, ma possono avere a loro assegnate diverse di queste strutture (il che significa che una singola chiamata a Invocare su un delegato multicast richiama tutti i metodi nell'elenco di invocazione assegnato).

Cosa significano per un modello di progettazione di eventi?

Intendono usare eventi in C # (che ha parole chiave speciali per implementare sofisticamente questo modello estremamente utile). Gli eventi in C # sono alimentati da delegati multicast.

Quando si definisce un evento, come in questo esempio:

class MyClass {
  // Note: EventHandler is just a multicast delegate,
  // that returns void and accepts (object sender, EventArgs e)!
  public event EventHandler MyEvent;

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

Il compilatore in realtà lo trasforma nel seguente codice:

class MyClass {
  private EventHandler MyEvent = null;

  public void add_MyEvent(EventHandler value) {
    MyEvent += value;
  }

  public void remove_MyEvent(EventHandler value) {
    MyEvent -= value;
  }

  public void DoSomethingThatTriggersMyEvent() {
    // ... some code
    var handler = MyEvent;
    if (handler != null)
      handler(this, EventArgs.Empty);
    // ... some other code
  }
}

Ti iscrivi quindi a un evento facendo

MyClass instance = new MyClass();
instance.MyEvent += SomeMethodInMyClass;

Che si compila fino a

MyClass instance = new MyClass();
instance.add_MyEvent(new EventHandler(SomeMethodInMyClass));

Quindi questo è un evento in C # (o .NET in generale).

In che modo la composizione risulta semplice se viene utilizzato un delegato?

Questo può essere facilmente dimostrato:

Supponiamo di avere una classe che dipende da una serie di azioni da passare ad essa. È possibile incapsulare tali azioni in un'interfaccia:

interface RequiredMethods {
  void DoX();
  int DoY();
};

E chiunque volesse passare azioni alla tua classe dovrebbe prima implementare quell'interfaccia. Oppure potresti rendere le loro vite più facili a seconda della seguente classe:

sealed class RequiredMethods {
  public Action DoX;
  public Func<int> DoY();
}

In questo modo i chiamanti devono solo creare un'istanza di RequiredMethods e legare i metodi ai delegati in fase di esecuzione. Di solito è più facile.

Questo modo di fare le cose è estremamente utile nelle giuste circostanze. Pensaci: perché dipendere da un'interfaccia quando tutto ciò che ti interessa davvero è avere un'implementazione passata a te?

Vantaggi dell'uso delle interfacce in presenza di un gruppo di metodi correlati

È utile utilizzare le interfacce perché le interfacce normalmente richiedono implementazioni esplicite in fase di compilazione. Ciò significa che crei una nuova classe.
E se hai un gruppo di metodi correlati in un singolo pacchetto, è utile che quel pacchetto sia riutilizzabile da altre parti del codice. Quindi, se possono semplicemente creare un'istanza di una classe invece di creare un insieme di delegati, è più facile.

Vantaggi dell'uso delle interfacce se una classe necessita solo di un'implementazione

Come notato in precedenza, le interfacce vengono implementate in fase di compilazione, il che significa che sono più efficienti di invocare un delegato (che è un livello di indiretta di per sé).

"Un'implementazione" potrebbe significare un'implementazione che esiste un unico luogo ben definito.
In caso contrario, un'implementazione potrebbe venire da qualsiasi parte del programma, che sembra appena essere conformi al metodo di firma. Ciò consente una maggiore flessibilità, poiché i metodi devono solo conformarsi alla firma prevista, anziché appartenere a una classe che implementa esplicitamente un'interfaccia specifica. Ma quella flessibilità potrebbe avere un costo, e in realtà rompe il principio di sostituzione di Liskov , perché la maggior parte delle volte si desidera esplicitare, perché riduce al minimo la possibilità di incidenti. Proprio come la digitazione statica.

Il termine potrebbe anche riferirsi ai delegati multicast qui. I metodi dichiarati dalle interfacce possono essere implementati una sola volta in una classe di implementazione. Ma i delegati possono accumulare più metodi, che verranno chiamati in sequenza.

Quindi, tutto sommato, sembra che la guida non sia abbastanza istruttiva e funzioni semplicemente come è: una guida, non un libro delle regole. Alcuni consigli potrebbero effettivamente sembrare un po 'contraddittori. Sta a te decidere quando è giusto applicare cosa. La guida sembra darci solo un percorso generale.

Spero che le tue domande abbiano ricevuto risposta in modo soddisfacente. E ancora, complimenti per la domanda.


2

Se la mia memoria di .NET è ancora valida, un delegato è fondamentalmente una funzione ptr o functor. Aggiunge un livello di riferimento indiretto a una chiamata di funzione in modo che le funzioni possano essere sostituite senza che il codice chiamante debba cambiare. Questa è la stessa cosa che fa un'interfaccia, tranne per il fatto che un'interfaccia raggruppa più funzioni insieme e un implementatore deve implementarle insieme.

Un modello di evento, in generale, è quello in cui qualcosa risponde agli eventi da altre parti (come i messaggi di Windows). L'insieme di eventi è di solito a tempo indeterminato, possono arrivare in qualsiasi ordine e non sono necessariamente correlati tra loro. I delegati lavorano bene per questo in quanto ogni evento può chiamare una singola funzione senza bisogno di un riferimento a una serie di oggetti di implementazione che potrebbero contenere anche numerose funzioni irrilevanti. Inoltre (è qui che la mia memoria .NET è vaga), penso che più delegati possano essere collegati a un evento.

Compositing, anche se non ho molta familiarità con il termine, fondamentalmente sta progettando un oggetto per avere più sotto-parti o figli aggregati a cui viene trasmessa l'opera. I delegati consentono ai bambini di essere mescolati e abbinati in un modo più ad-hoc in cui un'interfaccia potrebbe essere eccessiva o causare un eccessivo accoppiamento e la rigidità e la fragilità che ne derivano.

Il vantaggio di un'interfaccia per i metodi correlati è che i metodi possono condividere lo stato dell'oggetto di implementazione. Le funzioni delegate non possono condividere in modo così chiaro, o neppure contenere, lo stato.

Se una classe necessita di un'unica implementazione, un'interfaccia è più adatta poiché all'interno di qualsiasi classe che implementa l'intera raccolta, è possibile eseguire solo un'implementazione e si ottengono i vantaggi di una classe di implementazione (stato, incapsulamento, ecc.). Se l'implementazione potrebbe cambiare a causa dello stato di runtime, i delegati funzionano meglio perché possono essere sostituiti con altre implementazioni senza influire sugli altri metodi. Ad esempio, se ci sono tre delegati che hanno due possibili implementazioni, occorrerebbero otto classi diverse che implementano l'interfaccia a tre metodi per tenere conto di tutte le possibili combinazioni di stati.


1

Il modello di progettazione "eventing" (meglio noto come modello di osservatore) consente di associare più metodi alla stessa firma al delegato. Non puoi davvero farlo con un'interfaccia.

Non sono affatto convinto che la composizione sia più facile per un delegato che un'interfaccia. È un'affermazione molto strana. Mi chiedo se intenda perché è possibile associare metodi anonimi a un delegato.


1

Il più grande chiarimento che posso offrire:

  1. Un delegato definisce una firma della funzione - quali parametri accetta una funzione corrispondente e cosa restituirà.
  2. Un'interfaccia si occupa di un intero set di funzioni, eventi, proprietà e campi.

Così:

  1. Quando vuoi generalizzare alcune funzioni con la stessa firma, usa un delegato.
  2. Quando vuoi generalizzare un comportamento o la qualità di una classe, usa un'interfaccia.

1

La tua domanda sugli eventi è già stata ben coperta. Ed è anche vero che un'interfaccia può definire più metodi (ma in realtà non è necessario), mentre un tipo di funzione pone sempre e solo vincoli su una singola funzione.

La vera differenza però è:

  • i valori di funzione sono abbinati ai tipi di funzione per sottotipo strutturale (ovvero compatibilità implicita con la struttura richiesta). Ciò significa che se un valore di funzione ha una firma compatibile con il tipo di funzione, è un valore valido per il tipo.
  • le istanze sono abbinate alle interfacce dal sottotipo nominale (ovvero uso esplicito di un nome). Ciò significa che, se un'istanza è un membro di una classe, che implementa esplicitamente l'interfaccia data, l'istanza è un valore valido per l'interfaccia. Tuttavia, se l'oggetto ha solo tutti i membri richiesti dalle interfacce e tutti i membri hanno firme compatibili, ma non implementa esplicitamente l'interfaccia, non è un valore valido per l'interfaccia.

Certo, ma cosa significa?

Facciamo questo esempio (il codice è in haXe poiché il mio C # non è molto buono):

class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:T->Bool):Collection<T> { 
         //build a new collection with all elements e, such that predicate(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

Ora il metodo di filtro semplifica il passaggio in una piccola parte della logica, ignara dell'organizzazione interna della raccolta, mentre la raccolta non dipende dalla logica che le è stata assegnata. Grande. Tranne che per un problema:
la collezione non dipende dalla logica. La raccolta presuppone intrinsecamente che la funzione passata è progettata per testare un valore rispetto a una condizione e restituire l'esito positivo del test. Si noti che non tutte le funzioni che accettano un valore come argomento e restituiscono un valore booleano sono in realtà semplici predicati. Ad esempio il metodo di rimozione della nostra raccolta è una tale funzione.
Supponiamo di aver chiamato c.filter(c.remove). Il risultato sarebbe una collezione con tutti gli elementi del ctempocstesso diventa vuoto. Questo è un peccato, perché naturalmente ci aspetteremmo cche sia invariante.

L'esempio è molto costruito. Ma il problema chiave è che il codice che chiama c.filtercon qualche valore di funzione come argomento non ha modo di sapere se quell'argomento è adatto (cioè alla fine conserverà l'invariante). Il codice che ha creato il valore della funzione potrebbe o meno sapere che verrà interpretato come predicato.

Ora cambiamo le cose:

interface Predicate<T> {
    function test(value:T):Bool;
}
class Collection<T> {
    /* a lot of code we don't care about now */
    public function filter(predicate:Predicate<T>):Collection<T> { 
         //build a new collection with all elements e, such that predicate.test(e) == true
    }
    public function remove(e:T):Bool {
         //removes an element from this collection, if contained and returns true, false otherwise
    }
}

Cosa è cambiato? Ciò che è cambiato è che qualunque valore venga ora dato filterha esplicitamente firmato il contratto di essere un predicato. Naturalmente i programmatori maliziosi o estremamente stupidi creano implementazioni dell'interfaccia che non sono prive di effetti collaterali e quindi non sono predicati.
Ma ciò che non può più accadere è che qualcuno leghi un'unità logica di dati / codice, che viene erroneamente interpretata come un predicato a causa della sua struttura esterna.

Quindi, per riformulare quanto detto sopra in poche parole:

  • le interfacce significano usare la tipizzazione nominale e quindi relazioni esplicite
  • i delegati intendono usare la tipizzazione strutturale e quindi le relazioni implicite

Il vantaggio delle relazioni esplicite è che puoi esserne certo. Lo svantaggio è che richiedono un sovraccarico di esplicitazione. Al contrario, lo svantaggio delle relazioni implicite (nel nostro caso la firma di una funzione corrisponde alla firma desiderata), è che non si può davvero essere sicuri al 100% di poter usare le cose in questo modo. Il vantaggio è che puoi stabilire relazioni senza tutto il sovraccarico. Puoi semplicemente mettere insieme le cose, perché la loro struttura lo consente. Ecco cosa significa composizione facile.
È un po 'come LEGO: puoi semplicemente collegare una figura LEGO di guerre stellari a una nave pirata LEGO, semplicemente perché la struttura esterna lo consente. Ora potresti sentire che è terribilmente sbagliato, oppure potrebbe essere esattamente quello che vuoi. Nessuno ti fermerà.


1
Vale la pena ricordare che "implementa esplicitamente l'interfaccia" (sotto "sottotipizzazione nominale") non equivale all'implementazione esplicita o implicita dei membri dell'interfaccia. In C #, le classi devono sempre dichiarare esplicitamente le interfacce che implementano, come dici tu. Ma i membri dell'interfaccia possono essere implementati in modo esplicito (ad es. int IList.Count { get { ... } }) O implicitamente ( public int Count { get { ... } }). Questa distinzione non è rilevante per questa discussione, ma merita menzione per evitare di confondere i lettori.
phoog

@phoog: Sì, grazie per l'emendamento. Non sapevo nemmeno che il primo fosse possibile. Ma sì, il modo in cui una lingua impone effettivamente l'implementazione dell'interfaccia varia molto. In Objective-C per esempio, ti darà un avvertimento solo se non è implementato.
back2dos,

1
  1. Un modello di progettazione di eventi implica una struttura che prevede un meccanismo di pubblicazione e sottoscrizione. Gli eventi sono pubblicati da una fonte, ogni abbonato ottiene la propria copia dell'elemento pubblicato ed è responsabile delle proprie azioni. Questo è un meccanismo vagamente accoppiato perché l'editore non deve nemmeno sapere che ci sono gli abbonati. Per non parlare di avere abbonati non è obbligatorio (non può essere nessuno)
  2. la composizione è "più semplice" se viene utilizzato un delegato rispetto a un'interfaccia. Le interfacce definiscono un'istanza di classe come un oggetto "tipo di gestore" dove un'istanza del costrutto di composizione sembra "avere un gestore", quindi l'aspetto dell'istanza è meno limitato usando un delegato e diventa più flessibile.
  3. Dal commento qui sotto ho scoperto che dovevo migliorare il mio post, vedere questo come riferimento: http://bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate

1
3. Perché i delegati dovrebbero richiedere più casting e perché l'accoppiamento più stretto è un vantaggio? 4. Non è necessario utilizzare gli eventi per usare i delegati, e con i delegati è proprio come tenere un metodo: sai che è il tipo restituito e i tipi degli argomenti. Inoltre, perché un metodo dichiarato in un'interfaccia renderebbe più semplice la gestione degli errori rispetto a un metodo collegato a un delegato?
Yam Marcovic,

Nel caso delle specifiche di un delegato hai ragione. Tuttavia, nel caso della gestione degli eventi (almeno questo è il mio riferimento) devi sempre ereditare da qualche tipo di eventArgs per agire come contenitore per tipi personalizzati, quindi invece di essere in grado di testare in sicurezza un valore che avresti sempre devi scartare il tuo tipo prima di gestire i valori.
Carlo Kuip,

Per quanto riguarda il motivo per cui penso che un metodo definito in un'interfaccia renderebbe più semplice la gestione degli errori, il compilatore è in grado di controllare i tipi di eccezione generati da quel metodo. So che non è una prova convincente ma forse dovrebbe essere dichiarata come preferenza?
Carlo Kuip,

1
In primo luogo stavamo parlando di delegati vs interfacce, non di eventi vs interfacce. In secondo luogo, creare un evento basato su un delegato EventHandler è solo una convenzione e non è assolutamente obbligatorio. Ma ancora una volta, parlare di eventi qui in qualche modo manca il punto. Per quanto riguarda il tuo secondo commento, le eccezioni non vengono verificate in C #. Ti stai confondendo con Java (che per inciso non ha nemmeno delegati).
Yam Marcovic,

Ho trovato una discussione su questo argomento che spiega importanti differenze: bytes.com/topic/c-sharp/answers/252309-interface-vs-delegate
Carlo Kuip
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.