連載
» 2017年10月18日 05時00分 公開

.NET TIPS:確保したリソースを忘れずに解放するには?[C#/VB]

プログラム実行時に確保したリソースは忘れずに解放する必要がある。usingステートメント/Disposeパターンを使って、これを確実に行う方法を紹介する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 リソースとは、メモリやファイル、あるいはデバイスコンテキストやウィンドウハンドルなどといった、プログラムの外にあるおよそあらゆる全てのものだ。それらをプログラムで利用するために確保した場合は、(一部の例外を除き)プログラムで解放する必要がある。解放しないと、プログラムのメモリ使用量は増大し続けるし、排他的に確保しているリソースは他から利用できないままとなる。なお、.NET Frameworkが管理しているメモリは例外だ。使われなくなったオブジェクトに割り当てられているメモリは、ガベージコレクタ機構によって自動的に解放される。

 確保したリソースを忘れずに解放するにはusingステートメントDisposeパターンを使えばよい。本稿では、それらの使い方を解説するとともに、Disposeメソッドの振る舞いを調べてみる。

 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、usingステートメントとDisposeパターンは.NET Frameworkのバージョンによらず利用できるが、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2015以降が必要である。また、サンプルコードはコンソールアプリの一部であり、以下の名前空間の宣言が必要となる。

using System;
using System.IO;
using System.Text;
using static System.Console;

Imports System.Console
Imports System.IO
Imports System.Text

本稿のサンプルコードに必要な名前空間の宣言(上:C#、下:VB)

確保したリソースをusingステートメントで解放するには?

 .NET FrameworkのクラスでDisposeメソッドのあるものは、解放すべきリソースを持っている。Disposeメソッドを呼び出すと、そのオブジェクトが確保しているリソースが解放される。そのようなクラスは、usingステートメントを使うことで確実にリソースを解放できる(次のコード)。

// ファイルを開く
using (FileStream fs = File.OpenWrite(".\\test.txt"))
{
  // TextWriterオブジェクトを得る
  using (TextWriter writer = new StreamWriter(fs, Encoding.UTF8))
  {
    // TextWriterを使って、文字列をファイルに書き込む
    writer.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));

  } // ここでTextWriterオブジェクトのDisposeメソッドが呼び出される
} // ここでFileStreamオブジェクトのDisposeメソッドが呼び出される

' ファイルを開く
Using fs As FileStream = File.OpenWrite(".\\test.txt")
  ' TextWriterオブジェクトを得る
  Using writer As TextWriter = New StreamWriter(fs, Encoding.UTF8)

    ' TextWriterを使って、文字列をファイルに書き込む
    writer.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"))

  End Using ' ここでTextWriterオブジェクトのDisposeメソッドが呼び出される
End Using ' ここでFileStreamオブジェクトのDisposeメソッドが呼び出される

usingステートメントを使ってリソースを解放する例(上:C#、下:VB)
usingステートメントのブロックを抜けるときに自動的にDisposeメソッドが呼び出される(後ほど、実際に確認する)。そのため、Disposeメソッドを呼び出すコードは不要である。
なお、FileStreamクラス/TextWriterクラス(ともにSystem.IO名前空間)は、Disposeメソッドを呼び出すとクローズされるので、Closeメソッドも呼び出さなくてよい。ちなみに、UWPアプリ用のAPI(例えばFileOutputStreamクラスなど)では、Closeメソッドは廃止されている。

 なお、C#では、複数のusingステートメントを連続させるときに、途中の中かっこを省略できる(次のコード)。

using (FileStream fs = File.OpenWrite(".\\test.txt"))
using (TextWriter writer = new StreamWriter(fs, Encoding.UTF8))
{
  writer.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));
}

C#ではusingステートメントが連続するときに途中の中かっこを省略できる(C#)
このように書けば、インデントが深くならずに済む。

 usingステートメントは、そのブロックから抜け出すときにDisposeメソッドを呼び出してくれる。たとえブロック内で例外が発生したとしてもである。

 同等のコードをusingステートメントを使わずに書くと、次のコードのようになる。例外が発生してもDisposeメソッドを呼び出せるように、try〜finallyステートメントを使っている。

