.NETでは、不要と判断されたオブジェクトはガベージコレクタによって自動的に収集されます。 new等の構文によってオブジェクトを作成できる一方で、delete object;のような構文等によって明示的にオブジェクトを破棄・解放することはできません。

.NETでは、明示的にオブジェクトを破棄するためのインターフェイスとしてIDisposableインターフェイスが用意されています。 このインターフェイスは、オブジェクトが不要になった時点で速やかに解放すべきリソースを保持していることを表し、またそのリソースの破棄を明示的に行う手段(Disposeメソッド)を提供します。 一例として、ファイルの読み書きを行う(=ファイルハンドルを保持して操作する)ためのクラスFileStreamは、IDisposableインターフェイスを実装しています。 (§.IDisposableインターフェイス)

C#/VBを含む.NETの言語側においては、例外が発生した状況でもオブジェクトが保持しているリソースを確実に破棄するための構文としてusingステートメントが用意されています。 一般に、IDisposableインターフェイスを持つオブジェクトを扱う場合はusingステートメント内で使用すること、またクラス内で使用する場合はそのクラス自体にIDisposableインターフェイスを実装してDisposeメソッドで破棄できるようにすることが推奨されます。 (§.usingステートメント§.ファイナライザとIDisposableのデザインパターン (disposeパターン))

オブジェクトが保持しているリソースの破棄を行う機構としては、上記のIDisposableインターフェイスとusingステートメントを用いることが第一とされますが、仮にIDisposableによる破棄が行われなかった場合の保証として、ファイナライザ(デストラクタ)を実装することもできます。 (§.ファイナライザ)

ただし、.NET Core/.NET 5以降と.NET Frameworkではアプリケーションドメインのシャットダウン時におけるファイナライザ呼び出しの動作が異なる点に注意する必要があります。 (§.シャットダウン時におけるファイナライザ呼び出し)

.NETにおけるオブジェクトの破棄と保持するリソースの破棄

ここでは.NETにおけるオブジェクトの破棄、オブジェクトが保持するリソースの破棄についてと、それを実装するための手段・機構について解説します。

オブジェクトの破棄

.NETでは、オブジェクト自体の破棄ガベージコレクタ(garbage collector)によって行われます。 newによって作成したあと、どこからも参照されなくなった・使用されなくなったオブジェクト(garbage)は定期的に収集(collect)されることにより、確保されていた領域が解放されます。 オブジェクトはnewによって任意のタイミングで作成できる一方で、delete object;のような構文を用いて明示的にオブジェクトを破棄・解放することはできません。

VB6以前ではNothingを代入することでオブジェクトを破棄していましたが、.NETでは(C#/VBともに)null/Nothingを代入することはオブジェクトの破棄を明示的に指示するものにはなりません。 (オブジェクトの参照が破棄されることにより、ガベージコレクタによって収集されやすくなる可能性はあるが、不要になった時点でnull/Nothingを代入することは明確に推奨されてはいない)

オブジェクトの作成と破棄
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    var l = new List<string>(); // オブジェクト(List)を作成する

    l.Add("Hello, world!");

    delete l; // deleteのような、オブジェクトを明示的に破棄する構文はない

    l = null; // nullを代入することにより、確保したオブジェクトへの*参照を破棄する*ことはできる
              // ただし、オブジェクトの破棄を指示するものとはならない

    // 参照が破棄された(どこからも参照されなくなった)オブジェクトは、
    // いずれかのタイミングでガベージコレクタによって収集・破棄される
  }
}
オブジェクトの作成と破棄
Imports System
Imports System.Collections.Generic

Class Sample
  Shared Sub Main()
    Dim l As New List(Of String)(); ' オブジェクト(List)を作成する

    l.Add("Hello, world!")

    Delete l ' deleteのような、オブジェクトを明示的に破棄する構文はない

    l = Nothing ' Nothingを代入することにより、確保したオブジェクトへの*参照を破棄する*ことはできる
                ' ただし、オブジェクトの破棄を指示するものとはならない

    ' 参照が破棄された(どこからも参照されなくなった)オブジェクトは、
    ' いずれかのタイミングでガベージコレクタによって収集・破棄される
  End Sub
End Class

また、オブジェクトが保持(内包)しているオブジェクトもガベージコレクタによって自動的に収集されます。 このため、例えばコンストラクタでnewしたオブジェクトをファイナライザ(デストラクタ)で破棄する必要もありません。

クラスに保持されるオブジェクトの作成と破棄
using System;
using System.Collections.Generic;

class C {
  private readonly List<string> l;

  // コンストラクタ
  public C()
  {
    l = new List<string>(); // オブジェクト(List)を確保する
  }

  // ファイナライザ/デストラクタ (あくまで例示、実際は実装する必要がない)
  ~C()
  {
    delete l; // オブジェクト'l'はガベージコレクタによって自動的に収集されるので、
              // このように明示的に破棄する必要はない (そもそもそのような構文がない)
  }
}
クラスに保持されるオブジェクトの作成と破棄
Imports System
Imports System.Collections.Generic

Class C
  Private ReadOnly l As List(Of String)

  ' コンストラクタ
  Public Sub New()
    l = New List(Of String)() ' オブジェクト(List)を確保する
  End Sub

  ' ファイナライザ (あくまで例示、実際は実装する必要がない)
  Protected Overrides Sub Finalize()
    Delete l ' オブジェクト'l'はガベージコレクタによって自動的に収集されるので、
             ' このように明示的に破棄する必要はない (そもそもそのような構文がない)
  End Sub
End Class

ガベージコレクタによって管理(manage)され、収集対象となるマネージリソース(manage resources)の場合は、上記のように明示的に破棄・解放する必要はなく、そもそも明示的に破棄・解放することもできません。 一方、ガベージコレクタによって管理されない・収集対象外となるアンマネージリソース(unmanage resources)の場合は、確保した側の責任で破棄・解放を行う必要があります。

つまり、オブジェクト自体はガベージコレクタによって自動的に破棄・解放される一方、オブジェクトがアンマネージリソースを保持している場合、それらのリソースは明示的に破棄・解放を行う必要があります。

マネージリソースとアンマネージリソース

ガベージコレクタの収集対象・マネージリソースは、.NETランタイム上で作成されるオブジェクト(=.NETランタイム内で確保され、.NETランタイムにより管理されるメモリ領域)に限られます。 一方、ファイルハンドルやソケットなどのOS資源は、(.NETランタイムではなく)OS側で管理されているものであるため、ガベージコレクタの収集対象外・アンマネージリソースとなります。 こういったアンマネージリソースを確保する場合は、確保した側が破棄・解放の責任を負います。

一例として、ファイルハンドルと、それを扱う操作を提供するFileStreamクラスの場合を見てみます。 FileStreamでは、インスタンスの作成時にその内部でファイルハンドルの取得が行われ、FileStreamのメソッドを呼び出すことによりそのファイルハンドルを経由したファイル操作が行われます。

FileStreamクラスによって保持されているファイルハンドルは、FileStreamクラスが破棄される時点でFileStreamクラス自身によって解放されます。 FileStreamクラスがファイルハンドルの確保から解放までの責任を負っているため、このクラスを使う側(および.NETランタイム)は直接ファイルハンドルの管理には関与しません。

アンマネージリソースを扱うクラスの例
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // FileStreamでファイルを開く (FileStreamが、指定されたファイルのファイルハンドルを取得する)
    var stream = new FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read);

    stream.ReadByte();
      :
      :

    // (ガベージコレクタによって)FileStreamが解放される時点で、FileStream自身がファイルハンドルの解放を行う
  }
}
アンマネージリソースを扱うクラスの例
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' FileStreamでファイルを開く (FileStreamが、指定されたファイルのファイルハンドルを取得する)
    Dim stream As New FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read)

    stream.ReadByte()
      :
      :
    ' (ガベージコレクタによって)FileStreamが解放される時点で、FileStream自身がファイルハンドルの解放を行う
  End Sub
End Class

上記の例では、FileStreamが解放されるまでの間、ファイルハンドルは保持されたままになります。

FileStreamを含め、一般にIDisposableインターフェイスを持つオブジェクトは、usingステートメント内で使用するなどして、保持しているアンマネージリソースが解放されるよう、必ずいずれかのタイミングでDisposeメソッドが呼び出されるようにします。

FileStreamクラスを使ったファイル入出力の具体例はFileStreamクラスStreamReaderクラス・StreamWriterクラスを参照してください。

別の例として、Marshal.Alloc*()等のメソッドでユーザーが独自に確保したメモリ領域も、アンマネージリソースとなります。 Marshal.Alloc*は.NETランタイムが管理するメモリ領域ではなく、(.NETランタイムではない何らかの)アロケータが管理するメモリ領域を取得するため、ガベージコレクタの収集対象外となります。 この場合、解放の責任を負うのはメソッドを呼び出したユーザー自身となるため、ユーザー自身がMarshal.Free*()等のメソッドで解放する必要があります。

ユーザーコードでアンマネージリソースを扱う
using System;
using System.Runtime.InteropServices;

class Sample {
  static void Main()
  {
    const int length = 8;

    // アンマネージメモリ領域を確保する
    IntPtr ptr = Marshal.AllocHGlobal(length);

    // アンマネージメモリ領域を解放する
    Marshal.FreeHGlobal(ptr);
  }
}
ユーザーコードでアンマネージリソースを扱う
Imports System
Imports System.Runtime.InteropServices

Class Sample
  Shared Sub Main()
    Const length As Integer = 8

    ' アンマネージメモリ領域を確保する
    Dim ptr As IntPtr = Marshal.AllocHGlobal(length)

    ' アンマネージメモリ領域を解放する
    Marshal.FreeHGlobal(ptr)
  End Sub
End Class

明示的なリソースの破棄・確実なリソースの破棄

一般に、アンマネージリソースは必要がなくなった時点で速やかに破棄・解放することが求められます。 また、例外が発生した場合でも確実に破棄・解放されるようにしておく必要があります。

明示的なリソースの破棄

ガベージコレクタによるオブジェクトの破棄では、アンマネージリソースは対象とならない、また明示的に破棄を指示できないという特徴があります。 このため、アンマネージリソースを明示的かつ速やかに破棄するための手段としてIDisposableインターフェイスが用意されています。

IDisposableクラスを実装するクラスでは、IDisposable.Disposeメソッドを呼び出すことにより、アンマネージリソースの破棄を明示的に指示することができるようになっています。 アンマネージリソースを扱うクラスによっては、アンマネージリソースを破棄する手段がDispose以外のメソッド名で提供されている場合もありますが、どちらを呼び出した場合でもアンマネージリソースが破棄されるようになっています。


