[DotNetBook] IDisposableの実装:適切な使用

この記事から、一連の記事全体を公開し始め、その結果は.NET CLRおよび.NET全般の作業に関する本になります。 IDisposableテーマは、オーバークロックペンサンプラーとして選択されました。 本全体がGitHub: DotNetBookで利用可能になります。 だから問題とプルリクエストは大歓迎です:)



廃棄(使い捨てデザインの原則)





さて、おそらく、.NETプラットフォームで開発するほとんどすべてのプログラマーは、このパターンほど簡単なものはないと言うでしょう。 プラットフォームで使用されている最も有名なテンプレートについて知られていること。 ただし、最も単純で最も有名な問題領域であっても、常に2番目の底があり、その背後には見たことのないいくつかの隠れたポケットがあります。 ただし、トピックを初めて見ている人と他のすべての人(あなたがそれぞれの基本を覚えているように(これらのパラグラフをスキップしないでください(私は従います!))の両方)-最初から最後まですべてを説明します。



IDisposable





IDisposableとは何かを尋ねると、おそらくそれが何であるかを答えるでしょう。



public interface IDisposable { void Dispose(); }
      
      







インターフェースは何のために作成されましたか? 結局のところ、すべてのメモリをクリーンアップするスマートガベージコレクターがあり、メモリをクリーンアップする方法さえ考えないようにすると、それをクリーンアップする理由が完全には明らかになりません。 ただし、微妙な違いがあります。



ご注意



Habréで公開されている章は更新されておらず、おそらくすでに古いものです。 そのため、最新のテキストについては元に戻してください。









管理されていないリソースを解放するためにIDisposable



が作成されているという誤解があります。 そして、これは真実の一部にすぎません。 これがそうでないことをすぐに理解するには、アンマネージリソースの例を思い出すだけで十分です。 File



クラスは管理されていませんか? いや たぶんDbContext



? 繰り返しますが、いいえ。 アンマネージリソースは、.NETタイプシステムの一部ではないものです。 プラットフォームによって作成されたものではなく、その範囲を超えているもの。 簡単な例は、オペレーティングシステムで開いているファイルハンドルです。 記述子は、オペレーティングシステムによって開かれたファイルを一意に識別する番号です。 あなたではなく、オペレーティングシステムによって。 すなわち すべての制御構造(ファイルシステム上のファイルの座標、フラグメンテーションおよびその他のサービス情報の場合のそのフラグメント、磁気HDDの場合のシリンダー、ヘッド、セクター番号など)は、.NETプラットフォーム内ではなく、OS内にあります。 そして、.NETプラットフォームに入る唯一のアンマネージリソースは、IntPtr番号です。 この番号はFileSafeHandleをラップし、FileSafeHandleはFileクラスでラップします。 すなわち Fileクラス自体はアンマネージリソースではありませんが、追加のレイヤー(オープンファイル記述子IntPtr)を介してアンマネージリソース自体を蓄積します。 そのようなファイルからの読み取りはどうですか? 一連のWinAPIまたはLinuxメソッドを通じて。



アンマネージリソースの2番目の例は、マルチスレッドおよびマルチプロセスプログラムの同期プリミティブです。 ミューテックス、セマフォなど。 または、p / invokeを介して渡されるデータ配列。



いいね 管理されていないリソースが整理されました。 これらの場合にIDisposableを使用する理由 次に、.NET Frameworkには、それが存在しない場合に何が起こるかについての考えがありません。 OS関数を使用してファイルを開くと、.NETはそれについて何も知りません。 独自のニーズに合わせてメモリを割り当てた場合(たとえば、VirtualAllocを使用)、. NETはそれについて何も認識しません。 そして、彼がこれについて何も知らなければ、彼はVirtualAlloc呼び出しによって占有されたメモリを解放しません。 または、OS API呼び出しを介して直接開かれたファイルを閉じません。 これの結果は完全に異なり、予測不可能です。 メモリを割り当てすぎて解放しない場合(たとえば、古いメモリからポインタをリセットするだけ)、OSツールで開いたが閉じていなかったファイルボール上のファイルを長時間ブロックする場合、OutOfMemoryを取得できます。 。 ファイルボールを使用した例は、アプリケーションが閉じられた後でもロックが保持されるため、特に優れています。ファイルのオープン性は、ファイルが置かれている側によって規制されます。 また、リモート側では、自分でファイルを閉じなかった場合、ファイルを閉じる信号を受信しません。



