Come si impedisce a IDisposable di diffondersi in tutte le classi?


138

Inizia con queste semplici classi ...

Diciamo che ho un semplice set di classi come questo:

class Bus
{
    Driver busDriver = new Driver();
}

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

class Shoe
{
    Shoelace lace = new Shoelace();
}

class Shoelace
{
    bool tied = false;
}

A Busha a Driver, Driverha due Shoes, ognuna Shoeha a Shoelace. Tutto molto sciocco.

Aggiungi un oggetto IDisposable a Laccio

In seguito decido che alcune operazioni su Shoelacepotrebbero essere multi-thread, quindi aggiungo un messaggio EventWaitHandleper comunicare con i thread. Quindi Shoelaceora appare così:

class Shoelace
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    // ... other stuff ..
}

Implementare IDisposable su Shoelace

Ma ora FxCop di Microsoft si lamenterà: "Implementa IDisposable su 'Shoelace' perché crea membri dei seguenti tipi IDisposable: 'EventWaitHandle'."

Va bene, implemento IDisposablesu Shoelacee la mia classe di poco pulito diviene questa orribile pasticcio:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;
    private bool disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~Shoelace()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                if (waitHandle != null)
                {
                    waitHandle.Close();
                    waitHandle = null;
                }
            }
            // No unmanaged resources to release otherwise they'd go here.
        }
        disposed = true;
    }
}

Oppure (come sottolineato dai commentatori) poiché di per Shoelacesé non ha risorse non gestite, potrei usare l'implementazione di smaltimento più semplice senza la necessità di Dispose(bool)e Destructor:

class Shoelace : IDisposable
{
    private AutoResetEvent waitHandle = new AutoResetEvent(false);
    bool tied = false;

    public void Dispose()
    {
        if (waitHandle != null)
        {
            waitHandle.Close();
            waitHandle = null;
        }
        GC.SuppressFinalize(this);
    }
}

Guarda con orrore gli spread IDisposable

Esatto, è quello risolto. Ma ora FxCop si lamenterà che Shoecrea un Shoelace, così Shoedeve essere IDisposableanche.

E Drivercrea Shoecosì Driverdeve essere IDisposable. E Buscrea Drivercosì Busdeve essere IDisposablee così via.

Improvvisamente la mia piccola modifica Shoelacemi sta causando molto lavoro e il mio capo si chiede perché devo fare il checkout Busper fare una modifica Shoelace.

La domanda

Come evitare questa diffusione di IDisposable, ma garantire comunque che gli oggetti non gestiti siano correttamente smaltiti?


9
Una domanda eccezionalmente buona, credo che la risposta sia minimizzare il loro uso e cercare di mantenere gli IDisposable di alto livello di breve durata con l'uso, ma questo non è sempre possibile (specialmente dove quegli IDisposable sono dovuti all'interoperabilità con DLL C ++ o simili). Cerca le risposte.
annakata,

Va bene Dan, ho aggiornato la domanda per mostrare entrambi i metodi di implementazione di IDisposable su Shoelace.
GrahamS,

In genere sono cauto nel fare affidamento sui dettagli di implementazione di altre classi per proteggermi. Inutile rischiare se posso facilmente prevenirlo. Forse sono troppo cauto o forse ho trascorso troppo tempo come programmatore in C, ma preferirei adottare l'approccio irlandese: "Certo, certo" :)
GrahamS

@Dan: il controllo null è ancora necessario per assicurarsi che l'oggetto stesso non sia stato impostato su null, nel qual caso la chiamata a waitHandle.Dispose () genererà una NullReferenceException.
Scott Dorman,

1
In ogni caso, dovresti comunque utilizzare il metodo Dispose (bool) come mostrato nella sezione "Implement IDisposable on Shoelace" poiché (meno il finalizzatore) è il modello completo. Solo perché una classe impianta IDisposable non significa che abbia bisogno di un finalizzatore.
Scott Dorman,

Risposte:


36

