2009-09-14 6 views
12

Si consideri il seguente codice di esempio:La creazione di due istanze delegato per lo stesso metodo anonimo non sono uguali

static void Main(string[] args) 
{ 
    bool same = CreateDelegate(1) == CreateDelegate(1); 
} 

private static Action CreateDelegate(int x) 
{ 
    return delegate { int z = x; }; 
} 

Si potrebbe immaginare che le due istanze delegato paragonerei ad essere uguali, proprio come avrebbero quando si usa il buon vecchio metodo metodo denominato (nuova azione (MyMethod)). Non vengono confrontati per essere uguali perché .NET Framework fornisce un'istanza di chiusura nascosta per istanza delegata. Poiché queste due istanze delegate hanno ciascuna le loro proprietà di destinazione impostate sulla loro singola istanza nascosta, esse non vengono confrontate. Una possibile soluzione è che l'IL generato per un metodo anonimo memorizzi l'istanza corrente (questo puntatore) nella destinazione del delegato. Ciò consentirà ai delegati di confrontare correttamente e anche di aiutare da un punto di vista del debugger poiché vedrai la tua classe come target, invece di una classe nascosta.

È possibile leggere ulteriori informazioni su questo problema nel bug che ho inviato a Microsoft. Il bug report fornisce anche un esempio del perché stiamo usando questa funzionalità, e perché riteniamo che dovrebbe essere cambiato. Se ritieni che questo sia un problema, ti preghiamo di supportarlo fornendo rating e convalida.

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=489518

Potete vedere ogni possibile ragione per cui la funzionalità non dovrebbe essere cambiato? Pensi che questa sia stata la migliore linea di condotta per risolvere il problema, o ti consiglio di prendere una strada diversa?

+2

In quale specifica ha Microsoft contare che questi due dovrebbero essere uguali? Con lo stesso ragionamento, posso affermare che 'new MyClass() == new MyClass()' dovrebbe essere 'true' se' MyClass' è una classe vuota, per esempio. Sei troppo veloce per nominare un comportamento che non è menzionato in alcuna specifica come "bug". –

+0

In quale specifica hanno menzionato che non dovrebbero essere uguali? –

risposta

19

Non sono così propenso a pensare che questo sia un "bug". Sembra inoltre che stai assumendo alcuni comportamenti nel CLR che semplicemente non esiste.