IDisposableインターフェイスを実装するクラスの例として、FileStreamクラスを挙げます。 FileStreamでは、内部で保持しているファイルハンドルはFileStreamが破棄される時点で同時に破棄されるように実装されていますが、必要なくなったらその時点で明示的に破棄させることもできるようになっています。 FileStreamクラスではCloseメソッドまたはDisposeメソッドを呼び出すことで明示的にファイルハンドルの破棄を指示できます。

IDisposableインターフェイスを実装するクラスでの明示的なリソースの破棄
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // FileStreamでファイルを開く
    var stream = new FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read);

    stream.ReadByte();

    // Disposeメソッド、あるいはそれを呼び出すメソッド(FileStreamの場合はClose)を呼び出して解放する
    //   FileStreamがガベージコレクタによって破棄されるよりも前の時点で、
    //   明示的に保持しているファイルハンドルを破棄させることができる
    stream.Close();
  }
}
IDisposableインターフェイスを実装するクラスでの明示的なリソースの破棄
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' FileStreamでファイルを開く
    Dim stream As New FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read)

    stream.ReadByte()

    ' Disposeメソッド、あるいはそれを呼び出すメソッド(FileStreamの場合はClose)を呼び出して解放する
    '   FileStreamがガベージコレクタによって破棄されるよりも前の時点で、
    '   明示的に保持しているファイルハンドルを破棄させることができる
    stream.Close();
  End Sub
End Class

より詳しくは§.IDisposableインターフェイスで解説します。

確実なリソースの破棄

アンマネージリソースを確実に破棄する手段として、usingステートメントSafeHandleクラスファイナライザが用意されています。


usingステートメントはIDisposableインターフェイスによるリソースの破棄を補助する構文で、オブジェクトのスコープ(有効範囲)を明示するとともに、例外発生の有無に関わらずスコープから抜ける際に自動的にIDisposable.Disposeメソッドを呼び出すようにするものです。

usingステートメントを使った確実なリソースの破棄
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // FileStreamでファイルを開く (FileStreamが、指定されたファイルのファイルハンドルを取得する)
    using (var stream = new FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read)) {
      stream.ReadByte();

      throw new Exception(); // streamを操作中に例外が発生した場合を想定
    } // 仮に例外が発生した場合でも、streamがスコープから抜ける時点で、Disposeメソッドが自動的に呼び出される
  }
}
usingステートメントを使った確実なリソースの破棄
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' FileStreamでファイルを開く (FileStreamが、指定されたファイルのファイルハンドルを取得する)
    Using stream As New FileStream("file.dat", FileMode.Open, FileAccess.Read, FileShare.Read)
      stream.ReadByte()

      Throw New Exception() ' streamを操作中に例外が発生した場合を想定
    End Using ' 仮に例外が発生した場合でも、streamがスコープから抜ける時点で、Disposeメソッドが自動的に呼び出される
  End Sub
End Class

より詳しくは§.usingステートメントで解説します。


SafeHandle(およびその派生クラス)は、API呼び出し等で用いられるハンドルやポインタを安全に扱うためのクラスです。 SafeHandleクラスは、IDisposableインターフェイスを実装したクラスとしてハンドルをラップすることにより、usingステートメントと組み合わせてハンドルを扱うことができるようにします。 より詳しくは§.SafeHandleクラスで解説します。


ファイナライザ(デストラクタ)は、作成するときに呼び出されるコンストラクタとは逆に、オブジェクトが破棄されるときに呼び出されるものです。 ただし、C++等におけるデストラクタとは異なり、構文等によってファイナライザを明示的に実行させることはできません。 (§.ファイナライザの呼び出し)

ファイナライザは通常、アンマネージリソースを扱う場合にのみIDisposableインターフェイスとともに実装します。 IDisposable.Disposeメソッドが呼び出されなかった場合の保証としてファイナライザを実装します。 ファイナライザについては§.ファイナライザにて解説します。


ファイナライザとIDisposableインターフェイスはどのように実装するのがよいか、そのデザインパターンが提示されています。 ほとんどの場合は、これに従う形で実装することにより、明示的な・確実なリソースの破棄を行えるようにすることができます。 詳しくは§.ファイナライザとIDisposableのデザインパターン (disposeパターン)にて解説します。

IDisposableインターフェイス

IDisposableインターフェイスは、それを実装する型が適切に破棄・解放されるべきリソースを保持していることを表すと同時に、それを明示的に破棄させる手段を提供するためのインターフェイスです。 IDisposableインターフェイスには一つのメソッドDisposeがあり、このメソッドを呼び出すことでリソースを破棄・解放できるようにします。

ファイナライザによるリソースの破棄では、ファイナライザが実行されるタイミングを制御できない常に実行されるとは限らないという問題があります。 アンマネージリソースを扱うクラスなど、リソースを任意のタイミングで速やかに破棄・解放できるようにしたい場合には、IDisposableインターフェイスを実装します。

以下は、IDisposableインターフェイスを実装する例です。 この例では、適切に解放すべきリソースの一例としてMarshal.AllocHGlobalでメモリ領域を確保し、IDisposableインターフェイスによって解放できるようにしています。

あくまでIDisposableインターフェイスのみを主眼においた実装となっているため、例外時等の考慮は十分ではない点に注意してください。 より適切な実装については§.ファイナライザとIDisposableのデザインパターン (disposeパターン)を参照してください。

IDisposableインターフェイスを実装する
using System;
using System.Runtime.InteropServices;

class UnmanagedMemory : IDisposable {
  public IntPtr Ptr; // アンマネージメモリ領域を参照するポインタ

  public UnmanagedMemory(int size)
  {
    // メモリ領域を確保する
    Ptr = Marshal.AllocHGlobal(size);
  }

  // IDisposable.Disposeの実装
  public void Dispose()
  {
    if (Ptr == IntPtr.Zero)
      return; // すでに解放されている場合は何もしない(二重解放の抑止)

    // 確保されているメモリ領域を解放する
    Marshal.FreeHGlobal(Ptr);

    // 解放済みであることを示すためにIntPtr.Zeroを設定する
    Ptr = IntPtr.Zero;
  }
}

class Sample {
  static void Main()
  {
    // IDisposableインターフェイスを実装するオブジェクトを作成する
    // (本来ならusingステートメントを使ったほうがよいが、例示のため省略)
    var mem = new UnmanagedMemory(4);

    Marshal.WriteInt32(mem.Ptr, 16);

    // オブジェクトが必要なくなった時点でDisposeメソッドを呼び出し、アンマネージリソースを解放する
    mem.Dispose();
  }
}
IDisposableインターフェイスを実装する
Imports System
Imports System.Runtime.InteropServices

Class UnmanagedMemory
  Implements IDisposable

  Public Ptr As IntPtr ' アンマネージメモリ領域を参照するポインタ

  Public Sub New(ByVal size As Integer)
    ' メモリ領域を確保する
    Ptr = Marshal.AllocHGlobal(size)
  End Sub

  ' IDisposable.Disposeの実装
  Public Sub Dispose() Implements IDisposable.Dispose
    ' すでに解放されている場合は何もしない(二重解放の抑止)
    If Ptr = IntPtr.Zero Then Return

    ' 確保されているメモリ領域を解放する
    Marshal.FreeHGlobal(Ptr)

    ' 解放済みであることを示すためにIntPtr.Zeroを設定する
    Ptr = IntPtr.Zero
  End Sub
End Class

Class Sample
  Shared Sub Main()
    ' IDisposableインターフェイスを実装するオブジェクトを作成する
    ' (本来ならUsingステートメントを使ったほうがよいが、例示のため省略)
    Dim mem As New UnmanagedMemory(4)

    Marshal.WriteInt32(mem.Ptr, 16)

    ' オブジェクトが必要なくなった時点でDisposeメソッドを呼び出し、アンマネージリソースを解放する
    mem.Dispose()
  End Sub
End Class

IDisposableインターフェイスでは、Disposeメソッドが複数回呼び出された場合でも正しく動作するように実装する必要があります。 具体的には、リソースの二重解放が行われないようにするなどの考慮が必要です。 また、Disposeメソッドからは後述の例外ObjectDisposedExceptionをスローしないようにします。

IDisposableインターフェイスには、すでに破棄されているかどうかを知るためのIsDisposedのようなプロパティはありません。 このことからも、Disposeメソッドが複数回呼び出されても正しく動作するようにする必要があります。

Disposeメソッドを呼び出す時点でそのオブジェクト自体必要なくなっているケースがほとんどですが、IsDisposedのようなプロパティが必要ならば、Disposeメソッドが呼び出された時点で内部的にフラグを立て、それを返すプロパティを用意するなどします。

Disposeメソッド以外での解放処理の実装

単にリソースを明示的に解放できるようにしたいならば、IDisposableインターフェイスを使わずともReleaseCloseといった解放用のメソッドを用意しておき、それらのメソッドを呼び出すようにすることもできます。 しかし、IDisposableを実装しておくと、usingステートメントによる構文の補助に基づく確実な解放処理が行えるようになります。 加えて、IDisposableインターフェイスによって、型が解放されるべきリソースを保持していることを表明できます。

Dispose以外のメソッド名で解放処理を提供したいといった場合には、次の例のように別名の解放用メソッドを公開しつつ、IDisposable.Disposeメソッドは明示的に実装(またはPrivateとして実装)し、それを呼び出すようにすることができます。 このようにすることでDispose以外のメソッド名で解放処理を提供できると同時に、IDisposableインターフェイスによる解放処理も提供できます。

IDisposableインターフェイスを実装し、Dispose以外のメソッド名で解放処理を公開する
using System;
using System.Runtime.InteropServices;

class UnmanagedMemory : IDisposable {
  public IntPtr Ptr; // アンマネージメモリ領域を参照するポインタ

  public UnmanagedMemory(int size)
  {
    // メモリ領域を確保する
    Ptr = Marshal.AllocHGlobal(size);
  }

  // メモリ領域を確保するためのメソッドとして、Disposeではない別名のメソッドを提供する
  // (IDisposable.Disposeを明示的な実装にし、それを呼び出すようにする)
  public void Free() => (this as IDisposable).Dispose();

  // '明示的な'IDisposable.Disposeの実装
  void IDisposable.Dispose()
  {
    if (Ptr == IntPtr.Zero)
      return;

    Marshal.FreeHGlobal(Ptr);

    Ptr = IntPtr.Zero;
  }
}

class Sample {
  static void Main()
  {
    // IDisposableインターフェイスを実装するオブジェクトを作成する
    // (本来ならusingステートメントを使ったほうがよいが、例示のため省略)
    var mem = new UnmanagedMemory(4);

    Marshal.WriteInt32(mem.Ptr, 16);

    // Freeメソッドでアンマネージリソースを解放する
    // (このメソッド経由でIDisposable.Disposeメソッドが呼び出される)
    mem.Free();
  }
}
IDisposableインターフェイスを実装し、Dispose以外のメソッド名で解放処理を公開する
Imports System
Imports System.Runtime.InteropServices

