型安全なイベント駆動の極意:`EventHandler(Of T)` によるモダンVB.NET設計
レガシーなVB6やVBAの時代から、私たちはイベント駆動型プログラミングの恩恵を受けてきた。ボタンが押されれば `Click` が走り、タイマーが来れば `Tick` が走る。
しかし、業務システムの肥大化に伴い、「画面間やレイヤー間のデータ受け渡しに `Object` 型(非型安全)のバスを使い、実行時エラーの温床を作る」というアンチパターンをいまだに見かける。
`ByVal sender As Object, ByVal e As EventArgs`。
この見慣れたシグネチャは、VB.NETが成熟した現代において、厳格な型安全とパフォーマンス、そして保守性を手に入れるための「障壁」ではなく「キャンバス」でなければならない。
今回は、VB.NETのジェネリクスを極限まで活かし、メモリ効率と型安全性を両立させた `EventHandler(Of T)` によるカスタムイベント設計の真髄を解説する。
—
1. なぜ標準の `EventArgs` では不十分なのか
通常のイベント定義では、以下のようなコードが書かれる。
‘ レガシーなイベント定義
Public Event DataLoaded As EventHandler
これを受け取る側は、`e` が一体何のデータを持っているのかを知る術を持たない。`EventArgs.Empty` であるか、あるいは無理やりカスタムクラスにキャストする羽目になる。
Private Sub OnDataLoaded(sender As Object, e As EventArgs)
‘ 毎回キャストが必要、かつ型ミスのリスクが潜む
Dim customArgs = DirectCast(e, MyCustomEventArgs)
Console.WriteLine(customArgs.RecordCount)
End Sub
これはVB6の `Variant` 型の悪夢を .NET の世界に持ち込むようなものだ。シニアエンジニアであれば、コンパイル時に型が保証されない設計は排除すべきである。
—
2. `EventHandler(Of TEventArgs)` による型安全な設計
.NET Framework 2.0 以降、`EventHandler(Of TEventArgs)` デリゲートが提供されている。これを利用することで、イベントリスナー側は最初から「期待するデータ型」を引数として受け取ることが保証される。
ここでは、実務で頻出する「重いバッチ処理の進捗通知とデータ伝送」を例に、カスタムイベントの完全な実装を示す。
ステップ 1: イミュータブルなカスタム `EventArgs` の定義
イベント引数は、イベント発行者からリスナーへ渡す「スナップショット」である。不必要な状態変更を防ぐため、`ReadOnly` プロパティを持つイミュータブル(不変)なクラスとして設計するのが鉄則だ。さらに、大規模データ処理におけるメモリ最適化の観点から、不要になったリソースの確実な解放も考慮する。
Imports System
Namespace Enterprise.Architecture.Events
‘ イミュータブルなカスタムイベント引数
Public Class BatchProgressEventArgs
Inherits EventArgs
‘ 読み取り専用プロパティ(データのカプセル化)
Public ReadOnly Property CurrentStep As Integer
Public ReadOnly Property TotalSteps As Integer
Public ReadOnly Property Message As String
Public ReadOnly Property ProcessedData As ReadOnlyMemory(Of Byte)
‘ コンストラクタで初期化を強制
Public Sub New(currentStep As Integer, totalSteps As Integer, message As String, data As Byte())
Me.CurrentStep = currentStep
Me.TotalSteps = totalSteps
Me.Message = message
‘ メモリ最適化:ArraySegmentやReadOnlyMemoryを活用し、無駄なコピーを避ける
Me.ProcessedData = If(data IsNot Nothing, New ReadOnlyMemory(Of Byte)(data), ReadOnlyMemory(Of Byte).Empty)
End Sub
End Class
End Namespace
ステップ 2: 発行者(Publisher)側の実装とメモリ管理
イベントを発行するクラスでは、ガベージコレクション(GC)のリークを防ぐための配慮が必要だ。特に、長寿命のオブジェクトから短寿命のオブジェクトへイベントを登録したままにすると、リスナーが解放されずにメモリリーク(LOHフラグメンテーションや不要なオブジェクト保持)を引き起こす。
Namespace Enterprise.Architecture.Services
Public Class HeavyBatchProcessor
‘ EventHandler(Of T) を用いた型安全なイベント定義
‘ イベントハンドラーが登録されていない場合のNull確認を簡略化するため、初期値としてNothingを代入するか、C#の ?. に相当するVBの構文を使用する
Public Event ProgressChanged As EventHandler(Of BatchProgressEventArgs)
‘ イベントを発行するProtectedメソッド(VBの標準パターン)
Protected Overridable Sub OnProgressChanged(e As BatchProgressEventArgs)
‘ VB.NETの最新構文(Invokeメソッドと条件分岐の融合)
‘ スレッドセーフかつ、リスナーがNothingでないことを安全に確認
RaiseEvent ProgressChanged(Me, e)
End Sub
‘ 重い処理のシミュレーション
Public Sub Execute(total As Integer)
For i As Integer = 1 To total
‘ 業務ロジックの実行…
System.Threading.Thread.Sleep(100)
Dim dummyData As Byte() = System.Text.Encoding.UTF8.GetBytes($”Record_{i}”)
‘ イベント引数の生成と発行
Dim args As New BatchProgressEventArgs(i, total, $”処理中: ステップ {i}”, dummyData)
OnProgressChanged(args)
Next
End Sub
End Class
End Namespace
—
3. レガシーシステム・外部API連携における注意点
現場がレガシー環境(Windows Forms との混在、あるいは古いCOM相互運用)である場合、イベントの「スレッドコンテキスト」が問題になる。
背景として、Windows FormsのUIスレッド以外からイベントが発火され、直接UIコントロールを操作すると `InvalidOperationException`(クロススレッド操作のエラー)が発生する。これを防ぐためには、発行側ではなく購読側(Subscriber)で適切にディスパッチするか、あるいは発行側で同期をとる必要がある。
購読側(Subscriber)での安全な実装
Public Class MainForm
Inherits Form
Private WithEvents _processor As HeavyBatchProcessor
Private Sub InitializeBatch()
_processor = New HeavyBatchProcessor()
‘ ハンドラーの結び付け
AddHandler _processor.ProgressChanged, AddressOf HandleProgressChanged
End Sub
Private Sub HandleProgressChanged(sender As Object, e As BatchProgressEventArgs)
‘ UIスレッドへの安全なマーシャリング(InvokeRequiredの確認)
If Me.InvokeRequired Then
Me.Invoke(New Action(Sub() HandleProgressChanged(sender, e)))
Return
End Sub
‘ 型安全なプロパティアクセス(キャスト不要!)
Me.ProgressBar1.Maximum = e.TotalSteps
Me.ProgressBar1.Value = e.CurrentStep
Me.StatusLabel.Text = e.Message
‘ 業務データの処理(ReadOnlyMemoryから安全に配列化またはSpanで処理)
Dim span = e.ProcessedData.Span
‘ …高速なメモリ処理…
End Sub
End Class
—
4. チーフアーキテクトからの提言:パフォーマンスと保守性の極限へ
VB.NETは「レガシーな言語」ではない。.NET Runtimeの進化とともに、C#と同等のパフォーマンスとモダンな言語機能を手に入れている。
1. `Object` 型の排除: イベント引数に `Object` や `EventArgs` をそのまま使うのは、タイプミスと実行時エラーの温床である。必ず `EventHandler(Of T)` を採用せよ。
2. イミュータブルなデータ構造: イベント引数は「過去の事実」の伝達手段である。値の書き換えを許容してはならない。
3. メモリ効率の意識: 大規模なバイナリや文字列をイベントでやり取りする場合は、`String.Concat` の乱用を避け、`ReadOnlyMemory(Of T)` や `ReadOnlySpan(Of T)` を活用してGCプレッシャーを極限まで下げよ。
型安全とは、コンパイラという最強の相棒にコードの正しさを証明してもらうことだ。
あなたの書くVB.NETコードが、単なる「動くレガシー」から「堅牢無比なエンタープライズアーキテクチャ」へと昇華することを期待する。
