はじめに
IDisposable
なんて別にpublic void Dispose()
を実装するだけでも事足りることばかり。でもどうせならDisposeパターン
なんてより丁寧に書くための物があるんだから使いたくなるのが開発者の心情というもの。と言うわけでIDisposable
を使うときの個人的なメモ。
「より安全で丁寧なコードを書くにはどうしたら良いだろう?」という考え方が、以前書いた「【Windows/C#】なるべく丁寧にDllImportを使う」に通じるものがあるかも・・・1
Disposeパターン
C#でよくリソースを破棄するために使うIDisposable
インターフェイス。
それを丁寧に扱うためのテンプレートとして有名なDisposeパターン
。
Visual Studio 2019だとclass A : IDisposable
と打ち込んで、右クリックメニューから「クイックアクションとリファクタリング...」から「破棄パターン2を使ってインターフェイスを実装します」を押すと自動的に生成してくれるアレ。
class A : IDisposable
{
private bool disposedValue;
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
// TODO: マネージド状態を破棄します (マネージド オブジェクト)
}
// TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします
// TODO: 大きなフィールドを null に設定します
disposedValue = true;
}
}
// // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
// ~A()
// {
// // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
// Dispose(disposing: false);
// }
public void Dispose()
{
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
ファイナライザ いる?いらない?
自動生成されるコードでは、ファイナライザがコメントアウトされていて「アンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします」と書いてある。
またDisposeパターン
の正しい実装を求めてグーグル先生に尋ねてみたけど、どの情報も「アンマネージドリソースを使う場合はファイナライザを実装する」みたいな書き方になってた。
本当にそれで安全なのかな?
継承先でアンマネージドリソースが使われると...?
protected virtual void Dispose(bool disposing)
とvirtual
を付けているくらいだから継承できるはず。
とすると、そのクラスでアンマネージドリソースを使わなかったとしても、継承先で使われる可能性は十分あり得るのでは・・・?
もしその時に継承元がファイナライザを実装していないとどうなるのかな?
継承元のファイナライザが無いとどうなる?
まずファイナライザを実装した場合を実験
継承元のAクラス。
class A : IDisposable
{
protected virtual void Dispose(bool disposing)
{
Console.WriteLine($"A.Dispose({disposing})");
}
// TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
~A()
{
Console.WriteLine($"A.~A()");
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: false);
}
public void Dispose()
{
Console.WriteLine($"A.Dispose()");
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
継承先のBクラス。
class B : A
{
protected override void Dispose(bool disposing)
{
Console.WriteLine($"B.Dispose({disposing})");
base.Dispose(disposing);
}
}
あと実験用のProgram.cs。
class Program
{
static void Test()
{
B b = new B();
}
static void Main(string[] args)
{
Test();
GC.Collect();
}
}
単純にDispose
とファイナライザを呼び出したときにコンソール出力するだけのクラス。
それをわざとDispose
をすっぽかしてみる。
結果
A.~A()
B.Dispose(False)
A.Dispose(False)
当然BクラスのDispose
もFalse
で呼ばれている。
ちなみにGC.Collect()
を呼ばないと、ファイナライザを実装してても呼ばれなかった。
次にファイナライザを実装しなかった場合を実験
アンマネージドリソースを使ってないからとAクラスのファイナライザは実装しない。
class A : IDisposable
{
protected virtual void Dispose(bool disposing)
{
Console.WriteLine($"A.Dispose({disposing})");
}
// // TODO: 'Dispose(bool disposing)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします
// ~A()
// {
// // このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
// Dispose(disposing: false);
// }
public void Dispose()
{
Console.WriteLine($"A.Dispose()");
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
継承先のBクラスでは、ファイナライザでDispose(false)
を呼んでくれると信じて、アンマネージドリソースの破棄処理を実装したとする。
class B : A
{
protected override void Dispose(bool disposing)
{
Console.WriteLine($"B.Dispose({disposing})");
if (disposing)
{
Console.WriteLine($"マネージド リソースを破棄!");
}
Console.WriteLine($"アンマネージド リソースを破棄したい!");
base.Dispose(disposing);
}
}
そしてさっきと同じように実行。
結果
しーん・・・
やはり実行されなかった。
どうすべきか?
少なくともsealed
が付いていないクラス3でIDisposable
を実装するときは、
「継承してもアンマネージドリソースを使うわけがないクラス」
「そもそも継承する予定のないクラス」
「Disposeは必ず呼ぶから大丈夫」
色々あるだろうけど、別にファイナライザを実装しても行数が増える程度で特に問題はないので、なにも考えずにファイナライザのコメントを解除して実装してしまうのが1番安全な気がする。
作成当時は「有り得ない」と思ってても、時間が経ってから継承してアンマネージドリソースを使うかもしれない。
もちろん「アンマネージドリソースを使った側でファイナライザを実装する」というのもアリかもしれない。
class B : A
{
~B()
{
Dispose(disposing: false);
}
protected override void Dispose(bool disposing)
{
Console.WriteLine($"B.Dispose({disposing})");
if (disposing)
{
Console.WriteLine($"マネージド リソースを破棄!");
}
Console.WriteLine($"アンマネージド リソースを破棄!");
base.Dispose(disposing);
}
}
むしろ「アンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします」という説明的には、こっちの方が正しい気さえする。
でも継承先でDispose
をオーバライドしてもIDisposable
のテンプレートみたいにファイナライザは自動生成されないし、絶っっ対にファイナライザの実装を忘れると思う。
なによりいちいち継承先で実装するのは面倒臭い。
というわけで、IDisposable
をDisposeパターン
で実装するときは、自動生成のコメントに惑わされず常にファイナライザは実装していこうかなーって話でした。
余談
自動生成されるDisposeパターン
のコードがいつの間にか微妙に変わってる?
public void Dispose()
{
// このコードを変更しないでください。クリーンアップ コードを 'Dispose(bool disposing)' メソッドに記述します
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
なんで引数にdisposing:
をわざわざ付けるんだろう・・・
以前はGC.SuppressFinalize(this);
もコメントアウトされてて、アンマネージドリソースを使う場合のみ呼び出す形だったような・・・