オブジェクト解放(デリゲートによる参照)

Visual Basic 6など、COMでは参照カウントによってオブジェクトの解放を管理していました。オブジェクトに参照カウントを持たせておき、代入などにより参照されるときにカウントアップ、Nothingや別のオブジェクトの代入、オブジェクト変数のスコープが外れたときなど参照されなくなったときにカウントダウンし、参照カウントが0になったときに、どこからも参照されていないから不要と見なして解放する仕組みです。参照されている限りオブジェクトは解放されないので破棄されたオブジェクトにアクセスしてエラーが発生することはありません。参照カウントの仕組みは単純なオブジェクトではうまく動作しメモリ管理を楽にしてくれました。

しかしオブジェクトが循環参照を持つ場合にはいつまでもオブジェクトが解放されず、不必要なメモリを消費したりエラーの原因となったりしました。この場合、どのオブジェクトが循環参照を生じているのか解析しながら注意深く循環参照の連鎖を切る必要があり、面倒な作業を強いられました。

一方Net Frameworkでは、オブジェクト解放の仕組みが変わりガーベジコレクタが使われるようになりました。ガーベジコレクタはルートオブジェクトからたどって参照されているオブジェクトを有効と見なすので、循環参照が生じてもルートからたどれなくなっていれば解放されます。このため循環参照を気にする必要がなくなり管理がずいぶんと楽になりました。ガーベジコレクタはプログラマが明示しなくても適当なタイミングで実行されますがGC.Collect()によっても明示的に実行させることができます。

オブジェクトの解放と関連して、Net FrameworkにはIDisposableインタフェースを備えたクラスがあります。WindowsアプリケーションではControl、Formなどがそうです。これらのDisposeメソッドを呼び出すと、アンマネージリソースの解放、該当オブジェクトが参照しているオブジェクト変数へのNothingの代入が行われます。注意点としてマネージリソースは即時解放されるのではなくて次回ガーベジコレクタが働いたときに他のオブジェクトから参照されていなければ解放されること、Disposeメソッドが呼び出されたオブジェクトが解放されるわけではないということです。しかしDisposeが呼び出されたオブジェクトはまともに動作する状態ではありませんので再び必要になれば改めて生成する必要があります。Dispose呼び出し済かどうかはControlのIsDisposedメソッドで確認できます。

既定の動作ではフォームを閉じたときにはDisposeメソッドが呼び出されます。フォームを参照している変数がないと仮定して、フォームが閉じた後にGC.Collectを呼び出せばフォームおよびフォームが所有していてたオブジェクトは完全に解放されるはずです。しかし、フォームを解放したはずなのに解放したフォームが所有しているオブジェクトが解放されずにエラーが発生することがありました。

状況としては呼び出し元フォームをForm1、呼び出し先フォームをForm2として、Form2の所有するオブジェクトEventSinkTestがForm1のイベントを補足するためForm1への参照を持っています。具体的にはClickイベントを補足して、Form1がクリックされればメッセージが表示されるようになっています。Form2を閉じるとDisposeメソッドが呼び出され使用できない状態になります。さらに、GC.Collect()を呼べばEventSinkTestも解放されることを期待しますが、そのようにはならず、依然としてEventSinkTestがForm1のイベントを補足し続けます。

この状況でForm1からForm2のEventSinkTestへの参照がないと考えたのが間違いのようでした。イベントソース(この場合Form1)からイベントシンク(EventSinkTest)への参照はデリゲートを介して存在しており、構文上気がつきにくいオブジェクト図の波線で表した参照があることでEventSinkTestが解放されなかったのです。

これを回避するためFormがDisposeされるときEventSinkTestからForm1への参照もなくしてしまうと、期待通りイベントの発生が止まりました。実際にEventSinkTestが解放されるのはガーベジコレクションのときですがイベントはすぐに止まります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
'呼び出し元フォームのコード
Public Class Form1

Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim Form2 As New Form2
Form2.Show()
Form2.Initialize(Me)
End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
GC.Collect()
End Sub
End Class

'呼び出し先フォームのコード
Public Class Form2
Public Class EventSinkTest
Dim WithEvents _form As Form

Public Property Form() As Form
Get
Return _form
End Get
Set(ByVal value As Form)
_form = value
End Set
End Property

Private Sub _form_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles _form.Click
MsgBox("Form_Click")
End Sub
End Class

Dim es As EventSinkTest

Public Sub Initialize(ByVal form As Form)
es = New EventSinkTest()
es.Form = form
End Sub

Private Sub Form2_Disposed(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Disposed
'オブジェクトを解放するためには以下の行を追加する。
'es.Form = Nothing
End Sub
End Class