La cosa importante da capire qui è che si restituisce un nuovo metodo anonimo (e si inizializza una nuova classe di chiusura) ogni volta che si chiama il metodo CreateDelegate. Sembra che tu stia sperimentando la parola chiave delegate per utilizzare internamente qualche sorta di pool per metodi anonimi. Il CLR certamente non lo fa. Un delegato al metodo anonimo (come con un'espressione lambda) viene creato in memoria ogni volta che si chiama il metodo e poiché l'operatore di uguaglianza confronta ovviamente riferimenti in questa situazione, è il risultato previsto che restituisce false.

Sebbene il comportamento suggerito possa avere alcuni vantaggi in determinati contesti, sarebbe probabilmente piuttosto complicato da implementare e molto probabilmente porterebbe a scenari imprevedibili. Penso che l'attuale comportamento di generare un nuovo metodo anonimo e delegare ad ogni chiamata sia quello giusto, e sospetto che questo sia il feedback che otterrai anche su Microsoft Connect.

Se si è abbastanza insistenti nell'avere il comportamento descritto nella domanda, c'è sempre l'opzione memoizing la funzione CreateDelegate, che assicurerebbe che lo stesso delegato venga restituito ogni volta per gli stessi parametri. Infatti, poiché è così facile da implementare, è probabilmente uno dei tanti motivi per cui Microsoft non ha preso in considerazione l'implementazione nel CLR.

+0

Le espressioni lambda non hanno nulla a che fare con questo. Guarda l'IL. Non sto suggerendo alcun tipo di pazzesco raggruppamento o qualcosa del genere. Sto semplicemente suggerendo di mettere da parte l'implementazione, come faresti, come lo sviluppatore che scrive il codice, a farlo funzionare sapendo cosa sai del confronto di un delegato. Dimentica i dettagli di implementazione dei metodi anonimi. Voglio semplicemente cambiarlo per il meglio della comunità degli sviluppatori e del Framework. –

+0

@BigUnit: le espressioni Lambda hanno molto a che fare con questo. È proprio perché sono generati al volo che stai vivendo questa confusione. Personalmente, sono con gli sviluppatori di .NET Framework su questo. Se ho scritto il codice indicato nella domanda precedente, il risultato effettivo sarebbe il risultato atteso. Naturalmente, si può difficilmente classificare questo comportamento 'intuitivo', ma penso che vedrete che è perfettamente il comportamento ragionevolmente una volta che ti avvolgi nei meccanismi interni del CLR. – Noldorin

+0

@Noldorin: Siamo spiacenti, non vedo ancora dove si adatta l'espressione lambda. Non c'è nessuna LE nel mio codice, né nell'IL prodotto. Non esiste certamente una generazione al volo al volo, se è quello che stai ottenendo. –

4

Non conosco i dettagli specifici di C# di questo problema, ma ho lavorato sulla funzione equivalente di VB.Net che ha lo stesso comportamento.

La linea di fondo è questo comportamento è "By Design" per i seguenti motivi

La prima è che in questo scenario una chiusura è inevitabile. Hai usato un pezzo di dati locali all'interno di un metodo anonimo e quindi è necessaria una chiusura per catturare lo stato. Ogni chiamata a questo metodo deve creare una nuova chiusura per una serie di motivi.Pertanto ciascun delegato indicherà un metodo di istanza su quella chiusura.

Sotto il cofano un metodo/espressione anonimo è rappresentato da un'istanza derivata System.MulticastDelegate nel codice. Se si guarda al metodo Equals di questa classe si noterà 2 importanti dettagli

  • Si è sigillato in modo non v'è alcun modo per un delegato derivato per modificare il comportamento uguale
  • parte del metodo Equals fa un riferimento confronto sugli oggetti

Ciò rende impossibile per 2 espressioni lambda collegate a chiusure diverse da confrontare come uguali.

+0

Sono d'accordo sul fatto che oggi il Framework rende impossibile confrontare i metodi anonimi. Solo perché indicano due casi diversi non significa che non possano essere paragonati per essere equivalenti. Certo significherebbe un cambiamento, ma certamente non lo direi che sia impossibile così rapidamente. –

+0

@BigUnit, sto dicendo che è impossibile dato lo stato attuale del BCL. Allo stesso tempo, sono d'accordo sul fatto che il comportamento del BCL sia corretto. Due metodi di istanza su due oggetti non dovrebbero mai confrontarsi per essere uguali. – JaredPar

+0

@JaredPar: Capisco quello che stai dicendo, ma è astratto lontano da te. Hai dimenticato di dire che i due oggetti sono nascosti allo sviluppatore. Il fatto che acquisisca lo stato è solo per renderlo più facile allo sviluppatore. Dammi un buon esempio di codice esistente che potrebbe interrompere la modifica di questa funzionalità? Fa schifo non c'è un buon lavoro intorno a questo problema che rende semplice per la persona che usa l'API eccetto per non usare metodi anonimi ed essere forzato ad usare il paradigma Func/Action. Sarei quindi costretto a creare lo stesso numero di classi generiche nella mia situazione, circa 30. –

0

Non riesco a pensare a una situazione in cui abbia mai avuto bisogno di farlo. Se ho bisogno di confrontare i delegati che uso sempre delegati nominati, altrimenti qualcosa di simile sarebbe possibile:

MyObject.MyEvent += delegate { return x + y; }; 

MyObject.MyEvent -= delegate { return x + y; }; 

Questo esempio non è grande per dimostrare il problema, ma mi immagino che ci potrebbe essere una situazione in cui permettendo questo potrebbe rompere il codice esistente che è stato progettato con l'aspettativa che questo non è permesso.

Sono sicuro che ci sono dettagli di implementazione interni che rendono anche questa una cattiva idea, ma non so esattamente come i metodi anonimi siano implementati internamente.

+0

Nessuno al momento fa qualcosa del genere perché i risultati di solito sarebbero falsi. Ma qui è un caso in cui il Framework consente un tale risultato: Se ho un metodo come questo: public Action CreateDelegate() { delegato di ritorno {CallSomeMethod(); }; } E quindi utilizzare tale funzione per il collegamento/rimozione evento: MyEvent + = CreateDelegate(); MyEvent - = CreateDelegate(); Funziona senza problemi. Anche attualmente ci sono casi in cui permetteranno loro di essere equivalenti. Quindi, non c'è modo di scrivere codice per determinare l'equivalenza oggi. –

3

EDIT: Vecchio risposta lasciato per valore storico sotto la linea ...

Il CLR avrebbe dovuto lavorare i casi in cui le classi nascoste potrebbero essere considerati uguali, tenendo conto tutto ciò che potrebbe essere fatto con le variabili catturate.

In questo caso particolare, la variabile acquisita (x) non viene modificata all'interno del delegato o nel contesto di acquisizione, ma preferirei che il linguaggio non richiedesse questo tipo di complessità di analisi. Più il linguaggio è complicato, più difficile è capire. Dovrebbe distinguere tra questo caso e quello sotto, dove il valore della variabile catturata viene cambiato ad ogni invocazione - lì, fa una grande quantità di differenze che delegano la chiamata; non sono in alcun modo uguali.

Penso sia del tutto ragionevole che questa situazione già complessa (le chiusure sono spesso fraintese) non cerchi di essere troppo "intelligente" e di elaborare la potenziale uguaglianza.

IMO, si dovrebbe sicuramente prendere un altro percorso. Questi sono concettualmente istanze indipendenti di Action. Fingendolo costringendo gli obiettivi dei delegati è un orribile hack IMO.


Il problema è che si sta catturando il valore di x in una classe generata. Le due variabili x sono indipendenti, quindi sono delegati non uguali.Ecco un esempio che dimostra l'indipendenza:

using System; 

class Test 
{ 
    static void Main(string[] args) 
    { 
     Action first = CreateDelegate(1); 
     Action second = CreateDelegate(1); 
     first(); 
     first(); 
     first(); 
     first(); 
     second(); 
     second(); 
    } 

    private static Action CreateDelegate(int x) 
    { 
     return delegate 
     { 
      Console.WriteLine(x); 
      x++; 
     }; 
    } 
} 

uscita:

1 
2 
3 
4 
1 
2 

EDIT: Per vedere le cose in un altro modo, il programma originale era l'equivalente di:

using System; 

class Test 
{ 
    static void Main(string[] args) 
    { 
     bool same = CreateDelegate(1) == CreateDelegate(1); 
    } 

    private static Action CreateDelegate(int x) 
    { 
     return new NestedClass(x).ActionMethod; 
    } 

    private class Nested 
    { 
     private int x; 

     internal Nested(int x) 
     { 
      this.x = x; 
     } 

     internal ActionMethod() 
     { 
      int z = x; 
     } 
    } 
} 

come si può indicare che verranno create due istanze separate di Nested e che saranno gli obiettivi per i due delegati. Sono diseguali, quindi anche i delegati sono diseguali.

+0

Penso che l'OP lo capisca (abbastanza bene), e piuttosto la sua domanda principale riguarda * perché * questo è il comportamento del CLR. – Noldorin

+0

@Noldorin: Sì, ho riletto la domanda e ho risposto nuovamente. –

+0

Capisco perché attualmente fa quello che fa. Hai detto: "Più è complicato il linguaggio, più difficile è capire". Tuttavia, non sto proponendo alcuna modifica al linguaggio stesso, ma al sottostante IL generato che è già stato fatto tramite il compilatore. Il Framework, in un certo senso, è fatto per semplificare la vita, quindi perché renderlo più difficile quando non dobbiamo? Cambiare il bersaglio non è la migliore risposta, ci deve essere un modo migliore. Potrebbe memorizzarlo come un campo diverso nel delegato, oppure la classe di acquisizione potrebbe memorizzarlo in modo che il delegato possa accedere per il confronto. –

0

Questo comportamento è utile perché altrimenti i metodi anonimi verrebbero confusi (se avessero lo stesso nome, dato lo stesso corpo).

Si potrebbe modificare il codice per questo:

static void Main(){ 
    bool same = CreateDelegate(1) == CreateDelegate(1); 
} 

static Action<int> action = (x) => { int z = x; }; 

private static Action<int> CreateDelegate(int x){ 
    return action; 
} 

o, preferibilmente, dato che è un brutto modo di usarlo (più di comparare il risultato, e azione non hanno un valore di ritorno .. . utilizzare Func < ...> se si vuole restituire un valore):

static void Main(){ 
    var action1 = action; 
    var action2 = action; 
    bool same = action1 == action2; // TRUE, of course 
} 

static Action<int> action = (x) => { int z = x; };