c# のファイナライザーは、c++ のデストラクターよりも早期に呼び出されることがあるようなので、かなり注意が必要です。下のコードを見てください。
public static void Test()
{
new A().CallTest();
}
class A
{
protected IntPtr handle;
public A()
{
handle = new IntPtr(1);
Console.WriteLine("A(): " + handle);
}
~A()
{
handle = IntPtr.Zero;
Console.WriteLine("~A(): " + handle);
}
public void CallTest()
{
Console.WriteLine("a.CallTest() 開始: " + handle);
var b = new B(handle);
new Thread(b.Exec).Start();
GC.Collect();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("a.CallTest() 終了");
}
}
CallTest()
メソッドで、b.Exec
を実行するスレッドをスタートして終了する簡単なコードです。handle
は、ウィンドウハンドルやビットマップハンドル、あるいは、ネイティブコード用のポインターを想定していて、実際のコードでは、~A()
で開放します。
GC.Collect()
では、強制的にガベージコレクションが実行されます。
実行にはあまり影響ありませんが、一応クラス
B
のコードも掲載します。クラス
B
のメソッド呼び出し部分は、実際のコードでは、ネイティブコードの呼び出しを想定しています。例えば、handle を引数に Windows API を呼びだします。
class B
{
protected IntPtr handle;
public B(IntPtr handle)
{
this.handle = handle;
}
public void Exec()
{
Test(handle);
}
static void Test(IntPtr handle)
{
System.Threading.Thread.Sleep(500);
Console.WriteLine("b.Test(): " + handle);
}
}
このコードの実行結果は、環境によって異なるでしょうが、c# 2008 Express Edition でコンパイル、Windows XP x64 のデバッグ版で実行すると、↓のようになります。
A(): 1 a.CallTest() 開始: 1 b.Test(): 1 a.CallTest() 終了 ~A(): 0
ところが、驚くべきことに、リリース版では、結果が全く異なります。
A(): 1 a.CallTest() 開始: 1 ~A(): 0 b.Test(): 1 a.CallTest() 終了
注目すべきは、a
のメソッド
CallTest()
の実行が終わる前に、ファイナライザーが呼びだされている点です。
~A()
で
handle
の開放を行っている場合、
b.Test()で
は、
handle
が無効になってしまいます。この例そのものは安全ですが、実際には、
var b = new B(handle); new Thread(b.Exec).Start();
の部分で、ネイティブコードを呼びだすので、非常に危険です。アプリケーションが落ちたり、最悪な場合、有害なコードを実行してしまうかもしれません。
また、この現象は、
GC.Collect()
で、無理に引き起こしていますが、
GC.Collect()
が無くても起こります。このテストプログラムは小さいためにほぼ起きませんが、ある程度長く実行する実用的なプログラムでは、稀に起こります。
これを原因とする不具合は、デバッグ版など環境によっては絶対に起きない点、リリース版でも、タイミングによっては発生したりしなかったりするので、非常に厄介なので気をつけましょう。実際苦労しました・・・ (涙)。
不具合を回避する方法は、ネイティブリソースをイミュータブルクラスでラップする方法 か、 ネイティブリソースをラップする方法 をご覧ください。前者の方が効率的です。
このサイトのページへのリンクは自由に行っていただいてかまいません。
このサイトで公開している全ての画像、プログラム、文書の無断転載を禁止します。
ここをクリック
すると表示されるページから作者へメールで連絡できます。