Class UnmanagedMemory
  Implements IDisposable

  Public Ptr As IntPtr ' アンマネージメモリ領域を参照するポインタ

  Public Sub New(ByVal size As Integer)
    ' メモリ領域を確保する
    Ptr = Marshal.AllocHGlobal(size)
  End Sub

  ' メモリ領域を確保するためのメソッドとして、Disposeではない別名のメソッドを提供する
  ' (IDisposable.DisposeをPrivateな実装にし、それを呼び出すようにする)
  Public Sub Free()
    DirectCast(Me, IDisposable).Dispose()
  End Sub
  
  ' PrivateなIDisposable.Disposeの実装
  Private Sub Dispose() Implements IDisposable.Dispose
    If Ptr = IntPtr.Zero Then Return

    Marshal.FreeHGlobal(Ptr)

    Ptr = IntPtr.Zero
  End Sub
End Class

Class Sample
  Shared Sub Main()
    ' IDisposableインターフェイスを実装するオブジェクトを作成する
    ' (本来ならUsingステートメントを使ったほうがよいが、例示のため省略)
    Dim mem As New UnmanagedMemory(4)

    Marshal.WriteInt32(mem.Ptr, 16)

    ' Freeメソッドでアンマネージリソースを解放する
    ' (このメソッド経由でIDisposable.Disposeメソッドが呼び出される)
    mem.Free()
  End Sub
End Class

解放されたリソースへのアクセス拒否 (ObjectDisposedException)

IDisposable.Disposeメソッドによってリソースが破棄された場合、以降は破棄されたリソースにアクセスできないようにする必要があります。 このような場合にスローする例外がObjectDisposedExceptionです。 メソッド呼び出しやメンバ参照によって解放済みのリソースへのアクセスが生じる場合にこの例外をスローします。

ObjectDisposedExceptionコンストラクタの引数objectNameには例外をスローしたオブジェクトの名前を指定します。 通常はオブジェクトの名前として型の名前、具体的にはGetType().FullNameを指定します。

以下の例は、解放済みのリソースにアクセスしようとした場合にObjectDisposedExceptionをスローするようにしたものです。

すでに解放済みのリソースへのアクセスを試行した場合にObjectDisposedExceptionをスローする
using System;
using System.Runtime.InteropServices;

class UnmanagedMemory : IDisposable {
  private IntPtr ptr; // アンマネージメモリ領域を参照するポインタ

  public UnmanagedMemory(int size)
  {
    ptr = Marshal.AllocHGlobal(size);
  }

  public void Clear()
  {
    // Ptrが0の場合(すでにDisposeメソッドで解放済みの場合)、例外ObjectDisposedExceptionをスローする
    if (ptr == IntPtr.Zero)
      throw new ObjectDisposedException(GetType().FullName); // 例外の原因となるオブジェクト名として型名を指定する

    // そうでない場合はメソッドの通常の処理を行う
  }

  public void Dispose()
  {
    if (ptr == IntPtr.Zero)
      return; // すでに解放されている場合は何もしない(DisposeメソッドはObjectDisposedExceptionをスローしてはならない)

    Marshal.FreeHGlobal(ptr);

    // 解放済みであることを示すためにIntPtr.Zeroを設定する
    ptr = IntPtr.Zero;
  }
}

class Sample {
  static void Main()
  {
    // IDisposableインターフェイスを実装するオブジェクトを作成する
    // (本来ならusingステートメントを使ったほうがよいが、例示のため省略)
    var mem = new UnmanagedMemory(4);

    // Disposeメソッドを呼び出し、アンマネージリソースを解放する
    mem.Dispose();

    // アンマネージリソースへのアクセスを試みる
    // (オブジェクトはすでに破棄されているため、ObjectDisposedExceptionとなる)
    mem.Clear();
  }
}
すでに解放済みのリソースへのアクセスを試行した場合にObjectDisposedExceptionをスローする
Imports System
Imports System.Runtime.InteropServices

Class UnmanagedMemory
  Implements IDisposable

  Private ptr As IntPtr ' アンマネージメモリ領域を参照するポインタ

  Public Sub New(ByVal size As Integer)
    ptr = Marshal.AllocHGlobal(size)
  End Sub

  Public Sub Clear()
    ' Ptrが0の場合(すでにDisposeメソッドで解放済みの場合)、例外ObjectDisposedExceptionをスローする
    ' (例外の原因となるオブジェクト名として型名を指定する)
    If ptr = IntPtr.Zero Then Throw New ObjectDisposedException(Me.GetType().FullName)

    ' そうでない場合はメソッドの通常の処理を行う
  End Sub

  Public Sub Dispose() Implements IDisposable.Dispose
    ' すでに解放されている場合は何もしない(DisposeメソッドはObjectDisposedExceptionをスローしてはならない)
    If ptr = IntPtr.Zero Then Return

    Marshal.FreeHGlobal(ptr)

    ' 解放済みであることを示すためにIntPtr.Zeroを設定する
    ptr = IntPtr.Zero
  End Sub
End Class

Class Sample
  Shared Sub Main()
    ' IDisposableインターフェイスを実装するオブジェクトを作成する
    ' (本来ならUsingステートメントを使ったほうがよいが、例示のため省略)
    Dim mem As New UnmanagedMemory(4)

    ' Disposeメソッドを呼び出し、アンマネージリソースを解放する
    mem.Dispose()

    ' アンマネージリソースへのアクセスを試みる
    ' (オブジェクトはすでに破棄されているため、ObjectDisposedExceptionとなる)
    mem.Clear()
  End Sub
End Class
実行結果
ハンドルされていない例外: System.ObjectDisposedException: 破棄されたオブジェクトにアクセスできません。
オブジェクト名 'UnmanagedMemory' です。

上記の例にもあるように、IDisposable.Disposeメソッドでは、すでに解放済みの場合であってもObjectDisposedExceptionをスローしないようにします。 ObjectDisposedExceptionは、使用しようとしたリソースが既に解放済みだった場合にスローします。

仮にDisposeメソッドでオブジェクトが破棄されている場合でも、解放済みリソースへのアクセスが生じない処理の場合はObjectDisposedExceptionをスローしないようにすることもできます。 例えば、IsDisposedIsClosedのようなプロパティを用意する場合は、破棄されていてもObjectDisposedExceptionをスローしない動作とします。

破棄されたオブジェクトにおけるファイナライザの呼び出し抑止 (GC.SuppressFinalize)

IDisposableインターフェイスを実装する場合、仮にIDisposable.Disposeメソッドの呼び出しが行われなかった場合でもリソースが解放されるように、ファイナライザを実装して解放処理が呼び出されるようにすることができます。 (関連:§.ファイナライザ§.ファイナライザとIDisposableのデザインパターン (disposeパターン))

一方で、IDisposable.Disposeメソッドで明示的にリソースが破棄された場合、Disposeメソッドが呼び出されなかった場合の保証としてのファイナライザ呼び出しは不要となります。 そのような場合は、GC.SuppressFinalizeメソッドを呼び出すことで、オブジェクトのファイナライザ呼び出しを抑止することができます。

GC.SuppressFinalizeは、ガベージコレクタ(GC)に対してオブジェクトのファイナライザ呼び出しが不要であることを通知します。 ガベージコレクタは、これに基づいてガベージコレクションの際のファイナライザ呼び出しを行わなくなります。

GC.SuppressFinalizeで不要なファイナライザ呼び出しを抑止する
using System;
using System.Runtime.InteropServices;

class UnmanagedMemory : IDisposable {
  private IntPtr ptr; // アンマネージメモリ領域を参照するポインタ

  public UnmanagedMemory(int size)
  {
    // アンマネージリソースを確保する
    ptr = Marshal.AllocHGlobal(size);
  }

  // IDisposable.Disposeの実装
  public void Dispose()
  {
    // アンマネージリソースの解放処理を行う
    Free();

    // 以降このオブジェクトに対するファイナライザ呼び出しは不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(this);
  }

  // デストラクタ(ファイナライザ)の実装
  // (Disposeメソッドが呼び出されている場合は、GC.SuppressFinalizeでファイナライザの
  // 呼び出しが抑止されるため、呼び出されない)
  ~UnmanagedMemory()
  {
    // アンマネージリソースの解放処理を行う
    Free();
  }

  private void Free()
  {
    // すでに解放されている場合は何もしない(二重解放の抑止)
    if (ptr == IntPtr.Zero)
      return;

    // 解放されていなければ、解放処理を行う
    Marshal.FreeHGlobal(ptr);

    // 解放済みであることを示すためにIntPtr.Zeroを設定する
    ptr = IntPtr.Zero;
  }
}

class Sample {
  static void Main()
  {
    // IDisposableインターフェイスを実装するオブジェクトを作成する
    // (本来ならusingステートメントを使ったほうがよいが、例示のため省略)
    var mem = new UnmanagedMemory(4);

    // Disposeメソッドが明示的に呼び出されなかった場合を想定
    // mem.Dispose();

    // 以降、ガベージコレクタによってオブジェクトが回収される場合、
    // ファイナライザが呼び出され、アンマネージリソースが解放される
  }
}
GC.SuppressFinalizeで不要なファイナライザ呼び出しを抑止する
Imports System
Imports System.Runtime.InteropServices

Class UnmanagedMemory
  Implements IDisposable

  Private ptr As IntPtr ' アンマネージメモリ領域を参照するポインタ

  Public Sub New(ByVal size As Integer)
    ' アンマネージリソースを確保する
    ptr = Marshal.AllocHGlobal(size)
  End Sub

  ' IDisposable.Disposeの実装
  Public Sub Dispose() Implements IDisposable.Dispose
    ' アンマネージリソースの解放処理を行う
    Free()

    ' 以降このオブジェクトに対するファイナライザ呼び出しは不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(Me)
  End Sub

  ' ファイナライザの実装
  ' (Disposeメソッドが呼び出されている場合は、GC.SuppressFinalizeでファイナライザの
  ' 呼び出しが抑止されるため、呼び出されない)
  Protected Overrides Sub Finalize()
    ' アンマネージリソースの解放処理を行う
    Free()
  End Sub

  Private Sub Free()
    ' すでに解放されている場合は何もしない(二重解放の抑止)
    If ptr = IntPtr.Zero Then Return

    ' 解放されていなければ、解放処理を行う
    Marshal.FreeHGlobal(ptr)

    ' 解放済みであることを示すためにIntPtr.Zeroを設定する
    ptr = IntPtr.Zero
  End Sub
End Class

Class Sample
  Shared Sub Main()
    ' IDisposableインターフェイスを実装するオブジェクトを作成する
    ' (本来ならusingステートメントを使ったほうがよいが、例示のため省略)
    Dim mem As New UnmanagedMemory(4)

    ' Disposeメソッドが明示的に呼び出されなかった場合を想定
    ' mem.Dispose();

    ' 以降、ガベージコレクタによってオブジェクトが回収される場合、
    ' ファイナライザが呼び出され、アンマネージリソースが解放される
  End Sub
