MCP試験 70-483 Programming in C# の学習材料。
目次はこちら
2-6 オブジェクトのライフサイクルを管理する
リソースリークを起こさないプログラムを書く。
ガベージコレクション
マネージリソースとアンマネージリソース
.NET にはガベージコレクション(GC)機能がある。GC は使われなくなった(どこからも参照されなくなった)オブジェクトを自動的に破棄・開放してくれる。そのためプログラマは通常、リソースの確保・開放を意識する必要はない。
外部プログラムと相互運用するために、メモリや Windows オブジェクト(ハンドル)などのリソースを明示的に確保することもできる。ただし、プログラマが明示的に確保したリソースはプログラマ自身が開放する必要がある。もし開放を忘れてしまうとそのリソースはリーク(leak)し、プロセスが終了するまで残り続けてしまう。
このプログラマが明示的に確保したリソースのことをアンマネージリソース(Unmanaged Resource)という。通常の .NET オブジェクトはマネージリソース(Managed Resource)という。
- マネージリソース:GCによって自動的に開放されるリソース
- アンマネージリソース:プログラマの責任で開放する必要があるリソース
どれがマネージリソースでどれがアンマネージリソース?
string str = "";
FileStream fs = new FileStream("file", FileMode.Create);
IntPtr ptr = Marshal.AllocCoTaskMem(1024);
マネージリソース
- str と str が参照する string オブジェクト
- fs と fs が参照する FileStream オブジェクト
- ptr
アンマネージリソース
- fs が参照する FileStream オブジェクト が持っているファイルハンドル
- ptr の値が指しているアドレスに確保されたメモリ
ガベージコレクションの強制実行
GC は自動で実行されるが、明示的に実行させることもできる。
// GCの実行
GC.Collect();
// ファイナライズ(後述)の完了を待つ
GC.WaitForPendingFinalizers();
// ファイナライズで参照されなくなったオブジェクトを開放
GC.Collect();
ただしGCはプログラマより賢いので、特別な理由がない限り実行させる必要はない。
アンマネージリソースの解放
アンマネージリソースを確実に開放するための仕組みに、IDisposable インタフェースとデストラクタがある。
IDisposable インタフェース
プログラマが明示的にリソースを開放するためのDispose()メソッドを持つインタフェース。
public class DisposeTest : IDisposable
{
public void Dispose()
{
// 確保したリソースを開放する処理
Console.WriteLine("Dispose");
}
}
ちなみにDispose()メソッドは、複数回呼ばれても問題が発生しないように実装しなければならない。
デストラクタ
デストラクタは、オブジェクトがガベージコレクタによって破棄される際に自動的に実行される処理を定義する。この処理のことをファイナライズと言う。
public class DestructorTest
{
~DestructorTest()
{
// 確保したアンマネージリソースを開放する処理
Console.WriteLine("Finalize");
}
}
内部的には、デストラクタは Object.Finalize() をオーバライドする。なお、Finalize() メソッドをプログラマが明示的に呼ぶことはできない。
GC.SuppressFinalize()
ファイナライズの実行はコストが大きので、プログラマが明示的にDispose()を実行した場合はファイナライズ処理を実行させたくない。GC.SuppressFinalize()を使うと、ファイナライズの実行を抑制できる。
// 超ナンセンスなクラス
public class SuppressFinalize
{
public SuppressFinalize()
{
GC.SuppressFinalize(this);
}
~SuppressFinalize()
{
Console.WriteLine("このメッセージは出力されない");
}
}
デストラクタを持つ Dispose パターンの実装
デストラクタと IDisposable の両方を実装すると、下記のようになる。(お決まりのパターン)
public class Disposable : IDisposable
{
public Disposable()
{
Console.WriteLine("ctor");
}
~Disposable()
{
Console.WriteLine("finalize");
Dispose(false);
}
public void Dispose()
{
Console.WriteLine("dispose");
GC.SuppressFinalize(this);
Dispose(true);
}
private bool disposed = false;
public void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// マネージリソースの開放
}
// アンマネージリソースの開放
disposed = true;
}
}
}
この Dispose パターンを実装することで、アンマネージリソースのリークを防止できる。
- プログラマが Dispose() を呼ぶと、アンマネージリソースが開放される。ファイナライズは実行されない。
- プログラマが Dispose() を呼び損ねた場合、ファイナライズが実行されてアンマネージリソースが開放される。
オブジェクトの破棄
try-finally ステートメント
オブジェクトを使い終わったら破棄したい。例外が発生したとしても確実に。
StreamWriter sw = null;
try
{
// IOExceptionが発生するかも
sw = new StreamWriter(file);
sw.WriteLine("test");
}
finally
{
// Exception が発生してもしなくても Dispoose() は呼びたい
sw?.Dispose();
}
ちなみに Stream や TextReader/Writer の Dispose() は Close() と等価。
using ステートメント
オブジェクトが IDisposable インタフェースを実装している場合は、上記の try-finally と等価なコードを using ステートメントで短く書ける。
using(StreamWriter sw = new StreamWriter(file))
{
sw.WriteLine("test");
}
基本的には using ステートメントを使うべきだが、もしサードパーティのライブラリが IDisposable を実装していない場合は try-finally ステートメントで書くことになる。
その他覚えておいた方がよさそうなこと
- オブジェクトの世代(ジェネレーション)
- GC.ReRegisterForFinalize()
- SafeHandle
- スタックとヒープ
リソース
Fundamentals of Garbage Collection
Cleaning Up Unmanaged Resources
Quiz
-
IDisposable とデストラクタを正しく実装したクラスを使用する場合、必要のないリソースは GC が開放するのでリークは発生しない。これは本当?
-
アンマネージリソースを持たないクラスはデストラクタを定義すべきか否か。また、その理由。
-
下記のうち、GCが実行されても解放されないリソースはどれか。
string str = "";
FileStream fs = new FileStream("file", FileMode.Create);
IntPtr ptr = Marshal.AllocCoTaskMem(1024);
- str
- str が参照する string オブジェクト
- fs
- fs が参照する FileStream オブジェクト
- fs が参照する FileStream オブジェクトが持つファイルハンドル
- ptr
- ptr の値が指しているアドレスに確保されたメモリ