2010-01-25 18 views
11

Rzućmy okiem na niesławnej IDisposable interfejsu:Dlaczego IDisposable realizacja zaprojektowany tak jest

[ComVisible(true)] 
public interface IDisposable 
{ 
    void Dispose(); 
} 

i typowej implementacji, zgodnie z zaleceniem MSDN (I pominięta sprawdzanie, czy obecny obiekt został już umieszczony):

public class Base : IDisposable 
{ 
    protected virtual void Dispose(bool disposing) 
    { 
     if (disposing) 
     { 
      // release managed 
     } 
     // release unmanaged 
     disposed = true; 
    } 

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

    ~Base() 
    { 
     Dispose(false); 
    } 
} 

public class Derived : Base 
{ 
    protected override void Dispose(bool disposing) 
    { 
     base.Dispose(disposing); 
     if (disposing) 
     { 
      // release managed 
     } 
     // release unmanaged 
     disposed = true; 
    } 
} 

Problem jest następujący: Myślę, że ta implementacja jest sprzeczna z intuicją. Jest również znacząco różny w klasie podstawowej i pochodnej. Klasa pochodna ma na celu założyć, że klasa podstawowa zaimplementowana prawidłowo IDisposable, a następnie przesłonić Dispose (bool), która nie jest nawet częścią oryginalnego interfejsu.

Muszę przyznać, że wpadłem na to pytanie, ponieważ zwykle prosiłem młodszych programistów o wdrożenie IDisposable na rozmowę o pracę. Jeśli nie wiem dokładnie, jak to ma być zrobione, oni wymyślić coś blisko tego:

public class Base : IDisposable 
{ 
    public virtual void Dispose() 
    { 
     // release managed and unmanaged 
     GC.SuppressFinalize(this); 
    } 

    ~Base() 
    { 
     // release unmanaged 
    } 
} 

public class Derived : Base 
{ 
    public override void Dispose() 
    { 
     // release managed and unmanaged 
     base.Dispose(); 
    } 

    ~Derived() 
    { 
     // release unmanaged 
    } 
} 

do mnie, to realizacja jest bardziej jasne i bardziej spójne. Oczywiście, złe jest to, że musimy wydać niezarządzane zasoby w dwóch różnych miejscach, ale ważne jest to, że prawdopodobnie ponad 99% klas niestandardowych nie ma niczego niezarządzanego do usunięcia, więc i tak nie będą potrzebować finalizatora. Nie umiem wytłumaczyć młodszemu programistowi, dlaczego implementacja MSDN jest lepsza, ponieważ sam tego nie rozumiem.

Zastanawiam się więc, co doprowadziło do tak niezwykłych decyzji projektowych (uczynienie klasy pochodnej nadrzędną inną metodą niż w interfejsie i zmuszanie go do myślenia o niezarządzanych zasobach, których najprawdopodobniej nie zawiera). Wszelkie przemyślenia w tej sprawie?

Odpowiedz

7

Więc zastanawiam się, co doprowadziło do takich nietypowych decyzji projektowych (tworzących klasę pochodną zastąpić inną metodę niż w interfejsie i czyniąc go myśleć o niezagospodarowanych zasobów, które najprawdopodobniej nie robi” t zawierać). Wszelkie przemyślenia w tej sprawie?

Głównym problemem jest to, że IDisposable zostało zdefiniowane PO strukturze już zaprojektowanej i istniejącej. Jest przeznaczony do radzenia sobie z sytuacją, której kod zarządzany próbuje uniknąć - tak naprawdę jest to skrajny przypadek, jeśli jest bardzo powszechny. ;)

Można to zobaczyć, przy okazji, jeśli spojrzysz na C++/CLI. Został zaprojektowany po IDisposable, w wyniku czego implementuje IDisposable w znacznie bardziej naturalny sposób (destruktory [~ClassName] automatycznie stają się Dispose, a finalizatory [!ClassName] są traktowane jako finalizatory).

Inną kwestią jest to, że IDisposable obsługuje wiele sytuacji.Napisałem numer entire blog series, przechodząc przez różne implementacje, które powinny być używane podczas pakowania natywnego kodu, enkapsulacji klasy implementującej IDisposable i używania jej z typami faktycznymi.

Z technicznego punktu widzenia tylko musi zaimplementować interfejs bezpośrednio. Decyzja projektowa pozwalająca na zastosowanie metody protected virtual void Dispose(bool disposing) pozwala uzyskać dodatkową elastyczność, która nie byłaby łatwo i bezpiecznie obsługiwana w publicznym interfejsie.