End Class

.NET Core/.NET 5以降では、.NETランタイムのシャットダウン時(≒.NETプロセスの終了時)におけるファイナライザ呼び出しは行われません。 このため、上記の実装では、必ずしもファイナライザによってアンマネージリソースが解放されるとは限りません。 詳しくは§.シャットダウン時におけるファイナライザ呼び出しを参照してください。

IAsyncDisposableインターフェイス

IAsyncDisposableインターフェイスは.NET Standard 2.1/.NET Core 3.0以降で提供されるインターフェイスで、IDisposableインターフェイスの非同期版です。 IDisposableインターフェイスのDisposeメソッドと同様、DisposeAsyncメソッドが用意されており、このインターフェイスを実装することにより非同期の破棄・解放処理を行うための操作を提供することができるようになります。

IAsyncDisposableインターフェイスはIDisposableインターフェイスとともに実装することも、単独で実装することもできます。 つまり、同期的な破棄・解放処理と、非同期的な破棄・解放処理のどちらかのみ、あるいは両方を実装することができます。 ただし、両方を実装する場合は、どちらか一方で破棄・解放処理が行われたら、もう一方が呼び出されても二重に破棄・解放が行われないように実装する必要があります。

IDisposableインターフェイスはusingステートメントとともに使用することができるのと同様、IAsyncDisposableインターフェイスは非同期版のawait usingステートメントとともに使用することができます。

IAsyncDisposableは、主にStream等のIAsyncDisposableインターフェイスを実装するクラスを内包して扱うようなクラスを作成する際に実装することが多くなるものと思われます。

以下は、IAsyncDisposableを実装し、クラス内で保持しているStreamを非同期的に破棄できるようにする例です。

あくまでIAsyncDisposableインターフェイスのみを主眼においた実装となっているため、例外時等の考慮は十分ではない点に注意してください。 より適切な実装については§.ファイナライザとIDisposableのデザインパターン (disposeパターン)を参照してください。

IAsyncDisposableインターフェイスを実装する .NET Standard 2.1/.NET Core 3.0
using System;
using System.IO;
using System.Threading.Tasks;

// IAsyncDisposableインターフェイスを実装するクラス
class Resource : IAsyncDisposable {
  private Stream stream; // 解放すべきリソースとしてStreamを内包する

  public Resource()
  {
    // なんらかのStreamインスタンスを作成することを想定(ここでは代替としてStream.Nullを使用)
    this.stream = Stream.Null;
  }

  public async ValueTask DisposeAsync()
  {
    if (stream == null)
      return; // すでに解放済みの場合は何もしない

    // DisposeAsyncメソッドで解放する
    await stream.DisposeAsync().ConfigureAwait(false);

    // 解放済みであることを示すためにnullを設定する
    stream = null;
  }
}

class Sample {
  static async Task Main()
  {
    var res1 = new Resource();

    // DisposeAsyncメソッドを呼び出す
    await res1.DisposeAsync();

    // await usingステートメントを使用する場合は次のようになる
    await using (var res2 = new Resource()) {

    } // スコープから抜ける時点でDisposeAsyncメソッドが呼び出される
  }
}

VB16時点のVisual Basicでは、ValueTaskを返すAsync Functionがサポートされていない?(コンパイルエラーとなる)ため、IAsyncDisposableインターフェイスは実装できないようです。

Imports System
Imports System.IO
Imports System.Threading.Tasks

' IAsyncDisposableインターフェイスを実装するクラス
Class Resource
  Implements IAsyncDisposable

  Private stream As Stream ' 解放すべきリソースとしてStreamを内包する

  Public Sub New()
    ' なんらかのStreamインスタンスを作成することを想定(ここでは代替としてStream.Nullを使用)
    Me.stream = Stream.Null
  End Sub

  Public Async Function DisposeAsync() As ValueTask ' error BC36945: Async' 修飾子は、Sub と、Task または Task(Of T) を返す Function でのみ使用できます。
    If stream Is Nothing Then Return ' すでに解放済みの場合は何もしない

    ' DisposeAsyncメソッドで解放する
    Await stream.DisposeAsync().ConfigureAwait(False)

    ' 解放済みであることを示すためにNothingを設定する
    stream = Nothing
  End Function
End Class

Class Sample
  Shared Async Function Main() As Task
    Dim res1 As New Resource()

    ' DisposeAsyncメソッドを呼び出す
    Await res1.DisposeAsync()

    ' VB16時点ではC# 8.0のawait usingに相当する構文は用意されていない
    ' await using (var res2 = new Resource()) {
    ' } 
  End Function
End Class

usingステートメント

usingステートメントは、IDisposableインターフェイスとtry-finallyステートメントによるオブジェクト破棄の処理を、専用の構文で行えるようにしたものです。 usingステートメントを使うと、IDisposable.Disposeメソッドの呼び出しと、try-finallyステートメントによる例外発生時の考慮を、より簡単な構文で記述することができます。

IDisposableインターフェイスを実装するオブジェクトを扱う場合は、必要なくなった時点でDisposeメソッドを呼び出すようにすることが求められます。 また、例外が発生した場合でも最終的にDisposeメソッドでオブジェクトが破棄されるようにする必要があります。 usingステートメントを使うと、この2つの要求を単一の構文で満たすことができます。 オブジェクトを使用する範囲(オブジェクトの生存期間)がメソッド内で完結するような場合は、usingステートメントが特に便利です。

usingステートメントでは、処理中の例外発生の有無に関わらずスコープから抜ける時点で必ずDisposeメソッドが自動的に呼び出されます。 usingステートメント内では明示的にDisposeメソッドの呼び出しを記述する必要はありませんが、条件分岐などオブジェクトが不要になった時点で明示的に呼び出すこともできます。

usingステートメントを使った例として、StreamReaderを使用する場合は次のようになります。 比較としてtry-finallyステートメントで等価なコードを記述した場合も併記します。

usingステートメント
usingステートメントを使ってStreamReaderを確実に閉じる
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // StreamReaderインスタンスを作成して使用する
    using (var reader = new StreamReader("test.txt")) {
      Console.WriteLine(reader.ReadToEnd());
    }
    // usingステートメントを抜ける時点で
    // 自動的にDisposeメソッドが呼び出される
    // (途中で例外がスローされた場合でも呼び出される)
  }
}
usingステートメントを使ってStreamReaderを確実に閉じる
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' StreamReaderインスタンスを作成して使用する
    Using reader As New StreamReader("test.txt")
      Console.WriteLine(reader.ReadToEnd())
    End Using
    ' Usingステートメントを抜ける時点で
    ' 自動的にDisposeメソッドが呼び出される
    ' (途中で例外がスローされた場合でも呼び出される)
  End Sub
End Class
try-finallyステートメント
try-finallyステートメントを使ってStreamReaderを確実に閉じる
using System;
using System.IO;

class Sample {
  static void Main()
  {
    StreamReader reader = null;

    try {
      // StreamReaderインスタンスを作成して使用する
      reader = new StreamReader("test.txt");

      Console.WriteLine(reader.ReadToEnd());
    }
    finally {
      // try節を抜けた時点(途中で例外がスローされた場合含む)で
      // StreamReaderがnullでなければDisposeメソッドを呼び出す
      if (reader != null)
        reader.Dispose();
    }
  }
}
try-finallyステートメントを使ってStreamReaderを確実に閉じる
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    Dim reader As StreamReader = Nothing

    Try
      ' StreamReaderインスタンスを作成して使用する
      reader = new StreamReader("test.txt")

      Console.WriteLine(reader.ReadToEnd())
    Finally
      ' Try節を抜けた時点(途中で例外がスローされた場合含む)で
      ' StreamReaderがNothingでなければDisposeメソッドを呼び出す
      If Not reader Is Nothing Then reader.Dispose()
    End Try
  End Sub
End Class

usingステートメントを使うと、try-finallyステートメントとIDisposable.Disposeメソッドの呼び出しを簡略に記述できるだけでなく、オブジェクトのスコープ(有効範囲)と生存期間を明確にできるという利点もあります。

usingステートメントのバリエーション

usingステートメントは使用するオブジェクトを複数指定したり、for・foreachなどと同様に入れ子にして記述したりすることができるほか、ローカル変数宣言形式で記述したりすることができます。

複数のオブジェクトを指定したusingステートメント

usingステートメントでは、宣言部で複数のオブジェクトを指定することができます。 単一行で複数のローカル変数を宣言するのと同様に、使用するオブジェクトをカンマ,で区切って記述します。 以下の例はusingステートメントで二つのStreamを作成・使用する例です。

単一のusingステートメントで複数のオブジェクトを使用する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // "source.txt"を読み込むストリームと、"dest.txt"に書き込むストリームを作成し、使用し終わったら破棄する
    using (Stream fromStream = File.OpenRead("source.txt"), toStream = File.OpenWrite("dest.txt")) {
      // fromStreamの内容をtoStreamにコピーする
      // (ファイルsource.txtの内容をdest.txtにコピーする)
      fromStream.CopyTo(toStream);
    }

    // (比較として)単一行での複数のローカル変数宣言
    int x = 0, y = 0;
  }
}
単一のusingステートメントで複数のオブジェクトを使用する
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' "source.txt"を読み込むストリームと、"dest.txt"に書き込むストリームを作成し、使用し終わったら破棄する
    Using fromStream As FileStream = File.OpenRead("source.txt"), toStream As FileStream = File.OpenWrite("dest.txt")
      ' fromStreamの内容をtoStreamにコピーする
      ' (ファイルsource.txtの内容をdest.txtにコピーする)
      fromStream.CopyTo(toStream)
    End Using

    ' (比較として)単一行での複数のローカル変数宣言
    Dim x As Integer = 0, y As Integer = 0
  End Sub
End Class

この例で使用しているCopyToメソッドは.NET Framework 4以降で使用可能なメソッドです。 StreamクラスやCopyToメソッドについてはストリームの基本とStreamクラスを参照してください。

入れ子にしたusingステートメント

usingステートメントでは、宣言部で複数のオブジェクトを記述する以外にも、usingステートメントを入れ子にして複数記述することもできます。

usingステートメントを入れ子にして複数のオブジェクトを使用する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // "source.txt"を読み込むストリームを作成し、使用し終わったら破棄する
    using (var fromStream = File.OpenRead("source.txt")) {
      // "dest.txt"に書き込むストリームを作成し、使用し終わったら破棄する
      using (var toStream = File.OpenWrite("dest.txt")) {
        // fromStreamの内容をtoStreamにコピーする
        // (ファイルsource.txtの内容をdest.txtにコピーする)
        fromStream.CopyTo(toStream);
      }
    }
  }
}
usingステートメントを入れ子にして複数のオブジェクトを使用する
Imports System
Imports System.IO