これらのすべての場合において、型システムとプログラマーの間で普遍的で認識可能な「相互作用のプロトコル」が必要であり、強制閉鎖が必要な型を一意に識別します。 この_protocol_はIDisposableインターフェイスです。 そして、次のように聞こえます:型にIDisposableインターフェイスの実装が含まれている場合、そのインスタンスの操作を終了した後、 Dispose()



呼び出す必要があります。



そしてまさにこの理由のために、それを呼び出す2つの標準的な方法があります。 結局のところ、原則として、1つのメソッドのフレームワーク内で、またはエッセンスのインスタンスの存続期間内にエンティティをすばやく操作するために、エンティティのインスタンスを作成します。



最初のオプションは、 using(...){ ... }



てインスタンスをラップするときです。 すなわち usingブロックの最後で、オブジェクトを破棄する必要があることを明示的に示します。 すなわち Dispose()



と呼ばれる必要があります。 2番目のオプションは、オブジェクトの存続期間の終了時に破棄することです。これには、解放する必要があるオブジェクトへのリンクが含まれます。 しかし、.NETでは、ファイナライズメソッドに加えて、オブジェクトの自動破棄を示唆するものは何もありません。 そうだね しかし、ファイナライズは、呼び出されたときに認識されないという理由で、私たちにはまったく適していません。 そして、必要なときに解放する必要があります。たとえば、開いているファイルが不要になった直後です。 そのため、IDisposableを自分で実装する必要があり、Disposeメソッドでも、所有しているすべてのユーザーに対してDisposeを呼び出して、それらを解放する必要があります。 したがって、我々はプロトコルに準拠しており、これは非常に重要です。 結局、誰かが特定のプロトコルに準拠し始めた場合、プロセスのすべての参加者はそれを遵守しなければなりません。そうでなければ問題が発生します。



IDisposableの実装バリエーション





IDisposableの実装から単純なものから複雑なものへと進みましょう。



頭に浮かぶことができる最初で最も簡単な実装は、IDisposableを単に取得して実装することです。



 public class ResourceHolder : IDisposable { DisposableResource _anotherResource = new DisposableResource(); public void Dispose() { _anotherResource.Dispose(); } }
      
      







すなわち 最初に、解放する必要があり、Dispose()メソッドで解放されるリソースのインスタンスを作成します。

ここに存在せず、実装の一貫性を失わせる唯一のものは、 Dispose()



メソッドによる破棄後にクラスインスタンスをさらに処理する可能性です。



 public class ResourceHolder : IDisposable { private DisposableResource _anotherResource = new DisposableResource(); private bool _disposed; public void Dispose() { if(_disposed) return; _anotherResource.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } }
      
      







CheckDisposed()



の呼び出しは、クラスのすべてのパブリックメソッドの最初の式で呼び出す必要があります。 ただし、管理リソース( DisposableResource



)を破棄するためにResourceHolder



クラスの結果の構造が正常に見える場合、管理されていないリソースをカプセル化する場合はそうではありません。



管理されていないリソースオプションを考えてみましょう。



 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { CloseHandle(_handle); } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError=true)] private static extern bool CloseHandle(IntPtr hObject); }
      
      







最後の2つの例の動作の違いは何ですか? 最初のバージョンでは、管理対象リソースと別の管理対象リソースとの相互作用について説明します。 これは、プログラムが正常に動作する場合、リソースはいずれの場合でも解放されることを意味します。 結局のところ、 DisposableResource



