Perché dovrei mai usare i delegati se non sto facendo eventi? [duplicare]

25

Sto cercando di imparare come fare eventi in C # e in base a l'esercitazione sugli eventi di MSDN ,

Events are declared using delegates. If you have not yet studied the Delegates Tutorial, you should do so before continuing

quindi ora sto cercando di capire prima i delegati e, in caso contrario, in modo orribile. A cosa servono? Che problema stai cercando di risolvere dove delegare qualcosa?

PENSO che siano per passare metodi in altri metodi ma a che scopo? Perché non hai solo un metodo che fa tutto ciò che stai facendo in primo luogo?

    
posta medivh 12.09.2013 - 15:36
fonte

7 risposte

57

Passare le funzioni ad altre funzioni è un ottimo modo per generalizzare il codice , specialmente quando si ha un sacco di codice quasi duplicato con solo la logica nel mezzo diversa.

Il mio esempio (semplice) preferito di questo sta facendo una funzione Benchmark, che accetta un delegato contenente il codice che desideri confrontare. Utilizzerò gli oggetti lambda syntax e Func / Action , perché sono molto più concisi (e comuni) che vedere i delegati creati in modo esplicito al giorno d'oggi.

public void Benchmark(int times, Action func)
{
    var watch = new Stopwatch();
    double totalTime = 0.0;

    for (int i = 0; i < times; i++)
    {
        watch.Start();
        func(); // Execute our injected function
        watch.Stop();

        totalTime += watch.EllapsedTimeMilliseconds;
        watch.Reset();
    }

    double averageTime = totalTime / times;
    Console.WriteLine("{0}ms", averageTime);
}

Ora puoi passare qualsiasi blocco di codice a quella funzione di benchmark (insieme al numero di volte che vuoi eseguirlo) e recuperare il tempo medio di esecuzione!

// Benchmark the amount of time it takes to ToList a range of 100,000 items
Benchmark(5, () =>
{
    var xs = Enumerable.Range(0, 100000).ToList();
});

// You can also pass in any void Function() by its handler
Benchmark(5, SomeExpensiveFunction);

Se non si riuscisse a iniettare il codice che si desidera eseguire il benchmark nel mezzo di tale funzione, probabilmente si finirebbe per copiare e incollare la logica ovunque si volesse utilizzarla.

Linq fa ampio uso del passaggio di funzione, consentendo di avere un intero host di operazioni veramente flessibili su insiemi di dati. Prendiamo la funzione Where come esempio. Filtra gli elementi di un elenco che restituiscono "true" quando viene passata una funzione di confronto.

var xs = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// This function compares each element, and returns true for those that are above 5
//   Result = { 6, 7, 8, 9, 10 }
var above5 = xs.Where((x) => x > 5);

// This function only returns true if an element is even
//   Result = { 2, 4, 6, 8, 10 }
var evens = xs.Where((x) => x % 2 == 0);

La funzione Where è molto generalizzata. Semplicemente esegue il ciclo su una raccolta e produce una nuova raccolta contenente solo i valori che corrispondono a una funzione di predicato. Spetta a te iniettare il codice che dice esattamente che sta cercando.

    
risposta data 12.09.2013 - 15:50
fonte
10

Supponiamo di avere un calcolo matematico complicato da eseguire, che comporta l'esecuzione di rounding . Supponiamo che a volte vuoi che l'arrotondamento sia round to even (ovvero l'arrotondamento dei banchieri), ma altre volte vuoi arrotondare da zero (ovvero arrotondare [in assoluto valore] ').

Ora potresti scrivere due metodi:

double DoTheCalculationWithRoundToEven(double input1, double input2)
{
    var intermediate1 = Math.Pow(input1, input2);
    // more calculations...
    var intermediate5 = Math.Round(intermediate4, MidpointRounding.ToEven);
    // more calculations...
    var intermediate10 = Math.Abs(intermediate9);

    return intermediate10;
}

