著作一覧 |
次のプログラムはうまく動くのだが、どういう仕組みなんだろうか?
void Foo() { Timer t0 = new Timer(); var t1 = new Timer(); t0.Interval = 10000; t0.Tick += (o, ex) => {MessageBox.Show("hello"); t0.Stop();}; t1.Interval = 3000; t1.Tick += (o, ex) => { System.GC.Collect(); t1.Stop(); }; t0.Start(); t1.Start(); }
t0もt1もどちらもスタック上に確保したオブジェクトだから、メソッドを抜ければそこについての参照は消えてなくなる。
Tickへ与えたイベントハンドラはt0が10秒後、t1が3秒後に起動される。
t1は、その時点で全ジェネレーションの未参照オブジェクトを回収するはず。
とすれば、t0は回収されてしまい、したがって10秒後(t1実行後の7秒後)には何も起きないのではないか?
が、helloというメッセージボックスはポップアップする。
ということは、Timerオブジェクトはイベントを登録すると、オブジェクト内部のリソースで管理するのではなく、外部の何かを利用して管理している=参照を持たれる、ということのようだ。
あるいは、Tickへ与えたクロージャ内で参照しているから? でももしそうならば、循環参照となり、永遠に廃棄されないことになる(クロージャをTickイベントによって参照しているから)。
ということは、Timer#Disposeを明示的に呼ばなければ、Timerのインスタンスは廃棄されないということかな? (一度でもStopを呼べば廃棄可能かも知れないけど、本当にそうなのかな?)
何が知りたいかと言えば、もし、そういうものならば、Timerのインスタンスってインスタンス変数に格納しておく意味はまったく無いということになるわけだが、本当にそうなのかなぁ? ということ(もちろん、StopしたりStartしたりするのなら、インスタンス変数に入れておいた方が操作しやすいから良いわけだが、投げっぱなし系の一度だけタイマーについてばんばん使うとどんどん回収不能なゴミになるといやだなぁということなのだった。Stop呼べば回収可能となって回収されるなら良いが確認できない。というのは、参照なしだと一度Stop呼ぶともうStartは呼べないから調べる手段がないからだ。オブジェクトスペースのダンプを見るとかかな?)。
ジェズイットを見習え |
基本的にSystem.Timers.Timerであれば明示的に止める必要があるし、明示的にDisposeを呼ぶ必要もあります。また残念なことに上のコードだと1回実行ではなく設定間隔で繰り返しちゃうと思うのですが。。。<br>Timerはメインスレッドとは別のバックグラウンドのスレッドとして動作するので、この場合でもコード的にはスコープ抜けちゃうんでしょうけど、実際に必要なものはTimerのスレッドとともにGCされずに残っちゃうと思います。<br>個人的にはTimerはローカル変数じゃなくてメンバー変数にしてDisposeもそのクラスからDisposeを呼ぶパターンにした方が良いかなと。
もう少し書くとラムダ式で書かれたメソッドは実際にはコンパイル時に別のメソッドとして実装され、delegate経由でそのメソッドが呼ばれるようなILに変換されるので、ラムダ式で書いた内容はそのスコープ(Foo関数)の中には実際にはないことになります。(つまりスコープを外れたからと言ってラムダ式の内容がスタックからいなくなるわけではないはずです。)ってまちがってないよね(だれとなく
>(つまりスコープを外れたからと言ってラムダ式の内容がスタックからいなくなるわけではないはずです。<br>これはそうだと思います。無くなる(べき)なのは、イベントへ登録したデリゲートのインスタンスですね。<br>>設定間隔で繰り返しちゃう<br>こっちは、Stop呼び出しているからそれは無いですけど。呼ばなければ、そうなってしまいますね(逆にアプリケーション実行中は繰り返し続ける必要があれば、上の書き方でも良いということになりますね、というかそれも知りたかったところ)