FileStream fs = null;
TextWriter writer = null;
try
{
  // ファイルを開く
  fs = File.OpenWrite(".\\test.txt");
  // TextWriterオブジェクトを得る
  writer = new StreamWriter(fs, Encoding.UTF8);
  // TextWriterを使って、文字列をファイルに書き込む
  writer.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"));
}
finally
{
  // TextWriterを閉じて書き込みを完了させ、TextWriterオブジェクトの持つリソースを破棄する
  writer?.Dispose(); // Disposeを忘れると、ファイルに書き込まれない
  // ファイルを閉じてFileStreamオブジェクトの持つリソースを破棄する
  fs?.Dispose();
}

Dim fs As FileStream = Nothing
Dim writer As TextWriter = Nothing
Try
  ' ファイルを開く
  fs = File.OpenWrite(".\\test.txt")
  ' TextWriterオブジェクトを得る
  writer = New StreamWriter(fs, Encoding.UTF8)
  ' TextWriterを使って、文字列をファイルに書き込む
  writer.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff"))

Finally
  ' TextWriterを閉じて書き込みを完了させ、TextWriterオブジェクトの持つリソースを破棄する
  writer?.Dispose() ' Disposeを忘れると、ファイルに書き込まれない
  ' ファイルを閉じてFileStreamオブジェクトの持つリソースを破棄する
  fs?.Dispose()
End Try