0

Rozumiem, że cały powód IDisposable polega na udostępnianiu zasobów niezarządzanych, więc jestem zdezorientowany, dlaczego mówisz "99% niestandardowych klas nie ma niczego niezarządzanego do usunięcia" - jeśli implementujesz IDisposable, powinno być, ponieważ masz niezarządzane zasoby.

MSDN IDisposable

+0

Nieprawidłowy. Jeśli dysponujesz zasobami, powinieneś wdrożyć IDisposable i zutylizować je. – SLaks

+0

Nie zawsze prawda. Być może typem enkapsulacji jest natywne zasoby lub implementacja typu faktorowego. –

+0

@SLaks: Jeśli zasoby są w pełni zarządzane, jest to mniej ważne (ponieważ całkowicie zarządzany typ będzie obsługiwany poprawnie przez GC ...) –

1

MSDN Magazine ma article about this pattern.

To nie odpowiada na pytanie, ale możesz użyć poniższego fragmentu kodu do implementacji wzorca.

<?xml version="1.0" encoding="utf-8" ?> 
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> 
    <CodeSnippet Format="1.0.0"> 
     <Header> 
      <Title>Dispose pattern</Title> 
      <Shortcut>dispose</Shortcut> 
      <Description>Code snippet for virtual dispose pattern</Description> 
      <Author>SLaks</Author> 
      <SnippetTypes> 
       <SnippetType>Expansion</SnippetType> 
       <SnippetType>SurroundsWith</SnippetType> 
      </SnippetTypes> 
     </Header> 
     <Snippet> 
      <Declarations> 
       <Literal Editable="false"> 
        <ID>classname</ID> 
        <ToolTip>Class name</ToolTip> 
        <Default>ClassNamePlaceholder</Default> 
        <Function>ClassName()</Function> 
       </Literal> 
      </Declarations> 
      <Code Language="csharp"> 
       <![CDATA[ 
     ///<summary>Releases unmanaged resources and performs other cleanup operations before the $classname$ is reclaimed by garbage collection.</summary> 
     ~$classname$() { Dispose(false); } 
     ///<summary>Releases all resources used by the $classname$.</summary> 
     public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } 
     ///<summary>Releases the unmanaged resources used by the $classname$ and optionally releases the managed resources.</summary> 
     ///<param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param> 
     protected virtual void Dispose(bool disposing) { 
      if (disposing) { 
       $end$$selected$ 
      } 
     }]]> 
      </Code> 
     </Snippet> 
    </CodeSnippet> 
</CodeSnippets> 
12

Zwykle wyrzucam zgadywanie dla klas pochodnych. Oto mój .snippet file:

#region IDisposable pattern 
/// <summary> 
/// Dispose of (clean up and deallocate) resources used by this class. 
/// </summary> 
/// <param name="fromUser"> 
/// True if called directly or indirectly from user code. 
/// False if called from the finalizer (i.e. from the class' destructor). 
/// </param> 
/// <remarks> 
/// When called from user code, it is safe to clean up both managed and unmanaged objects. 
/// When called from the finalizer, it is only safe to dispose of unmanaged objects. 
/// This method should expect to be called multiple times without causing an exception. 
/// </remarks> 
protected virtual void Dispose(bool fromUser) 
    { 
    if (fromUser) // Called from user code rather than the garbage collector 
     { 
     // Dispose of managed resources (only safe if called directly or indirectly from user code). 
     try 
      { 
     DisposeManagedResources(); 
      GC.SuppressFinalize(this); // No need for the Finalizer to do all this again. 
      } 
     catch (Exception ex) 
      { 
      //ToDo: Handle any exceptions, for example produce diagnostic trace output. 
      //Diagnostics.TraceError("Error when disposing."); 
      //Diagnostics.TraceError(ex); 
      } 
     finally 
      { 
      //ToDo: Call the base class' Dispose() method if one exists. 
      //base.Dispose(); 
      } 
     } 
    DisposeUnmanagedResources(); 
    } 
/// <summary> 
/// Called when it is time to dispose of all managed resources 
/// </summary> 
    protected virtual void DisposeManagedResources(){} 
/// <summary> 
/// Called when it is time to dispose of all unmanaged resources 
/// </summary> 
    protected virtual void DisposeUnmanagedResources(){} 