Non puoi davvero "impedire" la diffusione di IDisposable. Alcune classi devono essere disposte, come AutoResetEvent, e il modo più efficace è farlo nel Dispose()metodo per evitare il sovraccarico dei finalizzatori. Ma questo metodo deve essere chiamato in qualche modo, così esattamente come nel tuo esempio le classi che incapsulano o contengono IDisposable devono smaltirle, quindi devono anche essere usa e getta, ecc. L'unico modo per evitarlo è:

  • evitare l'uso di classi IDisposable ove possibile, bloccare o attendere eventi in singoli luoghi, mantenere costose risorse in un unico posto, ecc
  • crearli solo quando ne hai bisogno e smaltirli subito dopo (lo usingschema)

In alcuni casi, IDisposable può essere ignorato perché supporta un caso opzionale. Ad esempio, WaitHandle implementa IDisposable per supportare un Mutex denominato. Se non viene utilizzato un nome, il metodo Dispose non fa nulla. MemoryStream è un altro esempio, non utilizza risorse di sistema e anche l'implementazione di Dispose non fa nulla. Un'attenta riflessione sull'utilizzo o meno di una risorsa non gestita può essere istruttiva. Così può esaminare le fonti disponibili per le librerie .net o usare un decompilatore.


4
Il consiglio che puoi ignorarlo perché l'implementazione oggi non fa nulla è male, perché una versione futura potrebbe effettivamente fare qualcosa di importante in Dispose, e ora hai difficoltà a rintracciare la perdita.
Andy,

1
"Mantieni risorse costose", le IDisposable non sono necessariamente costose, anzi questo esempio che utilizza una maniglia di attesa uniforme è circa un "peso leggero".
markmnl,

20

In termini di correttezza, non è possibile impedire la diffusione di IDisposable attraverso una relazione di oggetto se un oggetto padre crea e essenzialmente possiede un oggetto figlio che deve ora essere disponibile. FxCop è corretto in questa situazione e il genitore deve essere IDisposable.

Quello che puoi fare è evitare di aggiungere un IDisposable a una classe foglia nella gerarchia degli oggetti. Questo non è sempre un compito facile ma è un esercizio interessante. Dal punto di vista logico, non vi è alcun motivo per cui una scarpa deve essere usa e getta. Invece di aggiungere un WaitHandle qui, è anche possibile aggiungere un'associazione tra un ShoeLace e un WaitHandle nel punto in cui viene utilizzato. Il modo più semplice è tramite un'istanza del dizionario.

Se puoi spostare WaitHandle in un'associazione libera tramite una mappa nel punto in cui WaitHandle viene effettivamente utilizzato, puoi interrompere questa catena.


2
È molto strano fare un'associazione lì. Questo AutoResetEvent è privato dell'implementazione di Shoelace, quindi esporlo pubblicamente sarebbe sbagliato secondo me.
GrahamS,

@GrahamS, non sto dicendo di esporlo pubblicamente. Sto dicendo che può essere spostato al punto in cui i lacci delle scarpe sono legati. Forse se legare i lacci delle scarpe è un compito così complicato, dovrebbero essere una classe di lacci delle scarpe.
JaredPar,

1
Potresti espandere la tua risposta con alcuni frammenti di codice JaredPar? Potrei allungare un po 'il mio esempio, ma sto immaginando che Shoelace crei e avvii il thread di Tyer che attende pazientemente a waitHandle.WaitOne () The Shoelace chiamerebbe waitHandle.Set () quando voleva che il thread iniziasse a legare.
GrahamS,

1
+1 su questo. Penso che sarebbe strano che solo Shoelace venga chiamato contemporaneamente ma non gli altri. Pensa a quella che dovrebbe essere la radice aggregata. In questo caso dovrebbe essere Bus, IMHO, anche se non ho familiarità con il dominio. Quindi, in quel caso, il bus dovrebbe contenere il waithandle e tutte le operazioni contro il bus e tutti i suoi figli saranno sincronizzati.
Johannes Gustafsson,

16

Per evitare la IDisposablediffusione, dovresti provare a incapsulare l'uso di un oggetto usa e getta all'interno di un singolo metodo. Prova a progettare Shoelacediversamente:

class Shoelace { 
  bool tied = false; 

  public void Tie() {

    using (var waitHandle = new AutoResetEvent(false)) {

      // you can even pass the disposable to other methods
      OtherMethod(waitHandle);

      // or hold it in a field (but FxCop will complain that your class is not disposable),
      // as long as you take control of its lifecycle
      _waitHandle = waitHandle;
      OtherMethodThatUsesTheWaitHandleFromTheField();

    } 

  }
} 

L'ambito della maniglia di attesa è limitato a Tie metodo e la classe non ha bisogno di avere un campo usa e getta, quindi non dovrà essere usa e getta da sola.

Poiché l'handle wait è un dettaglio di implementazione all'interno di Shoelace, non dovrebbe in alcun modo cambiare la sua interfaccia pubblica, come aggiungere una nuova interfaccia nella sua dichiarazione. Cosa succederà quindi quando non avrai più bisogno di un campo monouso, rimuoverai la IDisposabledichiarazione? Se pensi Shoelace all'astrazione , ti rendi presto conto che non dovrebbe essere inquinato dalle dipendenze dell'infrastruttura, come IDisposable. IDisposabledovrebbe essere riservato alle classi la cui astrazione incapsula una risorsa che richiede una pulizia deterministica; vale a dire, per le classi in cui la disponibilità è parte dell'astrazione .


1
Concordo pienamente sul fatto che i dettagli di implementazione di Shoelace non debbano inquinare l'interfaccia pubblica, ma il mio punto è che può essere difficile da evitare. Quello che suggerisci qui non sarebbe sempre possibile: lo scopo di AutoResetEvent () è di comunicare tra i thread, quindi il suo ambito di solito si estenderebbe oltre un singolo metodo.
GrahamS,

@GrahamS: ecco perché ho detto di provare a progettarlo in quel modo. È inoltre possibile passare il materiale monouso ad altri metodi. Si interrompe solo quando le chiamate esterne alla classe controllano il ciclo di vita del monouso. Aggiornerò la risposta di conseguenza.
Jordão,

2
scusa, so che puoi passare il materiale usa e getta in giro, ma ancora non lo vedo funzionare. Nel mio esempio AutoResetEventviene utilizzato per comunicare tra thread diversi che operano all'interno della stessa classe, quindi deve essere una variabile membro. Non puoi limitarne l'ambito a un metodo. (ad esempio, immagina che un thread rimanga in attesa di lavoro bloccandosi waitHandle.WaitOne(). Il thread principale chiama quindi il shoelace.Tie()metodo, che fa semplicemente un waitHandle.Set()e ritorna immediatamente).
GrahamS,

3

Questo è fondamentalmente ciò che accade quando si mescolano le classi Composizione o Aggregazione con Classi usa e getta. Come accennato, la prima via d'uscita sarebbe quella di refactoring l'attesaHandle fuori dai lacci delle scarpe.

Detto questo, è possibile ridurre considerevolmente il modello Monouso quando non si hanno risorse non gestite. (Sto ancora cercando un riferimento ufficiale per questo.)

Ma puoi omettere il distruttore e GC.SuppressFinalize (questo); e forse ripulire un po 'il vuoto virtuale Dispose (bool disposing).


Grazie Henk. Se ho preso la creazione di waitHandle di merletto, allora qualcuno deve ancora smaltire qualche parte in modo da come vorrei che qualcuno sapere che il merletto è finito con esso (e che il merletto non ha superato a qualsiasi altra classe)?
GrahamS,

Dovrai spostare non solo la creazione ma anche la "responsabilità" del maniglione. La risposta di jaredPar ha un inizio interessante di un'idea.
Henk Holterman,

Questo blog tratta l'implementazione di IDisposable, e in particolare affronta come semplificare l'attività evitando di mescolare risorse gestite e non gestite in un blog di
PaulS

3

È interessante notare se Driverè definito come sopra:

class Driver
{
    Shoe[] shoes = { new Shoe(), new Shoe() };
}

Quindi quando Shoeviene creato IDisposable, FxCop (v1.36) non si lamenta che Driverdovrebbe esserloIDisposable .