usingステートメントを使わずにリソースを解放する例(上:C#、下:VB)
例外に対処するため複雑なコードになってしまう。
なお、Finallyブロック内に登場している「?」記号は、Null条件演算子である。オブジェクトが生成される前に例外が出た場合を考慮して、オブジェクトがnullのときはDisposeメソッドを呼び出さないようにしている。Visual Studio 2015以前では、nullチェックを行うif文に書き換えてほしい。

メンバ変数に確保したリソースをDisposeパターンで解放するには?

 確保したリソースを繰り返し利用するためにクラスのメンバ変数に保持することがある。この場合はusingステートメントが使えない。確実にリソースを解放するにはどうしたらよいだろうか?

 そのようなクラスには、IDisposableインタフェース(System名前空間)を実装すればよい。ただし「お約束」があり、Disposeパターンと呼ばれている。なお、IDisposableインタフェースを実装したクラスは、usingステートメントで利用できる。

 Disposeパターンは、そのスケルトンコードを最近のVisual Studioでは自動生成できる。例えばVisual Studio 2015では、「IDisposable」と書いたらそこで「電球」アイコンをクリックすると、Disposeパターンの生成を選択できる(次の画像)。

Disposeパターンのスケルトンを自動生成する(C#)
Disposeパターンのスケルトンを自動生成する(VB) Disposeパターンのスケルトンを自動生成する(上:C#、下:VB)
Visual Studio 2015での例だ。
クラスを宣言し、継承するインタフェースとして「IDisposable」と書いたら、そこにマウスカーソルをホバーさせるかキー入力キャレットを置くと、電球のアイコンが表示される。そこで電球のアイコンをクリックするかCtrl+.キーを押すと、画像のようなメニューが出てくるので、Disposeパターンの実装を選ぶ。

 自動生成されたDisposeパターンのスケルトンは、次の画像のようになる([Dispose パターンを使ってインターフェイスを実装します]を選択した場合のもの)。

自動生成されたDisposeパターンのスケルトン(C#)
自動生成されたDisposeパターンのスケルトン(VB) 自動生成されたDisposeパターンのスケルトン(上:C#、下:VB)
Visual Studio 2015での例である。

 上の画像のDisposeパターンのスケルトンで、コメントに「TODO」とある部分を実装していけばよい。ここではリソースを解放する代わりに、実験用としてコンソールに文字列を出力する例を示そう(次のコード)。

using System;
using static System.Console;

public class DisposableSample : IDisposable
{
  #region IDisposable Support
  private bool disposedValue = false; // 重複する呼び出しを検出するには

  protected virtual void Dispose(bool disposing)
  {
    if (!disposedValue)
    {
      if (disposing)
      {
        // マネージオブジェクト(.NET Frameworkのオブジェクト)を
        // メンバ変数に保持している場合は、ここでDisposeする。
        WriteLine("■.NET Frameworkのオブジェクトを破棄しました。");
      }

      // アンマネージリソース(.NET Framework外のオブジェクト)を
      // メンバ変数に保持している場合は、ここで解放する。
      WriteLine("■アンマネージリソースを解放しました。");

      disposedValue = true;
    }
  }

  // TODO: 上の Dispose(bool disposing) に
  // アンマネージ リソースを解放するコードが含まれる場合にのみ、
  // ファイナライザーをオーバーライドします。
  ~DisposableSample()
  {
    // このコードを変更しないでください。
    // クリーンアップ コードを上の Dispose(bool disposing) に記述します。
    Dispose(false);
  }

  // このコードは、破棄可能なパターンを正しく実装できるように追加されました。
  public void Dispose()
  {
    // このコードを変更しないでください。
    // クリーンアップ コードを上の Dispose(bool disposing) に記述します。
    Dispose(true);

    // TODO: 上のファイナライザーがオーバーライドされる場合は、
    // 次の行のコメントを解除してください。
    GC.SuppressFinalize(this);
    // ファイナライザーの実行コストは高いので、
    // Disposeした後はファイナライザーが呼び出されないようにする。
  }
  #endregion
}

Imports System.Console

Public Class DisposableSample
  Implements IDisposable

#Region "IDisposable Support"
  Private disposedValue As Boolean ' 重複する呼び出しを検出するには

  ' IDisposable
  Protected Overridable Sub Dispose(disposing As Boolean)
    If Not disposedValue Then
      If disposing Then
        ' マネージオブジェクト(.NET Frameworkのオブジェクト)を
        ' メンバ変数に保持している場合は、ここでDisposeする。
        WriteLine("■.NET Frameworkのオブジェクトを破棄しました。")
      End If

      ' アンマネージリソース(.NET Framework外のオブジェクト)を
      ' メンバ変数に保持している場合は、ここで解放する。
      WriteLine("■アンマネージリソースを解放しました。")

    End If
    disposedValue = True
  End Sub

  ' TODO: 上の Dispose(disposing As Boolean) に
  ' アンマネージ リソースを解放するコードが含まれる場合にのみ
  ' Finalize() をオーバーライドします。
  Protected Overrides Sub Finalize()
    ' このコードを変更しないでください。
    ' クリーンアップ コードを上の Dispose(disposing As Boolean) に記述します。
    Dispose(False)
    MyBase.Finalize()
  End Sub

  ' このコードは、破棄可能なパターンを正しく実装できるように
  ' Visual Basic によって追加されました。
  Public Sub Dispose() Implements IDisposable.Dispose
    ' このコードを変更しないでください。
    ' クリーンアップ コードを上の Dispose(disposing As Boolean) に記述します。
    Dispose(True)
    ' TODO: 上の Finalize() がオーバーライドされている場合は、
    ' 次の行のコメントを解除してください。
    GC.SuppressFinalize(Me)
    ' ファイナライザーの実行コストは高いので、
    ' Disposeした後はファイナライザーが呼び出されないようにする。
  End Sub
#End Region
End Class

実験用にDisposeパターンを実装したクラス(上:C#、下:VB)
リソースを解放する代わりに、コンソールに文字列を出力するようにした(太字の部分)。実際にはこの部分で、Disposeメソッドを持っているオブジェクト(マネージリソース)の解放と、.NET Framework外のアンマネージリソース(例えば、Win32 APIを呼び出して取得したハンドルなど)の解放を行う。
ずいぶん複雑なコードだが、自動生成されたスケルトンのコメントに従って何カ所かコメントを外し、太字にした部分のコードを記述しただけである。
なお、このDisposableSampleクラスは、以降のサンプルコードで利用する。

 上のDisposableSampleクラスを使ってみよう。まず、明示的にDisposeメソッドを呼び出してみる(次のコード)。

var ds = new DisposableSample();
WriteLine("オブジェクト存在中");
ds.Dispose();
WriteLine("Dispose後");
// 出力:
// オブジェクト存在中
// ■.NET Frameworkのオブジェクトを破棄しました。
// ■アンマネージリソースを解放しました。
// Dispose後

Dim ds = New DisposableSample()
WriteLine("オブジェクト存在中")
ds.Dispose()
WriteLine("Dispose後")
' 出力:
' オブジェクト存在中
' ■.NET Frameworkのオブジェクトを破棄しました。
' ■アンマネージリソースを解放しました。
' Dispose後

DisposableSampleクラスのDisposeメソッドを明示的に呼び出す(上:C#、下:VB)

 上のコードの出力を見ると、Disposeメソッドの呼び出しによって、マネージリソースとアンマネージリソースが順に解放されている。

 次は、usingステートメントを使ってみる(次のコード)。上のコードと同じようにリソースの解放が実行されている。確かに、usingステートメントによって自動的にDisposeメソッドが呼び出されているのである。

using (var ds = new DisposableSample())
{
  WriteLine("using内");
}
WriteLine("using外");
// 出力:
// using内
// ■.NET Frameworkのオブジェクトを破棄しました。
// ■アンマネージリソースを解放しました。
// using外

Using ds = New DisposableSample()
  WriteLine("using内")
End Using
WriteLine("using外")
' 出力:
' using内
' ■.NET Frameworkのオブジェクトを破棄しました。
' ■アンマネージリソースを解放しました。
' using外

DisposableSampleクラスをusingステートメントで使う(上:C#、下:VB)

Disposeメソッドを重複して呼び出すとどうなる?

 Disposeメソッドの呼び出し回数を間違えたらどうなってしまうだろうか? もちろん正しく1回だけDisposeメソッドを呼び出すべきではあるが、実際どうなるのか前述のDisposableSampleクラスを使って確かめてみよう。

 まずは、繰り返してDisposeメソッドを呼び出してしまった場合だ(次のコード)。

var ds = new DisposableSample();
WriteLine("オブジェクト存在中");
ds.Dispose();
WriteLine("Dispose後");
// 出力:
// オブジェクト存在中
// ■.NET Frameworkのオブジェクトを破棄しました。
// ■アンマネージリソースを解放しました。
// Dispose後

Console.WriteLine("もう一度Disposeを呼び出す");
ds.Dispose();
WriteLine("Disposeを重複して呼び出しても何も起きない");
// 出力:
// もう一度Disposeを呼び出す
// Disposeを重複して呼び出しても何も起きない

Dim ds = New DisposableSample()
WriteLine("オブジェクト存在中")
ds.Dispose()
WriteLine("Dispose後")
' 出力:
' オブジェクト存在中
' ■.NET Frameworkのオブジェクトを破棄しました。
' ■アンマネージリソースを解放しました。
' Dispose後

WriteLine("もう一度Disposeを呼び出す")
ds.Dispose()
WriteLine("Disposeを重複して呼び出しても何も起きない")
' 出力:
' もう一度Disposeを呼び出す
' Disposeを重複して呼び出しても何も起きない

DisposableSampleクラスのDisposeメソッドを繰り返して呼び出す(上:C#、下:VB)
このようなコードを書くべきではないが、もしも間違えて書いてしまっても、Disposeパターンがきちんと実装されたオブジェクトならば問題ない。

 Disposeパターンが正しく実装されているオブジェクトなら、Disposeメソッドを重複して呼び出してしまっても問題ない。

Disposeメソッドを呼び出し忘れるとどうなる?

 次に、Disposeメソッドの呼び出しを忘れてしまった場合だ(次のコード)。オブジェクトはいつか参照されなくなり、ガベージコレクタによっていつかは破棄される。このコードではそれをシミュレートするために、変数にnullをセットしてからガベージコレクタを強制的に動かしている。

var ds = new DisposableSample();
WriteLine("オブジェクト存在中");

ds = null;
WriteLine("オブジェクトへの参照がなくなった");

GC.Collect();
WriteLine("ガベージコレクション実行");

// 出力例:
// オブジェクト存在中
// オブジェクトへの参照がなくなった
// ガベージコレクション実行
// ■アンマネージリソースを解放しました。
// (注意)GCは非同期で実行されるため、最後の2行は入れ替わることがある

Dim ds = New DisposableSample()
WriteLine("オブジェクト存在中")

ds = Nothing
WriteLine("オブジェクトへの参照がなくなった")

GC.Collect()
WriteLine("ガベージコレクション実行")

' 出力例:
' オブジェクト存在中
' オブジェクトへの参照がなくなった
' ガベージコレクション実行
' ■アンマネージリソースを解放しました。
' (注意)GCは非同期で実行されるため、最後の2行は入れ替わることがある

Disposeメソッドを呼び出し忘れた場合のシミュレーション(上:C#、下:VB)
呼び出し忘れても、Disposeパターンがきちんと実装されていれば、ガベージコレクタ機構によってリソースは解放される。
ただし、オブジェクトの存在時間が長くなる分だけ、解放される前にアプリが異常終了してしまって解放されなくなるという可能性も高くなる。

 上の出力例を見ると、Disposeメソッドの呼び出しを忘れても、(Disposeパターンがきちんと実装されていれば)アンマネージリソースは解放される。マネージリソースはDisposeパターン内では解放されていないものの、マネージリソースを持つオブジェクトへの参照はなくなっているので、ガベージコレクタによって自動的に解放される。すなわち、Disposeメソッドの呼び出しを忘れても、リソースは解放されるのだ。

 では、Disposeメソッドを呼び出さなくても問題はないのかというと、そんなことはない。リソースを抱えたままでオブジェクトが生存し続ける時間が長くなってしまうのだ。それはひょっとするとアプリ終了時まで続くかもしれない。その間、排他的に確保したリソースは他から利用できない。また、ガベージコレクタによって破棄される前にアプリが異常終了してしまい、リソースが解放されなくなるという可能性も時間とともに増えるのだ。それと、Disposeメソッドを呼び出さなかった場合はファイナライザーよって解放処理が実行されるのだが、ファイナライザーを呼び出す処理自体のコストは高い(時間がかかる)。ガベージコレクションが実行されたときの負荷が上がるということである。

 なお、Disposeパターンがきちんと実装されていない場合は、Disposeメソッドを重複して呼び出したり、呼び出しを忘れたりすると、不具合を起こす可能性があることにも注意してほしい。そのようなオブジェクトでは、2回目のDisposeメソッド呼び出しで例外が出たり、ガベージコレクタによって破棄されるときにアンマネージリソースが解放されなかったりするのだ。

まとめ

  • リソースは不要になった時点で直ちに解放しよう
  • マネージリソースを確保して利用してすぐに解放するときは、usingステートメント
  • 確保したリソースをメンバ変数に置いて何回も利用するときは、Disposeパターン
  • Disposeメソッドを重複して呼び出しても、呼び出し忘れても、Disposeパターンがきちんと実装してあればリソースは解放される(ただし、これはあくまで保険だと考えてほしい)

利用可能バージョン:.NET Framework 1.1以降
カテゴリ:C# 処理対象:オブジェクト
カテゴリ:Visual Basic 処理対象:オブジェクト
使用ライブラリ:IDisposableインターフェース(System名前空間)
関連TIPS:確実な終了処理を行うには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。