【極限の知見】画面を閉じてもメモリが減らない? WeakEventManagerで実現する「死なない」イベント設計
「プログラムは動いている。でも、なぜか動作がどんどん重くなっていく……」
もしあなたが、複数の画面(フォーム)を行き来するアプリケーションを作っていて、そんな現象に直面しているとしたら、それはあなたが「イベント駆動設計の深淵」に足を踏み入れた証拠です。
こんにちは。今日は、マクロの記録から一歩踏み出し、真のエンジニアリングを目指すあなたへ、プロの世界で必須となる「メモリリークを防ぎつつ、画面間でスマートにデータを共有する技術」を伝授します。
難しい言葉が並ぶかもしれませんが、大丈夫。一つずつ紐解いていけば、必ずマスターできます。ここをクリアすれば、あなたの作るアプリケーションの品質は、一段上のステージへと跳ね上がりますよ。
1. なぜ、画面を閉じても「メモリ」は解放されないのか?
まず、私たちが直面する「問題の正体」を理解しましょう。
Windows Forms開発において、よくある設計はこうです。
「メイン画面のデータが変わったら、サブ画面にもそれを伝えたい。だから、メイン画面の `DataChanged` イベントに、サブ画面のメソッドを `AddHandler` で登録しよう!」
一見、完璧な設計に見えますよね。しかし、ここに「参照の罠」が潜んでいます。
【図解:メモリリークのメカニズム】
通常、オブジェクト(フォーム)が不要になったら、Windows(.NETのガベージコレクション:GC)が「あ、これはもう使わないな」と判断して、メモリから消去してくれます。
しかし、`AddHandler` を使ってイベントを登録すると、以下のような関係性が生まれます。
1. メイン画面(Publisher) が、サブ画面(Subscriber) のメソッドを握りしめる。
2. このとき、メイン画面はサブ画面に対して「強い参照(Strong Reference)」を持ちます。
3. ユーザーがサブ画面を「×」ボタンで閉じても、メイン画面がサブ画面を「握りしめている」限り、GCは「まだ使っている最中だ」と勘違いして、サブ画面をメモリから消せません。
これが、画面を閉じるたびにメモリ消費量が増えていく「メモリリーク」の正体です。
2. 解決策:WeakEventManager(弱参照)という魔法
この問題を解決する鍵が、「弱参照(Weak Reference)」です。
「強い参照」が「絶対に離さない! 抱きしめる!」という執着心だとしたら、「弱参照」は「もし相手が立ち去りたがっているなら、そっとしておくよ」という、紳士的な見守り方です。
相手(サブ画面)が不要になって消えようとしたとき、弱参照であれば、イベントの繋がりが原因で消えなくなることはありません。
今回は、この「紳士的なイベント通知」を実現するために、`WeakEventManager` という仕組みを自作して実装してみましょう。
3. 実践:メモリリークフリーなObserverパターンの構築
それでは、実際に動くコードを見ていきましょう。
「データを保持するクラス」が「通知を行う」役割を担い、複数のフォームがそれを「監視する」構成です。
Step 1: 弱参照によるイベント管理クラスを作る
まずは、イベントの購読者を「弱参照」として保持する、魔法の管理クラスを作成します。
Imports System.Runtime.CompilerServices
''' <summary>
''' 購読者を弱参照として管理し、メモリリークを防ぐイベントマネージャー
''' </summary>
Public Class WeakEventManager(Of TData)
' 購読者のアクションを弱参照で保持するためのリスト
Private ReadOnly _subscribers As New List(Of WeakSubscriber(Of TData))()
' 購読者(リスナー)を登録するメソッド
Public Sub Subscribe(action As Action(Of TData))
Dim subscriber = New WeakSubscriber(Of TData)(action)
SyncLock _subscribers
_subscribers.Add(subscriber)
End SyncLock
End Sub
' イベントを発生させるメソッド
Public Sub Raise(data As TData)
Dim toRemove As New List(Of WeakSubscriber(Of TData))()
SyncLock _subscribers
For Each subsc In _subscribers
' 参照先がまだ生きているか確認
If subsc.Action IsNot Nothing Then
subsc.Action.Invoke(data)
Else
' 参照先が消えていたら、削除リストへ
toRemove.Add(subsc)
End If
Next
' 死んでしまった購読者を掃除する
For Each deadSub In toRemove
_subscribers.Remove(deadSub)
Next
End SyncLock
End Sub
''' <summary>
''' 内部で使用する、アクションを弱参照で包むクラス
''' </summary>
Private Class WeakSubscriber(Of TData)
' Action自体を弱参照で保持する
Private _actionRef As WeakReference(Of Action(Of TData))
Public Sub New(action As Action(Of TData))
_actionRef = New WeakReference(Of Action(Of TData))(action)
End Sub
' 実行可能なアクションを返す(消えていたらNothing)
Public ReadOnly Property Action As Action(Of TData)
Get
Dim act As Action(Of TData) = Nothing
_actionRef.TryGetTarget(act)
Return act
End Get
End Property
End Class
End Class
Step 2: データを共有する「データモデル」を作る
次に、このマネージャーを内包した、データの持ち主(Subject)を作ります。
''' <summary>
''' データを保持し、変更を通知するクラス(Subject)
''' </summary>
Public Class SharedDataModel
' 共有するデータ
Public Property Message As String = "初期メッセージ"
' 弱参照イベントマネージャーを公開
Public ReadOnly Property OnDataChanged As New WeakEventManager(Of String)()
''' <summary>
''' データを更新し、通知を行う
''' </summary>
Public Sub UpdateMessage(newMessage As String)
Me.Message = newMessage
' 購読者に通知!
OnDataChanged.Raise(newMessage)
End Sub
End Class
Step 3: フォームでの利用例
最後に、これらを実際にフォームからどう使うかを見てみましょう。
【メインフォーム:データを更新する側】
Public Class MainForm
' 共有データモデルのインスタンス
Private _model As New SharedDataModel()
Private Sub btnUpdate_Click(sender As Object, e As EventArgs) Handles btnUpdate.Click
' データを更新(これがサブフォームに伝わる)
_model.UpdateMessage("こんにちは!" & DateTime.Now.ToString("HH:mm:ss"))
End Sub
Private Sub btnOpenSub_Click(sender As Object, e As EventArgs) Handles btnOpenSub.Click
' サブフォームを作成して表示
Dim subForm As New SubForm(_model)
subForm.Show()
End Sub
End Class
【サブフォーム:データを受け取る側】
Public Class SubForm
Private _model As SharedDataModel
Public Sub New(model As SharedDataModel)
InitializeComponent()
_model = model
' 【重要】AddHandlerではなく、WeakEventManagerにSubscribeする
' これにより、このSubFormが閉じられた後、_modelがこのフォームを掴み続けることはありません
_model.OnDataChanged.Subscribe(AddressOf HandleDataChanged)
End Sub
' データが更新された時に呼ばれるメソッド
Private Sub HandleDataChanged(newMessage As String)
' UI操作は必ずメインスレッドで行う必要がある点に注意
Me.Invoke(Sub()
lblDisplay.Text = newMessage
End Sub)
End Sub
End Class
4. ここがプロの視点!解説と注意点
このコードを理解するために、重要なポイントを整理しておきましょう。
なぜこれでメモリリークが防げるのか?
`WeakSubscriber` クラスの中で、`Action` を `WeakReference` で保持しています。
サブフォームが閉じられ、GCが走ると、サブフォームに関連付けられた `Action`(`HandleDataChanged` メソッド)はメモリから消滅します。すると、`_actionRef.TryGetTarget(act)` は `Nothing` を返すようになります。
`WeakEventManager` は、`Nothing` を検知するとリストから自動的に削除するため、「死んだ参照」が溜まっていくこともありません。
陥りやすいエラー:スレッドセーフティ
コード内で `Me.Invoke` を使いましたね。これは非常に重要です。
イベント通知は、必ずしも「ボタンを押したスレッド」で行われるとは限りません。UI(ラベルのテキストなど)を書き換えるときは、必ず `Invoke` を使って「メインスレッド(UIスレッド)で実行してください」とお願いする癖をつけましょう。これを忘れると、アプリが突然落ちる原因になります。
疎結合(Loose Coupling)のメリット
メインフォームは、サブフォームが「存在しているか」を知る必要がありません。ただデータを更新するだけです。
これこそが疎結合。部品同士がバラバラに動ける設計にすることで、後から「新しい画面を追加したい」となったときも、既存のコードを壊さずに拡張できるようになります。
まとめ:一歩先へ進むあなたへ
いかがでしたでしょうか?
1. `AddHandler` は便利だが、強い参照によるメモリリークの危険がある。
2. `WeakReference`(弱参照)を使えば、相手の生存を邪魔せずに通知できる。
3. `WeakEventManager` を使った設計は、大規模で複雑なアプリになればなるほど威力を発揮する。
最初は「ちょっと難しすぎるかも……」と感じても大丈夫です。このコードを一度コピペして動かし、サブフォームを何度も開いて閉じてみてください。そして、タスクマネージャーでメモリが異常に増えていないことを確認してください。
その「確かな手応え」こそが、エンジニアとしての自信に変わります。
ここをクリアすれば、Visual Basic (VB.NET) の設計思想は、もうあなたのものです。
さあ、自信を持って、次の素晴らしいアプリケーションを作りに行きましょう!