Class Sample
  Shared Sub Main()
    ' "source.txt"を読み込むストリームを作成し、使用し終わったら破棄する
    Using fromStream As FileStream = File.OpenRead("source.txt")
      ' "dest.txt"に書き込むストリームを作成し、使用し終わったら破棄する
      Using toStream As FileStream = File.OpenWrite("dest.txt")
        ' fromStreamの内容をtoStreamにコピーする
        ' (ファイルsource.txtの内容をdest.txtにコピーする)
        fromStream.CopyTo(toStream)
      End Using
    End Using
  End Sub
End Class

C#では、同一スコープかつ複数のusingステートメントを、連続して記述することもできます。 これは、ブレース{...}を省略したif文と同様、単にブレースを省略したusingを記述するものです。

同一スコープかつ複数のusingステートメントでオブジェクトを使用する
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // 同一スコープで複数のusingステートメント
    using (Stream fromStream = File.OpenRead("source.txt"))
    using (Stream toStream = File.OpenWrite("dest.txt")) {
      fromStream.CopyTo(toStream);
    }

    // 上記のコードは以下と同じ
    using (var fromStream = File.OpenRead("source.txt"))
      using (var toStream = File.OpenWrite("dest.txt"))
        fromStream.CopyTo(toStream);

    // 比較として上記と同じ構造の構文をifに置き換えると次のようになる
    if (true)
    if (true) {
      Console.WriteLine(true);
    }

    if (true)
      if (true)
        Console.WriteLine(true);
  }
}

using宣言

C# 8.0以降では、usingステートメントをローカル変数宣言形式にしたものであるusing宣言を使用することができます。 これはスコープがメソッド内全域となるusingステートメントを記述するものと同様で、見かけ上はvarconstなどのローカル宣言のように記述することができます。

ローカル変数宣言形式のusingステートメント(using宣言)でオブジェクトを使用する 
using System;
using System.IO;

class Sample {
  static void Main()
  {
    // "source.txt"を読み込むストリームを作成し、使用し終わったら破棄する
    using var fromStream = File.OpenRead("source.txt");
    // "dest.txt"に書き込むストリームを作成し、使用し終わったら破棄する
    using var toStream = File.OpenWrite("dest.txt");

    // fromStreamの内容をtoStreamにコピーする
    // (ファイルsource.txtの内容をdest.txtにコピーする)
    fromStream.CopyTo(toStream);

    // ローカル変数のスコープから抜ける(メソッドから抜ける)時点で
    // using宣言されたオブジェクトに対してDisposeメソッドが呼び出される
  }
}

await usingステートメント

C# 8.0以降では、usingステートメントの非同期版であるawait usingステートメントを用いることができます。 usingステートメントがIDisposableインターフェイスを持つオブジェクトに対して適用できるのに対し、await usingステートメントはIAsyncDisposableインターフェイスを持つオブジェクト・非同期の破棄・解放処理(DisposeAsync)をサポートしているオブジェクトに対して適用できます。

await usingステートメントは、usingステートメントと同様入れ子にすることができるほか、ローカル変数宣言形式で記述することもできます。

await usingステートメントを使ってオブジェクトを非同期的に破棄する 
using System;
using System.IO;
using System.Threading.Tasks;

class Sample {
  static async Task Main()
  {
    // "source.txt"を読み込むストリームと、"dest.txt"に書き込むストリームを作成し、
    // 使用し終わったら非同期的に破棄する
    await using (Stream fromStream = File.OpenRead("source.txt"), toStream = File.OpenWrite("dest.txt")) {
      // fromStreamの内容をtoStreamにコピーする
      // (ファイルsource.txtの内容をdest.txtにコピーする)
      await fromStream.CopyToAsync(toStream);
    }

    // await usingステートメントを入れ子にする場合は次のようになる
    await using (var fromStream = File.OpenRead("source.txt")) {
      await using (var toStream = File.OpenWrite("dest.txt")) {
        await fromStream.CopyToAsync(toStream);
      }
    }

    await CopyAsync();

    static async Task CopyAsync()
    {
      // ローカル変数宣言形式のawait usingステートメントの場合は次のようになる
      await using var fromStream = File.OpenRead("source.txt");
      await using var toStream = File.OpenWrite("dest.txt");

      await fromStream.CopyToAsync(toStream);
    }
  }
}

usingステートメントとIDisposableを使った終了処理のラップ

終了処理をIDisposable.Disposeメソッドとしてラップし、usingステートメントと組み合わせることにより、終了処理の記述をシンプルで確実なものにすることができます。

例として、BeginXXX/EndXXXやOpenXXX/CloseXXXのように必ずペアで呼び出す必要があるメソッドを考えます。 BeginXXX, OpenXXXの呼び出しをコンストラクタ、EndXXX, CloseXXXの呼び出しをDisposeメソッドで行うようにしたラッパークラスを用意し、usingステートメントと組み合わせて使うことでtry-finallyを使うよりも終了処理を簡単に記述できるようになります。

例えば次のようなクラスがあった場合を想定します。

BeginXXXとEndXXXをペアで呼び出す必要があるクラスと、それを使用したコード
using System;

class Operation {
  // 何らかの操作を開始するメソッド
  public void BeginOperation()
  {
  }

  // 開始した操作の終了処理を行うメソッド
  public void EndOperation()
  {
  }
}

class Sample {
  static void Main()
  {
    Operation o = new Operation();

    try {
      o.BeginOperation();
    }
    finally {
      // BeginOperationを呼び出した後は必ずEndOperationを呼びだして終了処理を行う必要があるとする
      o.EndOperation();
    }
  }
}

このクラスをラップしたクラスを用意して、BeginXXXをコンストラクタ、EndXXXをDisposeメソッドから呼び出すようにすると、usingステートメントを使って次のように記述することができるようになります。

ペアで呼び出す必要があるメソッドをIDisposableでラップした例
using System;

class Operation {
  // 何らかの操作を開始するメソッド
  public void BeginOperation()
  {
  }

  // 開始した操作の終了処理を行うメソッド
  public void EndOperation()
  {
  }
}

// BeginOperationとEndOperationの呼び出しをラップするクラス
class OperationWrapper : IDisposable {
  private Operation o;

  public OperationWrapper()
  {
    o = new Operation();
    o.BeginOperation();
  }

  public void Dispose()
  {
    if (o != null) {
      o.EndOperation();
      o = null;
    }
  }
}

class Sample {
  static void Main()
  {
    // BeginOperationとEndOperationの呼び出しを隠蔽しつつ、確実に終了処理を行えるようになる
    using (var wrapper = new OperationWrapper()) {
    }
  }
}

以下の例は、BitmapクラスのLockBits/UnlockBitsメソッドの呼び出しをラップするクラスBitmapLockを作成し、usingステートメントで使用できるようにしたものです。 この例では、画像ファイルorigin.pngを読み込み、色を反転した結果をnegated.pngとして保存しています。

LockBits/UnlockBitsの呼び出しをラップしてusingステートメントで使えるようにする
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

// Bitmap.LockBits/UnlockBitsの処理をラップしたクラス
class BitmapLock : IDisposable {
  private Bitmap bitmap;
  private BitmapData data;

  public BitmapLock(Bitmap bitmap, Rectangle rect, ImageLockMode mode, PixelFormat format)
  {
    this.bitmap = bitmap;
    this.data = bitmap.LockBits(rect, mode, format);
  }

  public void Dispose()
  {
    if (data != null) {
      bitmap.UnlockBits(data);

      data = null;
      bitmap = null;
    }
  }

  // ラインyの先頭のポインタを取得する
  public IntPtr GetScanLine(int y)
  {
    if (data == null) throw new ObjectDisposedException(GetType().FullName);

    return new IntPtr(data.Scan0.ToInt32() + y * data.Stride);
  }
}

class Sample {
  static void Main()
  {
    using (Bitmap bitmap = (Bitmap)Image.FromFile("origin.png")) {
      Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);

      // 画像全体のピクセルを読み書きできるようにロックする
      using (BitmapLock locked = new BitmapLock(bitmap, rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb)) {
        for (int y = 0; y < rect.Height; y++) {
          IntPtr line = locked.GetScanLine(y);

          for (int x = 0; x < rect.Width; x++) {
            // 1ピクセル分読み込み
            int col = Marshal.ReadInt32(line, x * 4);

            // 反転した値を書き込む
            Marshal.WriteInt32(line, x * 4, ~col);
          }
        }
      }

      bitmap.Save("negated.png", ImageFormat.Png);
    }
  }
}
LockBits/UnlockBitsの呼び出しをラップしてusingステートメントで使えるようにする
Imports System
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices

' Bitmap.LockBits/UnlockBitsの処理をラップしたクラス
Class BitmapLock
  Implements IDisposable

  Private bitmap As Bitmap
  Private data As BitmapData

  Public Sub New(ByVal bitmap As Bitmap, ByVal rect As Rectangle, ByVal mode As ImageLockMode, ByVal format As PixelFormat)
    MyClass.bitmap = bitmap
    MyClass.data = bitmap.LockBits(rect, mode, format)
  End SUb

  Public Sub Dispose() Implements IDisposable.Dispose
    If Not data is Nothing Then
      bitmap.UnlockBits(data)

      data = Nothing
      bitmap = Nothing
    End If
  End Sub

  ' ラインyの先頭のポインタを取得する
  Public Function GetScanLine(ByVal y As Integer) As IntPtr
    If data Is Nothing Then Throw New ObjectDisposedException(Me.GetType().FullName)

    Return New IntPtr(data.Scan0.ToInt32() + y * data.Stride)
  End Function
End Class

Class Sample
  Shared Sub Main()
    Using bitmap As Bitmap = DirectCast(Image.FromFile("origin.png"), Bitmap)
      Dim rect As New Rectangle(0, 0, bitmap.Width, bitmap.Height)

      ' 画像全体のピクセルを読み書きできるようにロックする
      Using locked As New BitmapLock(bitmap, rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb)
        For y As Integer = 0 To rect.Height - 1
          Dim line As IntPtr = locked.GetScanLine(y)

          For x As Integer = 0 To rect.Width - 1
            ' 1ピクセル分読み込み
            Dim col As Integer = Marshal.ReadInt32(line, x * 4)

            ' 反転した値を書き込む
            Marshal.WriteInt32(line, x * 4, Not col)
          Next
        Next
      End Using

      bitmap.Save("negated.png", ImageFormat.Png)
    End Using
  End Sub
End Class

SafeHandleクラス

SafeHandle(およびその派生クラス)は、アンマネージAPI呼び出し等で用いられるハンドルや、解放が必要なポインタ等を安全に扱えるようにするためのクラスです。

