In uno dei progetti a cui partecipo c'è un vasto uso di WeakAction
. Questa è una classe che consente di mantenere il riferimento a un'istanza di azione senza che la sua destinazione non sia raccolta di dati inutili. Il modo in cui funziona è semplice, richiede un'azione sul costruttore e mantiene un riferimento debole all'obiettivo dell'azione e al metodo, ma elimina il riferimento all'azione stessa. Quando arriva il momento di eseguire l'azione, controlla se il bersaglio è ancora vivo e, in caso affermativo, invoca il metodo sulla destinazione.Bug in WeakAction in caso di azione di chiusura
Funziona tutto bene tranne che per un caso - quando l'azione viene istanziata in una chiusura. consideri il seguente esempio:
public class A
{
WeakAction action = null;
private void _register(string msg)
{
action = new WeakAction(() =>
{
MessageBox.Show(msg);
}
}
}
Dal momento che l'espressione lambda sta usando la variabile locale msg
, l'auto compilatore C# genera una classe annidata per contenere tutte le variabili di chiusura. La destinazione dell'azione è un'istanza della classe nidificata invece dell'istanza di A. L'azione passata al costruttore WeakAction
non viene referenziata una volta che il costruttore è terminato e quindi il garbage collector può eliminarlo istantaneamente. Successivamente, se viene eseguito il WeakAction
, non funzionerà perché la destinazione non è più attiva, anche se l'istanza originale di A
è attiva.
Ora non riesco a cambiare il modo in cui viene chiamato il WeakAction
(poiché è ampiamente utilizzato), ma posso modificarne l'implementazione. Stavo pensando di provare a trovare un modo per accedere all'istanza di A e forzare l'istanza della classe nidificata a rimanere in vita mentre l'istanza di A è ancora viva, ma non so come ottenerla.
Ci sono un sacco di domande su ciò che A
ha a che fare con qualsiasi cosa, e suggerimenti per cambiare il modo in A
crea una debole azione (che non siamo in grado di fare) ecco una precisazione:
Un'istanza della classe A
desidera un'istanza della classe B
per notificarlo quando succede qualcosa, quindi fornisce una richiamata utilizzando un oggetto Action
. A
non sa che B
utilizza azioni deboli, fornisce semplicemente un Action
da utilizzare come richiamata. Il fatto che B
utilizzi WeakAction
è un dettaglio di implementazione non esposto. B
deve memorizzare questa azione e usarla quando necessario. Ma lo B
potrebbe vivere molto più a lungo di A
, e mantenendo un forte riferimento a una normale azione (che di per sé contiene un riferimento forte dell'istanza di A
che lo ha generato) fa sì che A
non venga mai sottoposto a garbage collection. Se A
fa parte di un elenco di elementi che non sono più in vita, ci si aspetta che A
venga raccolto in modo non corretto e poiché il riferimento è valido per B
dell'azione, che di per sé punta a A
, si verifica una perdita di memoria.
Così, invece di B
in possesso di un azione che A
fornito, B
avvolge in una WeakAction
e memorizza solo l'azione deboli. Quando arriva il momento di chiamarlo, B
lo fa solo se lo WeakAction
è ancora attivo, che dovrebbe essere finché lo A
è ancora attivo.
A
crea quell'azione all'interno di un metodo e non mantiene un riferimento a se stesso da solo - è un dato. Poiché il Action
è stato creato nel contesto di un'istanza specifica di A
, quell'istanza è la destinazione di A
e quando A
muore, tutti i riferimenti deboli ad esso diventano null
quindi B
sa di non chiamarlo e smaltisce l'oggetto WeakAction
.
Ma a volte il metodo che ha generato il Action
utilizza le variabili definite localmente in tale funzione. Nel qual caso il contesto in cui viene eseguita l'azione include non solo l'istanza di A
, ma anche lo stato delle variabili locali all'interno del metodo (che è chiamato "chiusura"). Il compilatore C# lo fa creando una classe nidificata nascosta per contenere queste variabili (chiamiamola A__closure
) e l'istanza che diventa la destinazione di Action
è un'istanza di A__closure
, non di A
. Questo è qualcosa di cui l'utente non dovrebbe essere a conoscenza. Tranne che questa istanza di A__closure
è referenziata solo dall'oggetto Action
. E poiché creiamo un riferimento debole all'obiettivo e non manteniamo un riferimento all'azione, non vi è alcun riferimento all'istanza A__closure
e il garbage collector può (e di solito lo fa) eliminarlo istantaneamente. Quindi, A__closure
vive, A__closure
, e nonostante il fatto che A
si aspetti ancora che il callback venga richiamato, lo B
non può farlo.
Questo è l'errore.
La mia domanda era se qualcuno conosce un modo in cui il WeakAction
costruttore, l'unico pezzo di codice che in realtà contiene l'oggetto di azione originale, in via temporanea, può in qualche modo magico estrarre l'istanza originale del A
dall'istanza A__closure
che trova nello Target
dello Action
. In tal caso, potrei forse estendere il ciclo di vita A__Closure
a quello dello A
.
È stato risolto nelle versioni successive di C#? –