Perché questo non causa una perdita di memoria quando l’evento non è annullato

Sto cercando di capire come gli eventi possono causare una perdita di memoria. Ho trovato una buona spiegazione a questa domanda di stackoverflow ma quando guardo gli oggetti in Windg, mi sto confondendo con il risultato. Per cominciare, ho una class semplice come segue.

class Person { public string LastName { get; set; } public string FirstName { get; set; } public event EventHandler UponWakingUp; public Person() { } public void Wakeup() { Console.WriteLine("Waking up"); if (UponWakingUp != null) UponWakingUp(null, EventArgs.Empty); } } 

Ora sto usando questa class in un’applicazione Windows form come segue.

 public partial class Form1 : Form { Person John = new Person() { LastName = "Doe", FirstName = "John" }; public Form1() { InitializeComponent(); John.UponWakingUp += new EventHandler(John_UponWakingUp); } void John_UponWakingUp(object sender, EventArgs e) { Console.WriteLine("John is waking up"); } private void button1_Click(object sender, EventArgs e) { John = null; GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); MessageBox.Show("done"); } } 

Come puoi vedere, ho istanziato la class Person e mi sono iscritto all’evento UponWakingUp. Ho un pulsante su questo modulo. Quando l’utente fa clic su questo pulsante, imposto questa istanza Persona a null senza annullare la sottoscrizione all’evento. Quindi chiamo GC.Collect per essere sicuro che la raccolta Garbade sia stata eseguita. Sto mostrando una finestra di messaggio qui in modo che possa albind Windbg per cercare i riferimenti help dalla class Form1 e all’interno di questa class non vedo alcun riferimento a quell’evento (l’output di Windbg è mostrato sotto sebbene Form1 abbia dati troppo lunghi, sto visualizzando relativo alla mia domanda). Questa class ha un riferimento alla class Person ma è nullo. Fondamentalmente questo non mi sembra una perdita di memoria perché Form1 non ha alcun riferimento alla class Person anche se non è stata annullata dall’evento.

La mia domanda è se questo causa perdite di memoria. Se no, perché no?

 0:005> !do 0158d334 Name: WindowsFormsApplication1.Form1 MethodTable: 00366390 EEClass: 00361718 Size: 332(0x14c) bytes File: c:\Sandbox\\WindowsFormsApplication1\WindowsFormsApplication1\bin\Debug\WindowsFormsApplication1.exe Fields: MT Field Offset Type VT Attr Value Name 619af744 40001e0 4 System.Object 0 instance 00000000 __identity 60fc6c58 40002c3 8 ...ponentModel.ISite 0 instance 00000000 site 619af744 4001534 b80 System.Object 0 static 0158dad0 EVENT_MAXIMIZEDBOUNDSCHANGED **00366b70 4000001 13c ...plication1.Person 0 instance 00000000 John** 60fc6c10 4000002 140 ...tModel.IContainer 0 instance 00000000 components 6039aadc 4000003 144 ...dows.Forms.Button 0 instance 015ad06c button1 0:008> !DumpHeap -mt 00366b70 Address MT Size total 0 objects Statistics: MT Count TotalSize Class Name Total 0 objects 

Questo è un caso di riferimento circolare . Il modulo ha un riferimento all’object che ascolta l’evento attraverso il campo John . A sua volta, John ha un riferimento al modulo quando il suo evento OnWakingUp è stato sottoscritto dal costruttore del modulo.

I riferimenti circolari possono essere un problema in alcuni schemi di gestione automatica della memoria, in particolare nel conteggio dei riferimenti. Ma il garbage collector .NET non ha problemi. Finché né l’object modulo né l’object Persona hanno riferimenti aggiuntivi, il riferimento circolare tra i due non può mantenerli vivi.

Non ci sono riferimenti aggiuntivi nel codice. Il che normalmente causerebbe la raccolta di dati da parte di entrambi gli oggetti. Ma la class Form è speciale, purché esista una finestra nativa di Windows, un riferimento interno memorizzato in una tabella handle-to-object gestita da Winforms mantiene vivo l’object form. Che tiene John vivo.

Quindi il modo normale in cui viene ripulito è che l’utente chiude la finestra facendo clic sulla X nell’angolo in alto a destra. Il che a sua volta fa sì che l’handle della finestra nativa venga distrutto. Che rimuove il riferimento al modulo da quella tabella interna. Il prossimo garbage collection ora non vede altro che il riferimento circolare e li raccoglie entrambi.

La risposta è in realtà nella risposta alla domanda a cui ti sei collegato:

Quando un listener collega un listener di eventi a un evento, l’object di origine otterrà un riferimento all’object listener. Ciò significa che il garbage collector non può raccogliere il listener fino a quando il gestore di eventi non viene rimosso o l’object di origine viene raccolto .

Stai rilasciando l’object sorgente ( Person ) in modo che il Listener (il tuo Form ) sia ok per essere raccolto, motivo per cui non vi è perdita di memoria.

Si verificherà una perdita di memoria quando questa situazione è inversa rispetto a IE quando si desidera disporre del Form ma la fonte dell’evento (object Person ) è ancora in vita con un riferimento.