double DoTheCalculationWithRoundAwayFromZero(double input1, double input2)
{
    var intermediate1 = Math.Pow(input1, input2);
    // more calculations...
    var intermediate5 = Math.Round(intermediate4, MidpointRounding.AwayFromZero);
    // more calculations...
    var intermediate10 = Math.Abs(intermediate9);

    return intermediate10;
}

con i due metodi è lo stesso tranne per quella riga nel mezzo.

Ora funziona, MA hai codice duplicato e la possibilità molto reale che qualche futuro manutentore (ad esempio, un futuro tu) cambierà una versione di intermediate2 in intermediate3 e dimentichi l'altro .

Un tipo di delegato e un parametro possono risolvere questa duplicazione.

Prima dichiara il tipo di delegato:

    public delegate double Rounder(double value);

Questo indica che un Rounder è un metodo che accetta un double e restituisce un double .

Successivamente, hai un metodo di calcolo che accetta Rounder e lo usa per arrotondare:

double DoTheCalculation(double input1, double input2, Rounder rounder)
{
    var intermediate1 = Math.Pow(input1, input2);
    // more calculations...
    var intermediate5 = rounder(intermediate4);
    // more calculations...
    var intermediate10 = Math.Abs(intermediate9);

    return intermediate10;
}

Infine, quando vuoi eseguire il calcolo, passa un Rounder appropriato. Puoi scrivere tutto con i metodi:

double RounderAwayFromZero(double value)
{
    return Math.Round(value, MidpointRounding.AwayFromZero);
}
// same for ToEven

quindi chiama uno di:

var result = DoTheCalculation(input1, input2, RounderAwayFromZero);

o

var result = DoTheCalculation(input1, input2, RounderToEven);

ma C # ti permette di fare tutto ciò in linea con lambdas:

var result = DoTheCalculation(input1, input2, 
     value => Math.Round(value, MidpointRounding.AwayFromZero));

dove l'espressione lambda definisce un metodo con la firma corretta come Rounder .

    
risposta data 12.09.2013 - 15:56
fonte
1

I delegati agiscono come un modo per parametrizzare un qualche tipo di azione. La funzione che viene passata a un delegato non si cura di come viene eseguita l'azione, solo che viene eseguita.

Ad esempio, consulta l'interfaccia IComparer . Ti permette di ordinare una lista senza fare affidamento su come il tipo nella lista è ordinato "naturalmente". Tuttavia, quando guardi l'interfaccia, definisce solo un metodo. Risulta piuttosto dispendioso definire un'intera classe che implementa solo una singola funzione.

I delegati risolvono questo problema 1 . Non è necessario definire una classe, solo il metodo. Ti permette di rimuovere il codice boilerplate e concentrarti su ciò che vuoi fare. Lambdas fa un passo in avanti in modo che la definizione della funzione possa essere allineata anziché definita da qualche altra parte nella classe.

1 Se guardi Sort() , esiste una versione sovraccaricata che utilizza un delegato.

    
risposta data 12.09.2013 - 15:53
fonte
1

I delegati sono simili alle interfacce. Stai semplicemente definendo un contratto che può essere soddisfatto da qualunque cosa stia consumando la classe che usa il delegato. La grande differenza tra un'interfaccia e un delegato è un'interfaccia che definisce un contratto per un'intera classe mentre un delegato definisce un contratto per un singolo metodo.

L'esempio migliore e più semplice del mondo reale dell'utilizzo dei delegati è il modello "Hole in the Middle". Non ho bisogno di questo schema molto spesso, ma quando lo faccio, è estremamente prezioso. Puoi realizzare questo stesso modello usando l'ereditarietà e i metodi astratti, tuttavia i delegati ti permettono di realizzare questo usando la composizione.

public delegate void MyDelegate(string param1, string param2);

public void SomeMethod(string param1, string param2) {
    Console.WriteLine(param1);
    //do some more work
}

public void DoSomething(MyDelegate somemethod) {
    //do something here
    somemethod(param1, param2);
    //do something else here
}
    
risposta data 12.09.2013 - 16:28
fonte
1