SafeHandleクラスでは、IDisposableインターフェイスを実装したクラスとしてハンドルをラップすることにより、usingステートメントと組み合わせたdisposeパターンをハンドルに対して適用することができます。 また、SafeHandleクラスは単なるラッパーとして動作するだけでなく、CriticalFinalizerObjectクラスから派生していることにより、.NETランタイムに対してファイナライザ呼び出しを可能な限り確実に行うよう要求することができます。

SafeHandleクラスは直接継承して使うことができるほか、ハンドルの無効値が定められたSafeHandleZeroOrMinusOneIsInvalidクラスやファイルハンドルを扱うSafeFileHandleクラス、メモリ領域を扱うためのメソッドを持つSafeBufferクラスといったより具体的な専用のクラスも用意されているため、ハンドルの種類や目的に合わせて選択することができます。


以下は、架空のライブラリlibfooを操作するハンドルをラップするクラスLibFooHandleを作成し、usingステートメントと組み合わせて使用する例です。 この例に登場する架空のライブラリlibfooには、以下のようなAPI関数が用意されているものと想定します。

foo_t* foo_create_handle()
libfooの操作関数を呼び出すためのインスタンスハンドルを作成する。
bool foo_release_handle(foo_t* handle)
libfooのインスタンスハンドルを解放する。
void foo_do_something(foo_t* handle)
libfooのインスタンスハンドルに対してなんらかの操作を行う。

なお、例示のためAPI呼び出しに付随して想定される例外等の考慮・処理は省略しています。

SafeHandleクラスを使ってライブラリのハンドルをラップする
using System;
using System.Runtime.InteropServices;

// libfooのAPIセットをDllImportしたものを想定した静的クラス
static class libfoo {
  /*[DllImport("libfoo")]*/ public static /*extern*/ IntPtr foo_create_handle() => new IntPtr(1);
  /*[DllImport("libfoo")]*/ public static /*extern*/ bool foo_release_handle(IntPtr handle) => true;
  /*[DllImport("libfoo")]*/ public static /*extern*/ void foo_do_something(IntPtr handle) => Console.WriteLine(handle);
}

// libfooのハンドルをラップするSafeHandle
public sealed class LibFooHandle : SafeHandle {
  public override bool IsInvalid => handle == IntPtr.Zero; // handleの値が0の場合、無効として扱う

  private LibFooHandle(IntPtr handle)
    : base(IntPtr.Zero, ownsHandle: true)
  {
    SetHandle(handle);
  }

  public static LibFooHandle Create()
  {
    // foo_create_handle関数が返すハンドルをラップしてLibFooHandleとして返す
    return new LibFooHandle(libfoo.foo_create_handle());
  }

  protected override bool ReleaseHandle()
  {
    // foo_release_handle関数を呼びハンドルを解放する
    return libfoo.foo_release_handle(this.handle);

    // このあとhandleには無効値(IntPtr.Zero)が設定され、IsInvalidがtrueとなる
    // IsInvalidがtrueの場合ReleaseHandleは呼び出されなくなるので、二重解放は起こらない
  }

  public void DoSomething()
  {
    if (IsInvalid)
      throw new InvalidOperationException("handle is invalid");

    // foo_do_something関数を呼びハンドルに対して操作を行う
    libfoo.foo_do_something(this.handle);
  }
}

class Sample {
  static void Main()
  {
    // libfooのハンドルを作成する
    using (var handle = LibFooHandle.Create()) {
      // libfooのハンドルを使って何らかの操作を行う
      handle.DoSomething();
    } // 必要なくなった時点でlibfooのハンドルを解放する
  }
}
SafeHandleクラスを使ってライブラリのハンドルをラップする
Imports System
Imports System.Runtime.InteropServices

' libfooのAPIセットをDllImportしたものを想定したモジュール
Module libfoo
  ' <DllImport("libfoo")>
  Public Function foo_create_handle() As IntPtr
    Return New IntPtr(1)
  End Function

  ' <DllImport("libfoo")>
  Public Function foo_release_handle(ByVal handle As IntPtr) As Boolean
    Return True
  End Function

  ' <DllImport("libfoo")>
  Public Sub foo_do_something(ByVal handle As IntPtr)
    Console.WriteLine(handle)
  End Sub
End Module

' libfooのハンドルをラップするSafeHandle
Public NotInheritable Class LibFooHandle
  Inherits SafeHandle

  Public Overrides ReadOnly Property IsInvalid As Boolean
    Get
      Return handle = IntPtr.Zero ' handleの値が0の場合、無効として扱う
    End Get
  End Property

  Private Sub New(ByVal handle As IntPtr)
    MyBase.New(IntPtr.Zero, ownsHandle := True)

    SetHandle(handle)
  End Sub

  Public Shared Function Create() As LibFooHandle
    ' foo_create_handle関数が返すハンドルをラップしてLibFooHandleとして返す
    Return New LibFooHandle(libfoo.foo_create_handle())
  End Function

  Protected Overrides Function ReleaseHandle() As Boolean
    ' foo_release_handle関数を呼びハンドルを解放する
    Return libfoo.foo_release_handle(Me.handle)

    ' このあとhandleには無効値(IntPtr.Zero)が設定され、IsInvalidがTrueとなる
    ' IsInvalidがTrueの場合ReleaseHandleは呼び出されなくなるので、二重解放は起こらない
  End Function

  Public Sub DoSomething()
    If IsInvalid Then Throw New InvalidOperationException("handle is invalid")

    ' foo_do_something関数を呼びハンドルに対して操作を行う
    libfoo.foo_do_something(Me.handle)
  End Sub
End Class

Class Sample
  Shared Sub Main()
    ' libfooのハンドルを作成する
    Using handle As LibFooHandle = LibFooHandle.Create()

      ' libfooのハンドルを使って何らかの操作を行う
      handle.DoSomething()

    End Using ' 必要なくなった時点でlibfooのハンドルを解放する
  End Sub
End Class

.NET Core 3.0以降では、ネイティブライブラリのロードに、DllImport属性だけでなくNativeLibraryクラスを使用することができるようになっています。

ファイナライザ