Tuttavia, se definito in questo modo:

class Driver
{
    Shoe leftShoe = new Shoe();
    Shoe rightShoe = new Shoe();
}

allora si lamenterà.

Ho il sospetto che questa sia solo una limitazione di FxCop, piuttosto che una soluzione, perché nella prima versione le Shoeistanze vengono ancora create da Drivere devono comunque essere eliminate in qualche modo.


2
Se Show implementa IDisposable, crearlo in un inizializzatore di campo è pericoloso. È interessante notare che FXCop non consentirebbe tali inizializzazioni, poiché in C # l'unico modo per eseguirle in sicurezza è con le variabili ThreadStatic (decisamente orribili). In vb.net, è possibile inizializzare oggetti IDisposable in modo sicuro senza variabili ThreadStatic. Il codice è ancora brutto, ma non così orribile. Vorrei che MS potesse fornire un buon modo per utilizzare in modo sicuro tali inizializzatori di campo.
supercat

1
@supercat: grazie. Puoi spiegare perché è pericoloso? Immagino che se un'eccezione venisse lanciata in uno dei Shoecostruttori, shoesrimarrebbe in uno stato difficile?
Graham

3
A meno che non si sappia che un particolare tipo di IDisposable può essere abbandonato in modo sicuro, è necessario assicurarsi che ogni oggetto creato venga eliminato. Se un IDisposable viene creato nell'inizializzatore di campo di un oggetto ma viene generata un'eccezione prima che l'oggetto sia completamente costruito, l'oggetto e tutti i suoi campi verranno abbandonati. Anche se la costruzione dell'oggetto è racchiusa in un metodo factory che utilizza un blocco "try" per rilevare che la costruzione non è riuscita, è difficile per la fabbrica ottenere riferimenti agli oggetti che necessitano di essere eliminati.
supercat,

3
L'unico modo che conosco per affrontarlo in C # sarebbe usare una variabile ThreadStatic per mantenere un elenco di oggetti che dovranno essere eliminati se viene lanciato l'attuale costruttore. Quindi fai in modo che ciascun inizializzatore di campo registri ogni oggetto mentre viene creato, fai cancellare la fabbrica dall'elenco se si completa correttamente e usa una clausola "finally" per eliminare tutti gli elementi dall'elenco se non è stato cancellato. Sarebbe un approccio praticabile, ma le variabili ThreadStatic sono brutte. In vb.net, si potrebbe usare una tecnica simile senza la necessità di variabili ThreadStatic.
supercat

3

Non credo che esista un modo tecnico per impedire la diffusione di IDisposable se il tuo design è così strettamente accoppiato. Ci si dovrebbe quindi chiedere se il design sia giusto.

Nel tuo esempio, penso che abbia senso avere la scarpa propria del laccio, e forse il conducente dovrebbe possedere le sue scarpe. Tuttavia, il bus non dovrebbe possedere l'autista. In genere, i conducenti di autobus non seguono gli autobus fino allo scalo :) Nel caso di conducenti e scarpe, raramente i conducenti si fabbricano le proprie scarpe, il che significa che non le "possiedono" realmente.

Un design alternativo potrebbe essere:

class Bus
{
   IDriver busDriver = null;
   public void SetDriver(IDriver d) { busDriver = d; }
}

class Driver : IDriver
{
   IShoePair shoes = null;
   public void PutShoesOn(IShoePair p) { shoes = p; }
}

class ShoePairWithDisposableLaces : IShoePair, IDisposable
{
   Shoelace lace = new Shoelace();
}

class Shoelace : IDisposable
{
   ...
}

Il nuovo design è purtroppo più complicato, in quanto richiede ulteriori classi per istanziare e smaltire esempi concreti di scarpe e driver, ma questa complicazione è inerente al problema da risolvere. La cosa buona è che gli autobus non devono più essere usa e getta semplicemente allo scopo di smaltire i lacci delle scarpe.


2
Questo non risolve davvero il problema originale. Potrebbe mantenere silenzioso FxCop ma qualcosa dovrebbe comunque smaltire il laccio.
AndrewS,