Lo scopo del passaggio di un metodo a un altro metodo consiste nel separare gli scopi di questi due metodi. Ciò è in linea con i principi accettati della progettazione orientata agli oggetti, in particolare il principio di responsabilità singola: "un oggetto codice dovrebbe fare una cosa e essere l'unico oggetto codice nella base di codici che fa quella cosa".

Caso in questione, supponiamo che tu abbia un metodo che legge un file in un determinato formato (diciamo il testo delimitato da pipe) e un metodo che digerisce le informazioni in quel file in un modo specifico per estrarre alcune informazioni. Tu dici che dovresti metterli insieme in un unico metodo. Bene, cosa succede quando il programma deve iniziare a leggere dai file CSV? Cosa succede quando hai bisogno di un sottoinsieme diverso dei dati in esso contenuti? Devi cambiare il metodo. Ma il programma deve ancora gestire il testo delimitato da pipe e tirare il vecchio set di dati. Ora hai un metodo che legge da due diversi tipi di file e produce due diversi set di dati. Questo metodo sta rapidamente diventando impossibile da mantenere.

Invece, puoi creare un metodo che legge il testo delimitato da pipe e un altro che legge i CSV. È possibile creare un metodo che estrae un set di dati e un altro che estrae il secondo. Quindi, puoi dare il metodo di lettura dei file come delegato al metodo di estrazione dei dati, o viceversa. Oppure puoi assegnare entrambi a una terza classe di metodi che li eseguirà semplicemente in ordine, fornendo i dati dal file al metodo di estrazione del set di dati.

Per esempio, aumenti il numero di metodi che stai scrivendo, ma ogni metodo ha uno e un solo scopo. Qualsiasi modifica che è necessario apportare affinché il metodo funzioni correttamente non influirà sullo scopo o sull'esecuzione di nessun altro. Il codice risultante è più facile da capire, più facile da mantenere e quindi richiederà meno test per garantirne la correttezza.

    
risposta data 12.09.2013 - 21:53
fonte
0

Ci sono un sacco di tipi di librerie di classi framework con operazioni che accettano i delegati come parametro.

Se usi la Libreria parallela delle attività , LINQ o Entity Framework, passerai in delegati come argomenti.

Molte firme dei delegati che vorresti utilizzare sono già definite ( Azione , Func , Predicate ) e la sintassi resa più semplice con le espressioni lambda.

    
risposta data 12.09.2013 - 15:46
fonte
0

Probabilmente il modo più semplice per capire i delegati e gli eventi è guardare la programmazione della GUI. Prendiamo un caso molto comune: hai un modulo con un pulsante e quando l'utente fa clic sul pulsante, vuoi che venga eseguito un determinato codice.

Il ragazzo che ha scritto la classe dei pulsanti non ha idea di cosa vuoi fare con esso, ovviamente. Quindi non può costruire il codice per "cosa dovrebbe fare questo pulsante" nella sua classe. E anche se potesse, non dovrebbe, perché poi smette di essere un "pulsante GUI" e si trasforma in un "pulsante del caso d'uso specifico di Medivh" che è molto meno utile nel caso generale.

Quindi, invece, il tizio che ha scritto la classe del pulsante ha inserito un evento, che è una sorta di gancio nella funzionalità dell'oggetto. Un evento è fondamentalmente un pezzo di codice che viene eseguito dove qualcun altro può inserire del codice. Pertanto, quando qualcuno fa clic sul pulsante, attiva l'evento OnClick e, se hai allegato qualcosa a quell'evento, ad esempio un metodo (un delegato) che causa l'apertura di un nuovo modulo, tale delegato verrà eseguito in questo modo punto.

Quando inizi a pensare ai metodi non solo una procedura o una funzione che puoi chiamare, ma un oggetto che puoi passare ad un altro codice, apre tutte le nuove possibilità. Alcune delle altre persone hanno già menzionato LINQ e Task Parallel Library, che contengono algoritmi che definiscono l'idea di base di qualcosa, e poi ti permettono di passare in un delegato per riempire i dettagli.

    
risposta data 12.09.2013 - 15:55
fonte

Leggi altre domande sui tag