/// <summary> 
/// Dispose of all resources (both managed and unmanaged) used by this class. 
/// </summary> 
public void Dispose() 
    { 
    // Call our private Dispose method, indicating that the call originated from user code. 
    // Diagnostics.TraceInfo("Disposed by user code."); 
    this.Dispose(true); 
    } 
/// <summary> 
/// Destructor, called by the finalizer during garbage collection. 
/// There is no guarantee that this method will be called. For example, if <see cref="Dispose"/> has already 
/// been called in user code for this object, then finalization may have been suppressed. 
/// </summary> 
~$MyName$() 
    { 
    // Call our private Dispose method, indicating that the call originated from the finalizer. 
    // Diagnostics.TraceInfo("Finalizer is disposing $MyName$ instance"); 
    this.Dispose(false); 
    } 
#endregion 
+1

+1 Tak też zwykle robię. – SwDevMan81

+0

Nie wywołałeś GC.SuppressFinalize (this); –

+0

@Sergej masz rację; to tylko część całości. Zaktualizowałem swoją odpowiedź obecną zawartością mojego pliku .Strumka, który jest połączeniem różnych fragmentów tam z osobistymi dotknięciami. – Will

0

Jeden problem widzę z implementacji jest to, że nie ma potention dla klasy pochodnej nie wywołać metodę Dispose klasy bazowej. W takim przypadku funkcja GC.SuppressFinalize może nie zostać wywołana, gdy powinna, a na końcu również wywołanie Finalizera. Podoba mi się rozwiązanie Will, które zapewnia wywołanie GC.SuppressFinalize. Zalecany sposób przez MSDN ma podobne odczucia i zapewnia wywołanie GC.SuppressFinalize, jeśli obiekt został usunięty przez programistę.

2

Odpowiedź na to i większość innych pytań dotyczących projektowania interfejsu API można znaleźć w tej książce.

ramowe wytyczne projektowe: Konwencje, idiomy i wzorce dla wielorazowych .NET Biblioteki http://www.amazon.com/gp/product/0321545613?ie=UTF8&tag=bradabramsblo-20&link_code=wql&camp=212361&creative=380601

To jest dosłownie zestaw reguł pracownicy Microsoft używają budować .NET API. Zasady są bezpłatne (patrz poniżej), ale książka zawiera komentarz objaśniający zasady. To naprawdę musi mieć dla programistów .NET.

http://msdn.microsoft.com/en-us/library/b1yfkh5e.aspx

+0

Również wspomniano w książce Effective C# –

2

powiedziałbym to better:

public class DisposableClass : IDisposable { 
    void IDisposable.Dispose() { 
    CleanUpManagedResources(); 
    CleanUpNativeResources(); 
    GC.SuppressFinalize(this); 
    } 

    protected virtual void CleanUpManagedResources() { 
    // ... 
    } 
    protected virtual void CleanUpNativeResources() { 
    // ... 
    } 

    ~DisposableClass() { 
    CleanUpNativeResources(); 
    } 
} 
0

Przydatną cechą zalecanego IDisposable wzoru jest to, że umożliwia on spójny wzorzec dla typów pochodnych przebiegających typy, które implementują IDisposable, niezależny tego, czy typ podstawowy udostępnia publiczną metodę bez parametrów o nazwie Dispose. To naprawdę nie jest zły wzorzec, jeśli traktujemy parametr jako manekina, który jest użyty po to, by nadać chronionej metodzie inną sygnaturę niż bez parametrowej Dispose(); Największą słabością jest to, że ochrona przed zbędnym usuwaniem nie jest wykonywana w sposób bezpieczny dla wątków.

Niezbyt użyteczną cechą zalecanego wzorca IDisposable jest to, że zachęca ono do użycia finalizatorów/destruktorów w wielu przypadkach, gdy nie są one odpowiednie. Bardzo rzadko klasa, która wywodzi się z czegokolwiek innego niż System.Object ma finalizatora do czyszczenia (klasy mogą mieć finalizatory do celów rejestrowania niepowodzeń w prawidłowym składowaniu). Jeśli klasa zawiera odwołania do wielu zarządzanych obiektów, a także posiada niezarządzany zasób, niezarządzany zasób powinien zostać przeniesiony do własnej klasy opakowania, przekształcając go w zarządzany zasób. Ta klasa wrapper może pochodzić z czegoś takiego jak SafeHandle, lub może pochodzić z Object i zdefiniować finalizator/destruktor; Oba sposoby działania wyeliminują potrzebę stosowania finalizatora/destruktora w głównej klasie.

Powiązane problemy