背景
GC 実行時にどのように Finalizer や Dispose が呼ばれるのかとか、IDisposable 継承クラスなどをどう扱うのかなどについて明確に整理できていなかったので、備忘録として整理することにした。
Finalizer や IDisposable に関する情報
ざっと以下を眺めた。
- ファイナライザー - C# プログラミング ガイド | Microsoft Docs
- IDisposable インターフェイス (System) | Microsoft Docs
- Dispose メソッドの実装 | Microsoft Docs
- C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて - Qiita
- C# デストラクタとDisposeについて - Qiita
- C# - 【C#】IDisposableインターフェイス|teratail
- using および try..finally を使用してリソースの後処理を行う
中でも、特に以下がわかりやすかったです。ありがとうございます。
一通り読んだら、知りたかったことが整理されており、ほとんどまとめることが無くなりました。(笑)
メモ
これでは終われないので、追加的に書いておきます。
Dispose パターン
Visual Studio で IDisposable を継承すると、「考えられる修正候補を表示する」の機能を利用して、コードを提案してもらえます(VS2015 からとのこと)。bool 値を引数に持つ仮想 Dispose メソッドを利用するパターンです。
実際のコードが以下です。
そのままにしないで、TODO コメントをちゃんと読んで対応しましょう。
- 終わったら TODO 消しましょう
- 使わないコードは削除しましょう
class Class1 : IDisposable
{
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: マネージド状態を破棄します (マネージド オブジェクト)
}
// TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
// TODO: 大きなフィールドを null に設定します
disposedValue = true;
}
}
// // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
// ~Class1()
// {
// // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
// Dispose(disposing: false);
// }
public void Dispose()
{
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
IDisposable について
IDisposable を継承すべきクラス
- (ファイルおよびパイプ ハンドル、レジストリ ハンドル、待機ハンドル、またはアンマネージド メモリ ブロックのポインターのような)アンマネージリソースを保持する場合
- IDisposable を実装するほかのクラス(≒マネージリソース)をメンバに保持する場合
Dispose メソッド
明示的に呼び出さなくとも、Finalizer 呼び出し時には呼び出されます。
しかし、GC まではリソースの開放が行われないため、明示的に呼び出すべきです。
- try... finally 構文を利用して、Dispose を明示的に呼ぶ
- using 構文を利用する(スコープを明確化でき、例外発生時も適切に Dispose を呼び出します)
※ using 構文は、コンパイル時に try...finally 呼び出しに置き換えられます。つまり、IL 的には等価なコードが生成されます。
IDisposable を継承しないクラス
Finalizer を定義すべきではありません。
定義すると呼び出されるため、不要な呼び出しオーバーヘッドが生じてしまいます。
アンマネージリソースを保持するクラス
アンマネージリソース自体には Finalizer がないので、必ずそれを保持するクラスの Finalizer にてアンマネージリソースを解放するようにします。これを行うのが Dispose パターンです。
アンマネージリソースは 「可能な限り SafeHandle から派生したクラスでラップしてね(推奨)」だそうです。
SafeHandle は、IDisposable を実装したクラスとしてハンドルをラップすることにより、using 構文と組み合わせてハンドルを扱うことができるようにするものです。
Finalizer
ファイナライザ? デストラクタ? の使い分け
MS の技術文書を読むと、ファイナライザもデストラクタも一緒みたいな書き方になっているが、混在しているとかえってわかりづらい。
「ファイナライザには頼らない」にも記載されているように、delete のように破棄を制御できる C++ などではデストラクタ、GC によって破棄される C# や Java などではファイナライザという言葉を使うほうがわかりやすいので、そのように使い分けたいと思います。
【小テスト】Finalizer 呼び出し順
質問
以下のような入れ子になったクラスで、Test() の呼び出し後に GC が実行されると、hoge と _hogehoge はどちらの Finalizer が先に呼び出されるでしょうか?
class Hoge
{
class HogeHoge _hogehoge;
}
class SampleApp
{
static void Main()
{
Test();
// ここで GC が実行と仮定
}
static void Test()
{
Hoge hoge; // ローカル変数
}
}
回答
答えは 不定 です。(どちらの Finalizer が先かは決まっていません。)
これは頭に入れておきましょう。
これが意味するところは、ファイナライザではそのクラスがメンバとして保持しているクラスオブジェクトにアクセスしてはいけないということです。
やっちゃいけないのですが、やっちゃっても即時にアクセス違反で落ちたりしないので、潜在的な問題を検出できなかったりするのですが、、、。
以下のどちらの環境で試しても、シンプルなサンプルコードでは落ちないんです。
やっちゃダメなのに。(゚д゚)!
- VS 2013 + .NET Framework 4.5.1 on Windows 7
- VS 2019 + .NET 5.0 on Windows 10
Release/Debug どちらでも落ちません。Test() を1回呼び出し、直後に1回 GC をループで回しても落ちないですし、Test() を複数回呼び出し、直後に1回 GC でも同様に落ちません。
Finalizer や IDisposable に関する実装指針
細かな説明や理解は前述の各種リンク先を見ていただくとして、クラス実装の際に使える指針としてフローを作ってみました。
参考になれば幸いです。
- useDisposePatternOrIDisposeWithNoFinalizer に該当するけど、既に 基底クラスが IDisposable を継承済みの場合、protected virtual void Dispose(bool disposing) をオーバーライドするだけで構いません。
- useDisposePatternOrIDisposeWithNoFinalizer と記載していますが、useDisposePatternWithNoFinalizer と読み替えて構いません。なぜなら、IDE にコード支援機能があるので、純粋な IDisposable 実装を自分で書くよりも、Disposable パターンで実装を統一したほうが良いと考えるからです。