Effective C# 原則18:實現標準的處理(Dispose)模式(譯)

來源:互聯網
上載者:User

Effective C# 原則18:實現標準的處理(Dispose)模式

我們已經討論過,處理一個佔用了非託管資來源物件是很重要的。現在是時候來討論如何寫代碼來管理這些類佔用的非記憶體資源了。一個標準的模式就是利用.Net架構提供的方法處理非記憶體資源。你的使用者也希望你遵守這個標準的模式。也就是通過實現IDisposable介面來釋放非託管的資源,當然是在使用者記得調用它的時候,但如果使用者忘記了,解構函式也會被動的執行。它是和記憶體回收行程一起工作的,確保在一些必要時候,你的對象只會受到因解構函式而造成的效能損失。這正是管理非託管資源的好方法,因此有必要徹底的弄明白它。

處在類繼承關係中頂層的基類應該實現IDisposable介面來釋放資源。這個類型也應該添加一個解構函式,做為最後的被動機制。這兩個方法都應該是用虛方法來釋放資源,這樣可以讓它的衍生類別重載這個函數來釋放它們自己的資源。衍生類別只有在它自己須要釋放資源時才重載這個函數,並且一定要記得調用基類的方法。

開始時,如果你的類使用了非記憶體資源,則一定得有一個解構函式。你不能指望你的使用者總是記得調用Dispose方法,否則當他們忘記時,你會丟失一些資源。這或許是因為他們沒有調用Dispose的錯誤,但你也有責任。唯一可以確保非記憶體資源可以恰當釋放的方法就是建立一個解構函式。所以,添加一個解構函式吧!

當記憶體回收行程運行時,它會直接從記憶體中移除不用析構的垃圾對象。而其它有解構函式的對象還保留在記憶體中。這些對象被添加到一個析構隊列中,記憶體回收行程會起動一個線程專門來析構這些對象。當析構線程完成它的工作後,這些垃圾對象就可以從記憶體中移除了。就是說,須要析構的對象比不須要析構的對象在記憶體中待的時間要長。但你沒得選擇。如果你是採用的這種被動模式,當你的類型佔用非託管資源時,你就必須寫一個解構函式。但目前你還不用擔心效能問題,下一步就保證你的使用者使用更加簡單,而且可以避免因為解構函式而造成的效能損失。

實現IDisposable介面是一個標準的模式來告訴使用者和進行時系統:你的對象佔有資源而且必須及時的釋放。IDisposable介面只有一個方法:

public interface IDisposable
{
  void Dispose( );
}

實現IDisposable.Dispose()方法有責任完成下面的任務:
1、感知所有的非託管資源。
2、感知所有的託管資源(包括卸載一些事件)。
3、設定一個安全的標記來標識對象已經被處理。如果在已經處理過的對象上調用任何方法時,你可以檢驗這個標記並且拋出一個ObjectDisposed的異常。
4、阻止析構。你要調用GC.SuppressFinalize(this)來完成最後的工作。

通過實現IDisposable介面,你寫成了兩件事:第一就是提供了一個機制來及時的釋放所有佔用的託管資源(譯註:這裡就是指託管資源,當實現了這個介面後,可以通過調用Dispose來立即釋放託管資源),另一個就是你提供了一個標準的模式讓使用者來釋放非託管資源。這是十分重要的,當你在你的類型上實現了IDisposable介面以後,使用者就可以避免析構時的損失。你的類就成了.Net社區中表現相當良好的成員。