Penso che tutto ciò che hai fatto sia spostare il problema altrove. ShoePairWithDisposableLaces ora possiede Shoelace (), quindi deve anche essere reso IDisposable - quindi chi dispone delle scarpe? Lo stai lasciando ad alcuni container IoC da gestire?
GrahamS,

@AndrewS: In effetti, qualcosa deve ancora disporre di driver, scarpe e lacci, ma lo smaltimento non dovrebbe essere avviato dagli autobus. @GrahamS: hai ragione, sto spostando il problema altrove, perché credo che appartenga altrove. In genere, ci sarebbe una classe di scarpe da istanziazione, che sarebbe anche responsabile dello smaltimento delle scarpe. Ho modificato il codice per rendere anche ShoePairWithDisposableLaces usa e getta.
Joh,

@Joh: grazie. Come dici tu, il problema con lo spostamento altrove è che crei molta più complessità. Se ShoePairWithDisposableLaces è creato da un'altra classe, allora quella classe non dovrebbe essere avvisata quando il Driver ha finito con le sue Scarpe, in modo che possa eliminarle correttamente ()?
GrahamS,

1
@Graham: sì, sarebbe necessario un meccanismo di notifica di qualche tipo. Ad esempio, è possibile utilizzare una politica di smaltimento basata sui conteggi di riferimento. C'è una certa complessità aggiuntiva, ma come probabilmente saprai, "Non esiste una scarpa gratis" :)
Joh,

1

Che ne dici di usare Inversion of Control?

class Bus
{
    private Driver busDriver;

    public Bus(Driver busDriver)
    {
        this.busDriver = busDriver;
    }
}

class Driver
{
    private Shoe[] shoes;

    public Driver(Shoe[] shoes)
    {
        this.shoes = shoes;
    }
}

class Shoe
{
    private Shoelace lace;

    public Shoe(Shoelace lace)
    {
        this.lace = lace;
    }
}

class Shoelace
{
    bool tied;
    private AutoResetEvent waitHandle;

    public Shoelace(bool tied, AutoResetEvent waitHandle)
    {
        this.tied = tied;
        this.waitHandle = waitHandle;
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var leftShoeWaitHandle = new AutoResetEvent(false))
        using (var rightShoeWaitHandle = new AutoResetEvent(false))
        {
            var bus = new Bus(new Driver(new[] {new Shoe(new Shoelace(false, leftShoeWaitHandle)),new Shoe(new Shoelace(false, rightShoeWaitHandle))}));
        }
    }
}

Ora hai risolto il problema con i chiamanti e Shoelace non può dipendere dalla maniglia di attesa disponibile quando ne ha bisogno.
Andy,

@andy Cosa intendi con "Laccio non può dipendere dalla maniglia di attesa disponibile quando ne ha bisogno"? È passato al costruttore.
Darragh,

Qualcos'altro potrebbe aver incasinato lo stato di autoresetevents prima di darlo a Shoelace, e potrebbe iniziare con l'ARE in un cattivo stato; qualcos'altro potrebbe confondere con lo stato di ARE mentre Shoelace sta facendo la sua cosa, facendo sì che Shoelace faccia la cosa sbagliata. È lo stesso motivo per cui blocchi solo i membri privati. "In generale, .. evita di bloccare istanze al di fuori del controllo dei tuoi codici" docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
Andy,

Sono d'accordo solo con il blocco dei membri privati. Ma sembra che le maniglie di attesa siano diverse. In effetti, sembra più utile passarli all'oggetto poiché è necessario utilizzare la stessa istanza per comunicare tra i thread. Tuttavia, penso che l'IoC offra una soluzione utile al problema del PO.
Darragh,

Dato che l'esempio dei PO aveva il waithandle come membro privato, il presupposto sicuro è che l'oggetto si aspetta un accesso esclusivo ad esso. Rendere visibile l'handle all'esterno dell'istanza è una violazione. In genere un IoC può essere utile per cose come questa, ma non quando si tratta di threading.
Andy,
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.