確保したリソースを忘れずに解放するには?[C#/VB]:.NET TIPS
プログラム実行時に確保したリソースは忘れずに解放する必要がある。usingステートメント/Disposeパターンを使って、これを確実に行う方法を紹介する。
リソースとは、メモリやファイル、あるいはデバイスコンテキストやウィンドウハンドルなどといった、プログラムの外にあるおよそあらゆる全てのものだ。それらをプログラムで利用するために確保した場合は、(一部の例外を除き)プログラムで解放する必要がある。解放しないと、プログラムのメモリ使用量は増大し続けるし、排他的に確保しているリソースは他から利用できないままとなる。なお、.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
確保したリソースを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ステートメントのブロックを抜けるときに自動的に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"));
}
このように書けば、インデントが深くならずに済む。
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
例外に対処するため複雑なコードになってしまう。
なお、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#、下:VB)
Visual Studio 2015での例だ。
クラスを宣言し、継承するインタフェースとして「IDisposable」と書いたら、そこにマウスカーソルをホバーさせるかキー入力キャレットを置くと、電球のアイコンが表示される。そこで電球のアイコンをクリックするかCtrl+.キーを押すと、画像のようなメニューが出てくるので、Disposeパターンの実装を選ぶ。
自動生成されたDisposeパターンのスケルトンは、次の画像のようになる([Dispose パターンを使ってインターフェイスを実装します]を選択した場合のもの)。
上の画像の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メソッドを持っているオブジェクト(マネージリソース)の解放と、.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後
上のコードの出力を見ると、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外
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を重複して呼び出しても何も起きない
このようなコードを書くべきではないが、もしも間違えて書いてしまっても、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パターンがきちんと実装されていれば、ガベージコレクタ機構によってリソースは解放される。
ただし、オブジェクトの存在時間が長くなる分だけ、解放される前にアプリが異常終了してしまって解放されなくなるという可能性も高くなる。
上の出力例を見ると、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]
Copyright© Digital Advantage Corp. All Rights Reserved.