但在你建立的機制中還是存在一些漏洞。如何讓一個衍生類別清理自己的資源,同時還可以讓基類很好的再做資源清理呢?(譯註:因為調用Dispose方法時,必須調用基類的Dispose,當然是在基類有這個方法時。但前面說過,我們只有一個標記來標識對象是否處理過,不管先調用那個,總得有一個方法不能處理這個標記,而這就存在隱患) 如果基類重載了解構函式,或者自己添加實現了IDisposable介面,而這些方法又都是必須調用基類的方法的;否則,基類無法恰當的釋放資源。同樣,析構和處理共用了一些相同的職責:幾乎可以肯定你是複製了析構方法和處理方法之間的代碼。正如你會在原則26中學到的,重載介面的方法根本沒有如你所期望的那樣工作。Dispose標準模式中的第三個方法,通過一個受保護的輔助性虛函數,製造出它們的常規任務並且掛接到衍生類別來釋放資源。基類包含介面的核心代碼, 衍生類別提供的Dispose()虛函數或者解構函式來負責清理資源:

protected virtual void Dispose( bool isDisposing );

重載的方法同時完成析構和處理必須提供的任務,又因為它是虛函數,它為所有的衍生類別提供函數進入點。衍生類別可以重載這個函數,提供恰當的實現來釋放它自己的資源,並且調用基類的函數。當
isDisposing為true時你可能同時清理託管資源和非託管資源,當isDisposing為false時你只能清理非託管資源。兩種情況下,都可以調用基類的Dispose(bool)方法讓它去清理它自己的資源。

當你實現這樣的模式時,這裡有一個簡單的例子。MyResourceHog 類展示了IDisposable的實現,一個解構函式,並且建立了一個虛的Dispose方法:
public class MyResourceHog : IDisposable
{
  // Flag for already disposed
  private bool _alreadyDisposed = false;

  // finalizer:
  // Call the virtual Dispose method.
  ~MyResourceHog()
  {
    Dispose( false );
  }

  // Implementation of IDisposable.
  // Call the virtual Dispose method.
  // Suppress Finalization.
  public void Dispose()
  {
    Dispose( true );
    GC.SuppressFinalize( true );
  }

  // Virtual Dispose method
  protected virtual void Dispose( bool isDisposing )
  {
    // Don't dispose more than once.
    if ( _alreadyDisposed )
      return;
    if ( isDisposing )
    {
      // TODO: free managed resources here.
    }
    // TODO: free unmanaged resources here.
    // Set disposed flag:
    _alreadyDisposed = true;
  }
}

如果衍生類別有另外的清理任務,就讓它實現Dispose方法:

public class DerivedResourceHog : MyResourceHog
{
  // Have its own disposed flag.
  private bool _disposed = false;

  protected override void Dispose( bool isDisposing )
  {
    // Don't dispose more than once.
    if ( _disposed )
      return;
    if ( isDisposing )
    {
      // TODO: free managed resources here.
    }
    // TODO: free unmanaged resources here.

    // Let the base class free its resources.
    // Base class is responsible for calling
    // GC.SuppressFinalize( )
    base.Dispose( isDisposing );

    // Set derived class disposed flag:
    _disposed = true;
  }
}

注和意,衍生類別和基類都有一個處理狀態的標記,這完全是被動的。重製的標記掩蓋了在處理時任何可能發生的錯誤,而且是單一的類型處理,而不是處理構成這個對象的所有類型。(譯註:就是基類與子類各自標記一個,互不影響。)

你應該被動的寫處理方法和解構函式,處理對象可能以任何順序發生,你可能會遇到這種情況:你的類中某個成員在你調用Dispose方法以前已經被處理過了。你沒有看到這種情況是因為Dispose()方法是可以多次調用的。如果在一個已經被處理過的對象上調用該方法,就什麼也不發生。解構函式也有同樣的規則。任何對象的引用存在於記憶體中時,你不用檢測null引用。然而,你引用的對象可能已經處理掉了,或者它已經析構了。