は管理されています。つまり、.NET CLRはそれについてよく知っており、不正な動作の場合は、メモリを解放します。 DisposableResource



型がカプセル化することを意図的に仮定していないことに注意してください。 任意のロジックと構造があります。 管理対象リソースと管理対象外リソースの両方を含めることができます。 これは私たちを悩ませるべきではありません 。 ただし、他の人のライブラリを逆コンパイルし、その人が使用するタイプ(マネージリソースまたはアンマネージリソース)を確認するよう求められることはありません。 そして、タイプがアンマネージリソースを使用する場合これを知る以外にありません。 これはFileWrapper



クラスで行います。 この場合、どうなりますか?



管理されていないリソースを使用する場合、すべてが正常でDisposeメソッドが呼び出されたとき(その後はすべて正常です:))、何かが発生してDisposeメソッドが機能しなかったときの2つのオプションがあることがわかります。 すぐに予約してください。これが起こらない理由:





このような場合はすべて、管理されていないリソースが宙に浮いた状態になります。 結局のところ、ガベージコレクターは、収集する必要があるとは考えていません。 彼が行う最大のこと-次のパスで、彼はFileWrapper



タイプのオブジェクトを含むオブジェクトのグラフで最後のリンクが失われ、リンクがあるオブジェクトによってメモリが摩擦されることを理解します。



これから身を守るには? これらの場合、オブジェクトのファイナライザーを実装する必要があります。 ファイナライザに誤ってそのような名前が付けられることはありません。 これはデストラクタではありません。最初はC#のファイナライザとC ++のデストラクタの宣言が類似しているためと思われます。 デストラクタとは異なり、ファイナライザは*保証*と呼ばれますが、デストラクタは呼び出されない場合があります( Dispose()



ように)。 ファイナライザは、ガベージコレクションの起動時に呼び出され(これまでの知識で十分ですが、実際にはすべてが多少複雑です)、 何か問題が発生した場合にキャプチャされたリソースのリリースを保証することを目的としています。 また、アンマネージリソースのリリースの場合、ファイナライザーを実装する必要があります。 また、GCの開始時にファイナライザーが呼び出されるという事実のために、私は繰り返しますが、一般的な場合、これがいつ起こるかわかりません。