ファイナライザ(finalizer)を実装することにより、オブジェクトが破棄される際に実行されるべき処理を記述することができます。 ファイナライザは、ガベージコレクタによってオブジェクトが破棄される際に呼び出されるもので、デストラクタ構文(C#)またはObject.Finalizeメソッドのオーバーライド(VB)によって実装できます。 (§.ファイナライザの実装)

ただし、ガベージコレクタによるメモリ収集の負荷を増やす原因となるため、ファイナライザを不必要にオーバーライドすることは避けるべきとされています。 解放すべきアンマネージリソースや行うべき終了処理が無い限りは、ファイナライザをオーバーライドする必要はありません。 特に、プレースホルダとして空のファイナライザを記述しておく、といったことは避けるべきです。

アンマネージリソースの解放を目的としてファイナライザを実装する場合は、通常IDisposableインターフェイスと合わせて実装します。 (§.ファイナライザとIDisposableのデザインパターン (disposeパターン)) 仮にIDisposable.Disposeメソッドによる解放が行われなかった場合の保証として、ファイナライザで解放処理を行うようにすることができます。

ただし、.NET Core/.NET 5以降ではプロセス終了時などにおけるファイナライザ呼び出しが行われません。 このため、アンマネージリソースを扱う場合は、まずIDisposableインターフェイスによる解放処理を第一の手段とし、ファイナライザはあくまでIDisposableによる解放が行われなかった場合の保証とします。 確約としての解放手段が必要な場合はファイナライザによらない何らかの管理・解放機構を実装する必要があります。

ファイナライザの実装

C#ではデストラクタ構文~Class()、VBではFinalizeメソッドのオーバーライドによってファイナライザを実装することができます。 C#では、デストラクタ構文によってObject.Finalizeメソッドがオーバーライドされることになります。 (このため、厳密に言えばC#にデストラクタは存在しない)

ファイナライザを実装できるのは、クラスに限られます。 構造体でファイナライザを実装することはできません。 また、静的クラス・モジュール(VB)についても同様で、静的な(staticSharedな)ファイナライザを実装することはできません。

ファイナライザを実装する(Object.Finalizeをオーバーライドする)
using System;

class Resource {
  // コンストラクタ
  public Resource()
  {
    Console.WriteLine("constructed");
  }

  // ファイナライザ(デストラクタ構文によってObject.Finalizeをオーバーライドする)
  ~Resource()
  {
    Console.WriteLine("finalized");
  }
}

class Sample {
  static void AllocateAndRelease()
  {
    new Resource();
  }

  static void Main()
  {
    // オブジェクトを作成して、そのまま放棄する(破棄・解放ではない)
    AllocateAndRelease();

    // ガベージコレクションを行わせ、ファイナライザの呼び出しを待機する
    // (例示のためのものであり、通常は不必要に呼び出すものではない)
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}
ファイナライザを実装する(Object.Finalizeをオーバーライドする)
Imports System

Class Resource
  ' コンストラクタ
  Public Sub New()
    Console.WriteLine("constructed")
  End Sub

  ' ファイナライザ
  Protected Overrides Sub Finalize()
    Console.WriteLine("finalized")
  End Sub
End Class

Class Sample
  Shared Sub AllocateAndRelease()
    Dim res As New Resource()
  End Sub

  Shared Sub Main()
    ' オブジェクトを作成して、そのまま放棄する(破棄・解放ではない)
    AllocateAndRelease()

    ' ガベージコレクションを行わせ、ファイナライザの呼び出しを待機する
    ' (例示のためのものであり、通常は不必要に呼び出すものではない)
    GC.Collect()
    GC.WaitForPendingFinalizers()
  End Sub
End Class
実行結果
constructed
finalized

この例で使用しているGC.Collectメソッド・GC.WaitForPendingFinalizersメソッドについては§.ガベージコレクタへの要求 (GCクラス)を参照してください。

ファイナライザとガベージコレクタ

ファイナライザの呼び出し

ファイナライザはガベージコレクタからによってのみ呼び出されるものであるため、ファイナライザを実装してもそれを直接・明示的に呼び出すことはできません。 また、オブジェクトを明示的に破棄するdelete objのような構文もありません。 VB6以前に存在したSet obj = NothingのようなNothingの代入は、.NETのVBにおいては単に参照の放棄となり、即座にオブジェクトの破棄(およびファイナライザの呼び出し)を指示するものとはなりません。 C#におけるnullの代入も同様です。

ファイナライザを直接・明示的に呼び出すことはできない
using System;

class Resource {
  ~Resource() {}

  public void Free()
  {
    ~Resource(); // error CS1955: 実行不可能なメンバー 'Resource' をメソッドのように使用することはできません。
    Finalize(); // error CS0245: デストラクター と object.Finalize を直接呼び出すことはできません。使用可能であれば IDisposable.Dispose を呼び出してください。
  }
}

class Sample {
  static void Main()
  {
    var res = new Resource();

    res.~Resource(); // error CS1001: 識別子がありません
    res.Finalize(); // error CS0122: 'Resource.~Resource()' はアクセスできない保護レベルになっています

    //delete res; // このような構文は存在しない
    res = null; // 単に参照が放棄されるだけで、オブジェクトの破棄・ファイナライザ呼び出しを直接指示するものとはならない
  }
}
ファイナライザを直接・明示的に呼び出すことはできない
Imports System

Class Resource
  Protected Overrides Sub Finalize()
  End Sub

  Public Sub Free()
    Me.Finalize() ' コンパイルエラーとはならないが、このような呼び出しは避けたほうがよい(IDisposable.Disposeを実装すべき)
  End Sub
End Class

Class Sample
  Shared Sub Main()
    Dim res As New Resource()

    res.Finalize() ' error BC30390: 'Resource.Protected Overrides Sub Finalize()' は 'Protected' であるため、このコンテキストではアクセスできません。

    Set res = Nothing ' error BC30807: 'Let' および 'Set' 代入ステートメントはサポートされなくなりました。
    res = Nothing ' 単に参照が放棄されるだけで、オブジェクトの破棄・ファイナライザ呼び出しを直接指示するものとはならない
  End Sub
End Class

何らかの終了処理・解放処理を行わせたい場合は、ファイナライザを直接呼び出すのではなく、IDisposable.Disposeメソッドあるいは専用のメソッドを用意し、それを呼び出すようにします。

ファイナライザがオーバーライドされたクラスを継承し、派生クラスでもファイナライザを実装するような場合については、§.ファイナライザとIDisposableのデザインパターン (disposeパターン)を参照してください。

ガベージコレクタへの要求 (GCクラス)

GCクラスを使用することで、ガベージコレクタに対する要求や、ガベージコレクタの状態の取得を行うことができます。

一例として、GC.Collectメソッドを使うと、ガベージコレクタに対してガベージコレクションの実施を要求することができます。 メソッドが呼び出される時点で不要と判断されるオブジェクトの回収が行われます。 なお、ガベージコレクションは通常、適切なタイミングで自動的に行われます。

また、GC.WaitForPendingFinalizersメソッドを使うと、不要と判断されているオブジェクトに対するファイナライザ(実行が保留されているファイナライザ)の実行終了を待機することができます。

これらはいずれも、.NETランタイム全域に影響を及ぼし、またパフォーマンスにも影響するため、不用意に呼び出すことは避けるべきです。

GC.Collectメソッドでガベージコレクションの実施を要求する、GC.WaitForPendingFinalizersで保留中のファイナライザ実行を待機する
using System;

class Resource {
  public Resource() => Console.WriteLine("constructed");
  ~Resource() => Console.WriteLine("finalized");
}

class Sample {
  static void Main()
  {
    static void AllocateAndRelease() => new Resource();

    // オブジェクトを作成して、そのまま放棄する(破棄・解放ではない)
    AllocateAndRelease();

    // GC.Collectメソッドを呼び出し、ガベージコレクションの実施を要求する
    Console.WriteLine("GC.Collect");
    GC.Collect();

    // GC.WaitForPendingFinalizersメソッドを呼び出し、
    // 保留されているファイナライザの実行を待機する
    Console.WriteLine("GC.WaitForPendingFinalizers");
    GC.WaitForPendingFinalizers();

    Console.WriteLine("end");
  }
}
GC.Collectメソッドでガベージコレクションの実施を要求する、GC.WaitForPendingFinalizersで保留中のファイナライザ実行を待機する
Imports System

Class Resource
  Public Sub New()
    Console.WriteLine("constructed")
  End Sub

  Protected Overrides Sub Finalize()
    Console.WriteLine("finalized")
  End Sub
End Class

Class Sample
  Shared Sub AllocateAndRelease()
    Dim res As New Resource()
  End Sub

  Shared Sub Main()
    ' オブジェクトを作成して、そのまま放棄する(破棄・解放ではない)
    AllocateAndRelease()

    ' GC.Collectメソッドを呼び出し、ガベージコレクションの実施を要求する
    Console.WriteLine("GC.Collect")
    GC.Collect()

    ' GC.WaitForPendingFinalizersメソッドを呼び出し、
    ' 保留されているファイナライザの実行を待機する
    Console.WriteLine("GC.WaitForPendingFinalizers")
    GC.WaitForPendingFinalizers()

    Console.WriteLine("end")
  End Sub
End Class
実行結果
constructed
GC.Collect
GC.WaitForPendingFinalizers
finalized
end

このほか、ファイナライザ呼び出しの抑止を行うGC.SuppressFinalizeメソッドがあります。 これについては§.破棄されたオブジェクトにおけるファイナライザの呼び出し抑止 (GC.SuppressFinalize)を参照してください。 また、一度抑止したファイナライザ呼び出しを再度要求する場合はGC.ReRegisterForFinalizeメソッドを使います。

シャットダウン時におけるファイナライザ呼び出し

.NET Coreおよび.NET 5以降では、アプリケーションドメインのシャットダウン中のファイナライザ呼び出しは行われません。 このため、オブジェクトによってはファイナライザ呼び出しが行われないまま.NETプロセスが正常終了することになります。 .NET Frameworkでは、シャットダウン中のファイナライザ呼び出しが行われることが保証されています。 (ファイナライザ呼び出しの抑止が行われている場合は除く)

ガベージコレクタによるファイナライザ呼び出し動作の違いについては、Object.Finalizeメソッドのドキュメントにおいて次のように記載されています。 (引用に際して下線部を強調、太字部分は原文ママ)

How finalization works

(中略)

The garbage collector then calls the Finalize method automatically under the following conditions:

  • After the garbage collector has discovered that an object is inaccessible, unless the object has been exempted from finalization by a call to the GC.SuppressFinalize method.
  • On .NET Framework only, during shutdown of an application domain, unless the object is exempt from finalization. During shutdown, even objects that are still accessible are finalized.
Object.Finalize Method (System) | Microsoft Docs

.NET/.NET Core/.NET Framework以外のランタイムとして、version 6.12時点でのMonoランタイムでは.NET Frameworkと同様の動作で、ファイナライザ呼び出しが保証されています。

例として以下のコードで動作を確認すると次のようになります。 .NET Core/.NET 5以降で実行した場合は、プロセス終了までの間にファイナライザの呼び出しが行われません。 なお、GC.WaitForPendingFinalizersでファイナライザ呼び出しを待機する場合(無効化してあるコード)は、ファイナライザ呼び出しが行われます。

各.NETランタイムにおけるプロセス終了時のファイナライザ呼び出しの違い
using System;

class Resource {
  public Resource() => Console.WriteLine("constructed");
  ~Resource() => Console.WriteLine("finalized");
}

class Sample {
  static void Main()
  {
    Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);

    static void AllocateAndRelease() => new Resource();

    AllocateAndRelease();

#if false
    GC.Collect();
    GC.WaitForPendingFinalizers();
#endif
  }
}
各.NETランタイムにおけるプロセス終了時のファイナライザ呼び出しの違い
Imports System

Class Resource
  Public Sub New()
    Console.WriteLine("constructed")
  End Sub

  Protected Overrides Sub Finalize()
    Console.WriteLine("finalized")
  End Sub
End Class

Class Sample
  Shared Sub AllocateAndRelease()
    Dim res As New Resource()
  End Sub

  Shared Sub Main()
    Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription)

    AllocateAndRelease()

#If False
    GC.Collect()
    GC.WaitForPendingFinalizers()
#End If
  End Sub
End Class
.NET 5 RC1での実行結果
.NET 5.0.0-rc.1.20451.14
constructed
.NET Core 3.1での実行結果
.NET Core 3.1.8
constructed
.NET Framework 4.8での実行結果
.NET Framework 4.8.4250.0
constructed
finalized
Mono 6.12での実行結果
Mono 6.12.0.90 (tarball Fri Sep  4 14:02:38 UTC 2020)
constructed
finalized

GC.Collectメソッド・GC.WaitForPendingFinalizersメソッドについては§.ガベージコレクタへの要求 (GCクラス)を参照してください。


このように.NET Core/.NET 5以降ではファイナライザの実行が保証されない場合があるため、代替手段としてAssemblyLoadContext.Unloadingイベントが追加されています。 実装したい終了処理がクリティカルな場合は、このイベントを捕捉し、そこで終了処理を行うようにする必要があります。 特に、プロセス終了とともに自動的に解放されないようなリソースを扱う場合は、ファイナライザではなくAssemblyLoadContext.Unloadingイベントで解放処理を行う必要があります。

AssemblyLoadContext.Unloadingイベントを捕捉して終了処理を行う .NET Standard 1.6/.NET Core 2.0
using System;
using System.Runtime.Loader;

class Sample {
  static void Main()
  {
    // AssemblyLoadContext.Unloadingイベントで必要な終了処理を行う
    AssemblyLoadContext.GetLoadContext(typeof(Sample).Assembly).Unloading += (ctx) => Console.WriteLine("unload");

    Console.WriteLine("end");
  }
}
AssemblyLoadContext.Unloadingイベントを捕捉して終了処理を行う .NET Standard 1.6/.NET Core 2.0
Imports System
Imports System.Runtime.Loader

Class Sample
  Shared Sub Main()
    ' AssemblyLoadContext.Unloadingイベントで必要な終了処理を行う
    AddHandler AssemblyLoadContext.GetLoadContext(GetType(Sample).Assembly).Unloading, Sub(ByVal ctx As AssemblyLoadContext)
      Console.WriteLine("unload")
    End Sub

    Console.WriteLine("end")
  End Sub
End Class

ファイナライザとIDisposableのデザインパターン (disposeパターン)

.NETでは、IDisposableインターフェイスファイナライザを実装した破棄可能なクラスのデザインパターン・disposeパターンが提示されています。 これについては、以下のページで解説されています。

継承を許可しないクラスでのdisposeパターン

以下は、disposeパターンにしたがって、IDisposableインターフェイスとファイナライザを実装するクラスResourceを作成する例です。 このクラスでは継承を許可しないものとします。

disposeパターンに従った継承を許可しないクラスの実装例
using System;
using System.IO;
using System.Runtime.InteropServices;

// 使用後に解放されるべきリソースを扱うクラス
sealed class Resource : IDisposable {
  private bool disposed = false; // リソースが破棄(解放)されていることを表すフラグ
  private IntPtr ptr = Marshal.AllocHGlobal(4); // コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト
  private Stream stream = Stream.Null; // コンストラクタ等で確保されるマネージリソースを想定したオブジェクト

  // ファイナライザ
  ~Resource()
  {
    // アンマネージリソースのみを破棄させる
    Dispose(false);
  }

  // IDisposable.Disposeの実装
  public void Dispose()
  {
    // アンマネージリソースと、マネージリソースの両方を破棄させる
    Dispose(true);
    // すべてのリソースが破棄されているため、以後ファイナライザの実行は不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(this);
  }