這就引入用了一個非常重要的忠告:對於任何與處理和資源清理相關的方法,你必須只釋放資源! 不要在處理過程中添加其它任何的任務。你在處理和清理中添加其它任務時,可能會在對象的生存期中遇到一些嚴重而繁雜的問題。對象在你建立它時出生,在記憶體回收行程認領它時死亡。你可以認為當你的程式不能再訪問它們時,它們是睡眠的。你無法訪問對象,無法調用對象的方法。種種跡象表明,它們就像是死的。但對象在宣布死亡前,解構函式還有最後一氣。解構函式什麼也不應該做,就是清理非託管資源。如果解構函式通過某些方法讓對象又變得可訪問,那麼它就複活了。(譯註:解構函式不是使用者調用的,也不由.Net系統調用,而是在由GC產生的額外線程上啟動並執行) 它又活了,但這並不好。即使是它是從睡眼中喚醒的。這裡有一個明顯的例子:

public class BadClass
{
  // Store a reference to a global object:
  private readonly ArrayList _finalizedList;
  private string _msg;

  public BadClass( ArrayList badList, string msg )
  {
    // cache the reference:
    _finalizedList = badList;
    _msg = (string)msg.Clone();
  }

  ~BadClass()
  {
    // Add this object to the list.
    // This object is reachable, no
    // longer garbage. It's Back!
    _finalizedList.Add( this );
  }
}

當一個BadClass對象的解構函式執行時,它把自己的一個引用添加到了全域的鏈表中。這使得它自己又是可達的,它就又活了。前面向你介紹的這個方法會遇到一些讓人畏縮的難題。對象已經被析構了,所以記憶體回收行程從此相信再也不用調用它的解構函式了。如果你實際要析構一個可達對象,這將不會成功。其次,你的一些資源可能不再有用。GC不再從記憶體上移除那些只被析構隊列引用的對象,但它們可能已經析構了。如果是這樣,它們很可能已經不能使用了。(譯註:也就是說利用上面的那個方法讓對象複活後,很有可能對象是停用。)儘管BadClass所擁有的成員還在記憶體裡,它們像是可以被析構或者處理,但C#語言沒有一個方法可以讓你控制析構的次序,你不能讓這樣的結構可靠的運行。不要嘗試。

我還沒有看到這樣的代碼:用這樣明顯的方式來複活一個對象,除非是學術上的練習。但我看過這樣的代碼,解構函式試圖完成一些實質的工作,最後還通過解構函式的調用把引用放到對象中,從而把自己複活。解構函式裡面的代碼看上去是精心設計的,另外還有處理函數裡的。再檢查一遍,這些代碼是做了其它事情,而不是釋放資源!這些行為會為你的應用程式在後期的運行中產生很多BUG。刪除這些方法,確保解構函式和Dispose()方法除了清理資源外,什麼也不做。

在託管環境裡,你不用為每一個建立的類寫解構函式;只有須要釋放一些使用的非託管資源時才添加,或者你的類所包含的成員有實現了IDisposable介面的時候也要添加。即使如此,你也只用實現IDisposable介面完成所有的功能就行了,不用解構函式。否則,你會限制你的衍生類別實現實現標準的Dispose習慣。 遵守這個我所講敘的標準的Dispose習慣。這會讓你的程式生活變得輕鬆,也為你的使用者,也為那些從你的類建立衍生類別的人。

=======================
   

Item 18: Implement the Standard Dispose Pattern
We've discussed the importance of disposing of objects that hold unmanaged resources. Now it's time to cover how to write your own resource-management code when you create types that contain resources other than memory. A standard pattern is used throughout the .NET Framework for disposing of nonmemory resources. The users of your type will expect you to follow this standard pattern. The standard dispose idiom frees your unmanaged resources using the IDisposable interface when clients remember, and it uses the finalizer defensively when clients forget. It works with the Garbage Collector to ensure that your objects pay the performance penalty associated with finalizers only when necessary. This is the right way to handle unmanaged resources, so it pays to understand it thoroughly.

The root base class in the class hierarchy should implement the IDisposable interface to free resources. This type should also add a finalizer as a defensive mechanism. Both of these routines delegate the work of freeing resources to a virtual method that derived classes can override for their own resource-management needs. The derived classes need override the virtual method only when the derived class must free its own resources and it must remember to call the base class version of the function.