コードを拡張しましょう:



 public class FileWrapper : IDisposable { IntPtr _handle; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { InternalDispose(); GC.SuppressFinalize(this); } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
      
      







ファイナライズプロセスに関する知識を使用して例を強化し、それにより、何かがうまくいかず、Dispose()が呼び出されない場合にリソースに関する情報が失われないようにアプリケーションを保護しました。 さらに、GC.SuppressFinalizeを呼び出して、Dispose()が呼び出された場合に型インスタンスのファイナライズを無効にしました。 同じリソースを2回解放する必要はありませんか? これは、別の理由でも行う価値があります。コードのランダムセクションを高速化することで、ファイナライズキューから負担を取り除きます。これと並行して、ファイナライズがランダムに行われます。



次に、例を強化しましょう。



 public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
      
      







これで、アンマネージリソースをカプセル化する型の実装例は完全に見えます。 残念ながら、Repeated Dispose()



はプラットフォームの事実上の標準であり、呼び出すことができます。 多くの場合、呼び出しコードのトラブルを避けるために、 Dispose()



繰り返し呼び出しを許可しますが、これは正しくありません。 ただし、MSドキュメントに目を向けるライブラリのユーザーは、これを考慮せず、 Dispose()



複数の呼び出しを許可する場合があります。 いずれにしても、他のパブリックメソッドを呼び出すと、オブジェクトの整合性が失われます。 オブジェクトを破棄すると、そのオブジェクトを使用できなくなります。 これは、各パブリックメソッドの先頭にCheckDisposed



呼び出しを挿入する必要があることを意味します。



ただし、このコードには非常に深刻な問題があり、意図したとおりに動作しません。 ガベージコレクションプロセスがどのように機能するかを思い出すと、詳細が1つわかります。 ガベージコレクションの場合、GCはまず Objectから直接継承されたすべてをファイナライズし、その後、 CriticalFinalizerObjectを実装するオブジェクトに対して取得されます。 しかし、設計した両方のクラスがObject:を継承していることがわかりました。これは問題です。 「ラストマイル」に進む順序はわかりません。 ただし、より高いレベルのオブジェクトは、ファイナライザーにアンマネージリソースを格納するオブジェクトを操作しようとする場合があります(これは既に悪い考えのように聞こえますが)。 ここで、ファイナライズの順序は非常に役立ちます。 そして、設定するには、CriticalFinalizerObjectからアンマネージリソースをカプセル化する型を継承する必要があります。



2番目の理由はより深いルーツを持っています。 メモリをあまり気にしないアプリケーションの作成を許可したと想像してください。 キャッシュなどのトリックなしで大量に割り当てます。 そのようなアプリケーションはOutOfMemoryExceptionで失敗します。 また、アプリケーションがこの例外でクラッシュした場合、コードの実行には特別な条件があります。何も割り当てようとすべきではありません。 結局、前の例外がキャッチされたとしても、これは例外の繰り返しにつながります。 これは、オブジェクトの新しいインスタンスを作成してはならないという意味ではありません。 通常のメソッド呼び出しは、この例外につながる可能性があります。 たとえば、ファイナライズメソッドを呼び出します。 メソッドは、初めて呼び出されたときにコンパイルされることを思い出させてください。 そして、これは通常の動作です。 この問題から身を守るには? 簡単です。 CriticalFinalizerObjectからオブジェクトを継承する場合、この型のすべてのメソッドは、型がメモリにロードされるとすぐにコンパイルされます。 それだけでなく、 [PrePrepareMethod]



属性でメソッドをマークすると、それらもプリコンパイルされ、十分なリソースがない場合は呼び出しの観点から安全になります。



なぜこれがそんなに重要なのですか? 別の世界に向けて出発する人々になぜそんなに努力を費やすのですか? そして問題は、管理されていないリソースが非常に長い時間システムにハングアップする可能性があることです。 アプリケーションが完了した後でも。 コンピューターを再起動した後でも(ユーザーがアプリケーションでファイルボールを含むファイルを開いた場合、そのファイルはリモートホストによってブロックされ、タイムアウトによって、またはファイルを閉じてリソースを解放すると、解放されます。再起動後でも、リモートホストがリリースするまで十分に長い時間待つ必要があります)。 さらに、ファイナライザで例外をスローすることを許可しないでください。これにより、CLRの死が加速し、アプリケーションからの最終スローが発生します。ファイナライザへの呼び出しはtry .. catch



なりません。 すなわち リソースを解放するとき、リソースをまだ解放できることを確認する必要があります。 最後になりますが、CLRが緊急ドメインのアンロードを実行する場合、Objectから直接継承するものとは異なり、CriticalFinalizerObjectから派生した型のファイナライザーも呼び出されます。



SafeHandle / CriticalHandle / SafeBuffer /デリバティブ





私は今あなたのためにPandoraの箱を開けるだろうと感じています。 SafeHandle、CriticalHandle、およびそれらの派生物という特別なタイプについて話しましょう。 最後に、アンマネージリソースへのアクセスを提供するタイプのテンプレートを完成させます。 しかしその前に、管理されていない世界から「通常」私たちに届くすべてのものをリストしてみましょう。







SafeHandleは、CriticalFinalizerObjectを継承する特別な.NET CLRクラスであり、オペレーティングシステムのハンドルをできるだけ安全かつ便利にラップするように設計されています。



 [SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { protected IntPtr handle; // ,    private int _state; //  (,  ) private bool _ownsHandle; //    handle.   ,     handle       private bool _fullyInitialized; //   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { } //     Dispose(false) [SecuritySafeCritical] ~SafeHandle() { Dispose(false); } //  hanlde    ,     p/invoke Marshal -  [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected void SetHandle(IntPtr handle) { this.handle = handle; } //    ,   IntPtr     .  //   ,    ,       //   .  ,      : // -        SetHandleasInvalid, DangerousGetHandle //       . // -        .     // ,       .       // IntPtr   ,             //      IntPtr [ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public IntPtr DangerousGetHandle() { return handle; } //   (    ) public bool IsClosed { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get { return (_state & 1) == 1; } } //      .    ,  . public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get; } //     Close() [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Close() { Dispose(true); } //     Dispose() [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Dispose() { Dispose(true); } [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Dispose(bool disposing) { // ... } //       ,  ,  handle    . //     ,    [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void SetHandleAsInvalid(); //   ,  ,     // .       , ..   //    ,      . //   -     . //     = false,    // SafeHandleCriticalFailure,     SafeHandleCriticalFailure // Managed Debugger Assistant    . [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected abstract bool ReleaseHandle(); //    .      [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void DangerousAddRef(ref bool success); public extern void DangerousRelease(); }
      
      







, SafeHandle, , .NET : . .., , SafeHandle , .. . , CLR. すなわち unsafe . : , SafeHandle, unsafe , , — . , unsafe , , ( , , ) , SafeHandle. : SafeHandle , . . : , .



CriticalFinalizerObject



, . SafeHandle-based SafeHandle-based , , ReleaseHandle — . , , . , .



, SafeHandlers:



 public class FileWrapper : IDisposable { SafeFileHandle _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; _handle.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern SafeFileHandle CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); /// other methods }
      
      







? , DllImport SafeHandle-based , Marshal , 1, SafeFileHandle CreateFile. , ReadFile WriteFile (.. , — , handle ). , , . . , finalizer , . .



マルチスレッド



. IDisposable , Disposable , : . , — . — , . : . , . ,



 public class FileWrapper : IDisposable { IntPtr _handle; bool _disposed; object _disposingSync = new object(); public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Seek(int position) { lock(_disposingSync) { CheckDisposed(); // Seek API call } } public void Dispose() { lock(_disposingSync) { if(_disposed) return; _disposed = true; } InternalDispose(); GC.SuppressFinalize(this); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { lock(_disposingSync) { if(_disposed) { throw new ObjectDisposedException(); } } } private void InternalDispose() { CloseHandle(_handle); } ~FileWrapper() { InternalDispose(); } /// other methods }
      
      







_disposed



Dispose()



, , . . ? ? Dispose, ObjectDisposedException



. : Dispose() . すなわち , FileWrapper



. , .



Disposable Design Principle



IDisposable



.NET ? , ? :



 public class Disposable : IDisposable { bool _disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if(disposing) { //    } //    } protected void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } ~Disposable() { Dispose(false); } }
      
      







? . , , : - . , . . , . . *Disposable Design Principle*. , :



Disposing :





: . -.



まとめ



長所



, . :

  1. : ,
  2. ,
  3. , (, - )




短所



, :

  1. , , , , : , . , , . , , IDE ( , Dis… , ). Dispose , . , . :
     IEnumerator<T>
          
          



    IDisposable



    ?
  2. , , IDisposable : IDisposable. «» , . , . , * -*, . Dispose() — . * *. — , ;
  3. , Dispose() . **. . , CheckDisposed() . , : « !»;
  4. , IDisposable



    *explicit* . , IDisposable , . , . Dispose(), ;
  5. . . GC . , , DisposableObject, , virtual void Dispose()



    , , ;
  6. Dispose()



    , tor



    . disposing .
  7. , , , — ** . , Dispose() . . , Lifetime.






  1. IDisposable . , , ;
  2. IDisposable . , , Garbage Collector;
  3. IDisposable Dispose() . : , IDisposable ;
  4. . すなわち , : , SafeHandle / CriticalHandle / CriticalFinalizerObject. Dispose(): .
  5. , . , Inversion of Control Lifetime



    , .













All Articles