  // リソースの解放処理を行うためのメソッド
  private void Dispose(bool disposing)
  {
    // 既にリソースが破棄されている場合は何もしない
    if (disposed) return;

    // アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
    if (disposing) {
      // 破棄されていないマネージリソースの解放処理を行う
      if (stream != null) {
        stream.Close(); // Disposeメソッド、あるいはそれを呼び出すメソッドを呼び出して解放する
        stream = null;
      }
    }

    // 破棄されていないアンマネージリソースの解放処理を行う
    if (ptr != IntPtr.Zero) {
      Marshal.FreeHGlobal(ptr);
      ptr = IntPtr.Zero;
    }

    // リソースは破棄されている
    disposed = true;
  }

  // 既にリソースが破棄されているかチェックして、ObjectDisposedExceptionをスローするためのメソッド
  private void ThrowIfDisposed()
  {
    if (disposed) throw new ObjectDisposedException(GetType().FullName);
  }

  // 保持しているリソースを使って何らかの操作を行うメソッドを想定
  public void UseResource()
  {
    // 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed();

    // 以降、リソースが利用可能な場合はそれを使った操作を行う
  }
}
disposeパターンに従った継承を許可しないクラスの実装例
Imports System
Imports System.IO
Imports System.Runtime.InteropServices

' 使用後に解放されるべきリソースを扱うクラス
NotInheritable Class Resource
  Implements IDisposable

  Private disposed As Boolean = False ' リソースが破棄(解放)されていることを表すフラグ
  Private ptr As IntPtr = Marshal.AllocHGlobal(4) ' コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト
  Private stream As Stream = Stream.Null ' コンストラクタ等で確保されるマネージリソースを想定したオブジェクト

  ' ファイナライザ
  Protected Overrides Sub Finalize()
    ' アンマネージリソースのみを破棄させる
    Dispose(false)
  End Sub

  ' IDisposable.Disposeの実装
  Public Overloads Sub Dispose() Implements IDisposable.Dispose
    ' アンマネージリソースと、マネージリソースの両方を破棄させる
    Dispose(true)
    ' すべてのリソースが破棄されているため、以後ファイナライザの実行は不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(Me)
  End Sub

  ' リソースの解放処理を行うためのメソッド
  Private Overloads Sub Dispose(ByVal disposing As Boolean)
    ' 既にリソースが破棄されている場合は何もしない
    If disposed Then Return

    ' アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
    If disposing Then
      ' 破棄されていないマネージリソースの解放処理を行う
      If Not stream Is Nothing Then
        stream.Close() ' Disposeメソッド、あるいはそれを呼び出すメソッドを呼び出して解放する
        stream = Nothing
      End If
    End If

    ' 破棄されていないアンマネージリソースの解放処理を行う
    If ptr <> IntPtr.Zero Then
      Marshal.FreeHGlobal(ptr)
      ptr = IntPtr.Zero
    End If

    ' リソースは破棄されている
    disposed = True
  End Sub

  ' 既にリソースが破棄されているかチェックして、ObjectDisposedExceptionをスローするためのメソッド
  Private Sub ThrowIfDisposed()
    If disposed Then Throw New ObjectDisposedException(Me.GetType().FullName)
  End Sub

  ' 保持しているリソースを使って何らかの操作を行うメソッドを想定
  Public Sub UseResource()
    ' 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed()

    ' 以降、リソースが利用可能な場合はそれを使った操作を行う
  End Sub
End Class

ここでは例示のためハンドル・ポインタをそのまま扱っていますが、可能ならSafeHandleでラップするようにします。 このほか、個々の事項については以下を参照してください。

継承を許可したクラスでのdisposeパターン

以下は、disposeパターンにしたがって、IDisposableインターフェイスとファイナライザを実装するクラスResourceBaseと、それを継承したクラスExtendedResourceを作成する例です。

disposeパターンに従ったクラスの実装と継承の例
using System;
using System.IO;
using System.Runtime.InteropServices;

// 使用後に解放されるべきリソースを扱うクラス
class ResourceBase : IDisposable {
  private bool disposed = false; // リソースが破棄(解放)されていることを表すフラグ
  private IntPtr ptr = Marshal.AllocHGlobal(4); // コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト

  // ファイナライザ
  ~ResourceBase()
  {
    // アンマネージリソースのみを破棄させる
    Dispose(false);
  }

  // IDisposable.Disposeの実装
  public void Dispose()
  {
    // アンマネージリソースと、マネージリソースの両方を破棄させる
    Dispose(true);
    // すべてのリソースが破棄されているため、以後ファイナライザの実行は不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(this);
  }

  // リソースの解放処理を行うためのメソッド
  protected virtual void Dispose(bool disposing)
  {
    // 既にリソースが破棄されている場合は何もしない
    if (disposed) return;

    // アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
    if (disposing) {
      // このクラスはマネージリソースを持たないので、ここですべきことは無い
    }

    // 破棄されていないアンマネージリソースの解放処理を行う
    if (ptr != IntPtr.Zero) {
      Marshal.FreeHGlobal(ptr);
      ptr = IntPtr.Zero;
    }

    // リソースは破棄されている
    disposed = true;
  }

  // 既にリソースが破棄されているかチェックして、ObjectDisposedExceptionをスローするためのメソッド
  protected void ThrowIfDisposed()
  {
    if (disposed) throw new ObjectDisposedException(GetType().FullName);
  }

  // 保持しているリソースを使って何らかの操作を行うメソッドを想定
  public virtual void UseResource()
  {
    // 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed();

    // リソースが利用可能な場合はそれを使った操作を行う
  }
}

// IDisposableとファイナライザが実装された型を継承したクラス
class ExtendedResource : ResourceBase {
  private IntPtr ptr = Marshal.AllocHGlobal(8); // コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト
  private Stream stream = Stream.Null; // コンストラクタ等で確保されるマネージリソースを想定したオブジェクト

  // 基底クラスのリソース解放処理をオーバーライド
  // (ファイナライザ・IDisposable.Disposeメソッドは直接オーバーライドせず、このメソッドで実装する)
  protected override void Dispose(bool disposing)
  {
    try {
      // アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
      if (disposing) {
        // 破棄されていないマネージリソースの解放処理を行う
        if (stream != null) {
          stream.Close(); // Disposeメソッド、あるいはそれを呼び出すメソッドを呼び出して解放する
          stream = null;
        }
      }

      // 破棄されていないアンマネージリソースの解放処理を行う
      if (ptr != IntPtr.Zero) {
        Marshal.FreeHGlobal(ptr);
        ptr = IntPtr.Zero;
      }
    }
    finally {
      // 例外が発生しても基底クラスのDisposeメソッドを確実に呼び出すようにする
      base.Dispose(disposing);
    }
  }

  public override void UseResource()
  {
    // 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed();

    // 以降、リソースが利用可能な場合はそれを使った操作を行う
  }
}
disposeパターンに従ったクラスの実装と継承の例
Imports System
Imports System.IO
Imports System.Runtime.InteropServices

' 使用後に解放されるべきリソースを扱うクラス
Class ResourceBase
  Implements IDisposable

  Private disposed As Boolean = False ' リソースが破棄(解放)されていることを表すフラグ
  Private ptr As IntPtr = Marshal.AllocHGlobal(4) ' コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト

  ' ファイナライザ
  Protected Overrides Sub Finalize()
    ' アンマネージリソースのみを破棄させる
    Dispose(false)
  End Sub

  ' IDisposable.Disposeの実装
  Public Overloads Sub Dispose() Implements IDisposable.Dispose
    ' アンマネージリソースと、マネージリソースの両方を破棄させる
    Dispose(true)
    ' すべてのリソースが破棄されているため、以後ファイナライザの実行は不要であることをガベージコレクタに通知する
    GC.SuppressFinalize(Me)
  End Sub

  ' リソースの解放処理を行うためのメソッド
  Protected Overridable Overloads Sub Dispose(ByVal disposing As Boolean)
    ' 既にリソースが破棄されている場合は何もしない
    If disposed Then Return

    ' アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
    If disposing Then
      ' このクラスはマネージリソースを持たないので、ここですべきことは無い
    End If

    ' 破棄されていないアンマネージリソースの解放処理を行う
    If ptr <> IntPtr.Zero Then
      Marshal.FreeHGlobal(ptr)
      ptr = IntPtr.Zero
    End If

    ' リソースは破棄されている
    disposed = True
  End Sub

  ' 既にリソースが破棄されているかチェックして、ObjectDisposedExceptionをスローするためのメソッド
  Protected Sub ThrowIfDisposed()
    If disposed Then Throw New ObjectDisposedException(Me.GetType().FullName)
  End Sub

  ' 保持しているリソースを使って何らかの操作を行うメソッドを想定
  Public Overridable Sub UseResource()
    ' 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed()

    ' 以降、リソースが利用可能な場合はそれを使った操作を行う
  End Sub
End Class

' IDisposableとファイナライザが実装された型を継承したクラス
Class ExtendedResource
  Inherits ResourceBase

  Private ptr As IntPtr = Marshal.AllocHGlobal(8) ' コンストラクタ等で確保されるアンマネージリソースを想定したオブジェクト
  Private stream As Stream = Stream.Null ' コンストラクタ等で確保されるマネージリソースを想定したオブジェクト

  ' 基底クラスのリソース解放処理をオーバーライド
  ' (ファイナライザ・IDisposable.Disposeメソッドは直接オーバーライドせず、このメソッドで実装する)
  Protected Overrides Sub Dispose(ByVal disposing As Boolean)
    Try
      ' アンマネージリソースと、マネージリソースの両方の破棄が要求された場合
      If disposing Then
        ' 破棄されていないマネージリソースの解放処理を行う
        If Not stream Is Nothing Then
          stream.Close() ' Disposeメソッド、あるいはそれを呼び出すメソッドを呼び出して解放する
          stream = Nothing
        End If
      End If

      ' 破棄されていないアンマネージリソースの解放処理を行う
      If ptr <> IntPtr.Zero Then
        Marshal.FreeHGlobal(ptr)
        ptr = IntPtr.Zero
      End If
    Finally
      ' 例外が発生しても基底クラスのDisposeメソッドを確実に呼び出すようにする
      MyBase.Dispose(disposing)
    End Try
  End Sub

  Public Overrides Sub UseResource()
    ' 既にリソースが破棄されているかチェックし、破棄されていればObjectDisposedExceptionをスローする
    ThrowIfDisposed()

    ' 以降、リソースが利用可能な場合はそれを使った操作を行う
  End Sub
End Class

ここでは例示のためハンドル・ポインタをそのまま扱っていますが、可能ならSafeHandleでラップするようにします。 このほか、個々の事項については以下を参照してください。