To begin, your class must have a finalizer if it uses nonmemory resources. You should not rely on clients to always call the Dispose() method. You'll leak resources when they forget. It's their fault for not calling Dispose, but you'll get the blame. The only way you can guarantee that nonmemory resources get freed properly is to create a finalizer. So create one.

When the Garbage Collector runs, it immediately removes from memory any garbage objects that do not have finalizers. All objects that have finalizers remain in memory. These objects are added to a finalization queue, and the Garbage Collector spawns a new thread to run the finalizers on those objects. After the finalizer thread has finished its work, the garbage objects can be removed from memory. Objects that need finalization stay in memory for far longer than objects without a finalizer. But you have no choice. If you're going to be defensive, you must write a finalizer when your type holds unmanaged resources. But don't worry about performance just yet. The next steps ensure that it's easier for clients to avoid the performance penalty associated with finalization.

Implementing IDisposable is the standard way to inform users and the runtime system that your objects hold resources that must be released in a timely manner. The IDisposable interface contains just one method:

public interface IDisposable
{
  void Dispose( );
}

 

The implementation of your IDisposable.Dispose() method is responsible for four tasks:

Freeing all unmanaged resources.

Freeing all managed resources (this includes unhooking events).

Setting a state flag to indicate that the object has been disposed. You need to check this state and throw ObjectDisposed exceptions in your public methods, if any get called after disposing of an object.

Suppressing finalization. You call GC.SuppressFinalize(this) to accomplish this task.

You accomplish two things by implementing IDisposable: You provide the mechanism for clients to release all managed resources that you hold in a timely fashion, and you give clients a standard way to release all unmanaged resources. That's quite an improvement. After you've implemented IDisposable in your type, clients can avoid the finalization cost. Your class is a reasonably well-behaved member of the .NET community.

But there are still holes in the mechanism you've created. How does a derived class clean up its resources and still let a base class clean up as well? If derived classes override finalize or add their own implementation of IDisposable, those methods must call the base class; otherwise, the base class doesn't clean up properly. Also, finalize and Dispose share some of the same responsibilities: You have almost certainly duplicated code between the finalize method and the Dispose method. As you'll learn in Item 26, overriding interface functions does not work the way you'd expect. The third method in the standard Dispose pattern, a protected virtual helper function, factors out these common tasks and adds a hook for derived classes to free resources they allocate. The base class contains the code for the core interface. The virtual function provides the hook for derived classes to clean up resources in response to Dispose() or finalization:

protected virtual void Dispose( bool isDisposing );

 

This overloaded method does the work necessary to support both finalize and Dispose, and because it is virtual, it provides an entry point for all derived classes. Derived classes can override this method, provide the proper implementation to clean up their resources, and call the base class version. You clean up managed and unmanaged resources when isDisposing is TRue; clean up only unmanaged resources when isDisposing is false. In both cases, call the base class's Dispose(bool) method to let it clean up its own resources.

Here is a short sample that shows the framework of code you supply when you implement this pattern. The MyResourceHog class shows the code to implement IDisposable, a finalizer, and create the virtual Dispose method:

public class MyResourceHog : IDisposable
{
  // Flag for already disposed
  private bool _alreadyDisposed = false;

  // finalizer:
  // Call the virtual Dispose method.
  ~MyResourceHog()
  {
    Dispose( false );
  }

  // Implementation of IDisposable.
  // Call the virtual Dispose method.
  // Suppress Finalization.
  public void Dispose()
  {
    Dispose( true );
    GC.SuppressFinalize( true );
  }

  // Virtual Dispose method
  protected virtual void Dispose( bool isDisposing )
  {
    // Don't dispose more than once.
    if ( _alreadyDisposed )
      return;
    if ( isDisposing )
    {
      // TODO: free managed resources here.
    }
    // TODO: free unmanaged resources here.
    // Set disposed flag:
    _alreadyDisposed = true;
  }
}

 

If a derived class needs to perform additional cleanup, it implements the protected Dispose method:

public class DerivedResourceHog : MyResourceHog
{
  // Have its own disposed flag.
  private bool _disposed = false;

  protected override void Dispose( bool isDisposing )
  {
    // Don't dispose more than once.
    if ( _disposed )
      return;
    if ( isDisposing )
    {
      // TODO: free managed resources here.
    }
    // TODO: free unmanaged resources here.

    // Let the base class free its resources.
    // Base class is responsible for calling
    // GC.SuppressFinalize( )
    base.Dispose( isDisposing );

    // Set derived class disposed flag:
    _disposed = true;
  }
}

 

Notice that both the base class and the derived class contain a flag for the disposed state of the object. This is purely defensive. Duplicating the flag encapsulates any possible mistakes made while disposing of an object to only the one type, not all types that make up an object.

You need to write Dispose and finalize defensively. Disposing of objects can happen in any order. You will encounter cases in which one of the member objects in your type is already disposed of before your Dispose() method gets called. You should not view that as a problem because the Dispose() method can be called multiple times. If it's called on an object that has already been disposed of, it does nothing. Finalizers have similar rules. Any object that you reference is still in memory, so you don't need to check null references. However, any object that you reference might be disposed of. It might also have already been finalized.

This brings me to the most important recommendation for any method associated with disposal or cleanup: You should be releasing resources only. Do not perform any other processing during a dispose method. You can introduce serious complications to object lifetimes by performing other processing in your Dispose or finalize methods. Objects are born when you construct them, and they die when the Garbage Collector reclaims them. You can consider them comatose when your program can no longer access them. If you can't reach an object, you can't call any of its methods. For all intents and purposes, it is dead. But objects that have finalizers get to breathe a last breath before they are declared dead. Finalizers should do nothing but clean up unmanaged resources. If a finalizer somehow makes an object reachable again, it has been resurrected. It's alive and not well, even though it has awoken from a comatose state. Here's an obvious example:

public class BadClass
{
  // Store a reference to a global object:
  private readonly ArrayList _finalizedList;
  private string _msg;

  public BadClass( ArrayList badList, string msg )
  {
    // cache the reference:
    _finalizedList = badList;
    _msg = (string)msg.Clone();
  }

  ~BadClass()
  {
    // Add this object to the list.
    // This object is reachable, no
    // longer garbage. It's Back!
    _finalizedList.Add( this );
  }
}

 

When a BadClass object executes its finalizer, it puts a reference to itself on a global list. It has just made itself reachable. It's alive again! The number of problems you've just introduced will make anyone cringe. The object has been finalized, so the Garbage Collector now believes there is no need to call its finalizer again. If you actually need to finalize a resurrected object, it won't happen. Second, some of your resources might not be available. The GC will not remove from memory any objects that are reachable only by objects in the finalizer queue, but it might have already finalized them. If so, they are almost certainly no longer usable. Although the members that BadClass owns are still in memory, they will have likely been disposed of or finalized. There is no way in the language that you can control the order of finalization. You cannot make this kind of construct work reliably. Don't try.

I've never seen code that has resurrected objects in such an obvious fashion, except as an academic exercise. But I have seen code in which the finalizer attempts to do some real work and ends up bringing itself back to life when some function that the finalizer calls saves a reference to the object. The moral is to look very carefully at any code in a finalizer and, by extension, both Dispose methods. If that code is doing anything other than releasing resources, look again. Those actions likely will cause bugs in your program in the future. Remove those actions, and make sure that finalizers and Dispose() methods release resources and do nothing else.

In a managed environment, you do not need to write a finalizer for every type you create; you do it only for types that store unmanaged types or when your type contains members that implement IDisposable. Even if you need only the Disposable interface, not a finalizer, implement the entire pattern. Otherwise, you limit your derived classes by complicating their implementation of the standard Dispose idiom. Follow the standard Dispose idiom I've described. That will make life easier for you, for the users of your class, and for those who create derived classes from your types.

 
   

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.