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()
が無くても起こります。このテストプログラムは小さいためにほぼ起きませんが、ある程度長く実行する実用的なプログラムでは、稀に起こります。
これを原因とする不具合は、デバッグ版など環境によっては絶対に起きない点、リリース版でも、タイミングによっては発生したりしなかったりするので、非常に厄介なので気をつけましょう。実際苦労しました・・・ (涙)。
不具合を回避する方法は、ネイティブリソースをイミュータブルクラスでラップする方法 か、 ネイティブリソースをラップする方法 をご覧ください。前者の方が効率的です。
このサイトのページへのリンクは自由に行っていただいてかまいません。
このサイトで公開している全ての画像、プログラム、文書の無断転載を禁止します。
ここをクリック
すると表示されるページから作者へメールで連絡できます。