【入門編】複数画面間における疎結合なデータ共有:WeakEventManagerを活用したメモリリークフリーなオブザーバーパターンの構築 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

【極限の知見】画面を閉じてもメモリが減らない? 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) の設計思想は、もうあなたのものです。

さあ、自信を持って、次の素晴らしいアプリケーションを作りに行きましょう!

タイトルとURLをコピーしました