VB.NETを極める:Const、ReadOnly、Staticが織りなす変数ライフサイクルとメモリ最適化の真髄
レガシーなVBA(Visual Basic for Applications)の受託開発から、モダンな.NET Core / .NET 8環境での高スループットなバックエンドシステムまで、我々は常に「状態(State)」との戦いを続けてきた。
変数に値を格納し、それを読み出し、破棄する。この一連のライフサイクル管理を誤れば、メモリリーク、マルチスレッド環境における競合状態(Race Condition)、そして原因不明のバグという名の魔物が牙をむく。
VB.NETには、変数の不変性や生存期間を制御するための強力なキーワードが用意されている。それが `Const`、`ReadOnly`、そして `Static` である。
今回は、これら3つのキーワードの本質を、IL(Intermediate Language)レベルの挙動やメモリ最適化、さらにはWindows API連携といった実務の現場における極限の知見を交えて徹底解剖する。
—
1. コンパイル時定数 `Const`:インライン展開の罠とパフォーマンス
`Const` は、文字通り「定数」を定義する。最大の特徴は、コンパイル時に値がコードへ直接埋め込まれる(インライン展開される)点にある。
ILレベルの挙動と注意点
`Const` で定義された値は、参照する側のメソッドやアセンブリのILコードに直接リテラルとしてハードコーディングされる。つまり、外部アセンブリ(DLL)で定義された `Const` の値を変更した場合、それを参照している側も再コンパイル(Rebuild)しなければ古い値が使われ続けるという致命的な罠がある。
Public Class SystemConfig
‘ 厳密な定数:アセンブリ境界を越える公開には注意が必要
Public Const DefaultTimeoutMs As Integer = 5000
End Class
【実務知見】Windows API定義における使い分け
レガシーシステム連携で避けて通れない Windows API(`DllImport`)の定数定義においては、`Const` は非常に有効である。しかし、OSのバージョンアップ等で値が変わる可能性がある定数については、後述する `ReadOnly`(特に `Shared ReadOnly`)への置き換えを視野に入れるべきだ。
—
2. 実行時不変フィールド `ReadOnly`:インスタンスの整合性とメモリ効率
`ReadOnly` は、フィールドが書き込み可能である期間を「コンストラクタの実行完了まで」に限定する修飾子である。
オブジェクトのライフサイクルとスレッドセーフティ
`ReadOnly` の真価は、マルチスレッド環境におけるイミュータビリティ(不変性)の保証にある。ロック機構(`SyncLock` 等)を使わずとも、スレッドセーフなオブジェクト共有が可能になるため、コンテキストスイッチのオーバーヘッドを劇的に削減できる。
特に `Shared ReadOnly`(C#の `static readonly` に相当)は、アプリケーションのライフサイクル全体を通じて一度だけ初期化されるため、極めて強力だ。
Imports System.Runtime.InteropServices
Public NotInheritable Class WinApiConnector
‘ 実行時初期化されるイミュータビリティの高いフィールド
Private ReadOnly _hModule As IntPtr
Private Shared Function LoadLibrary(lpFileName As String) As IntPtr
End Function
Private Shared Function FreeLibrary(hModule As IntPtr) As Boolean
End Function
Public Sub New(dllPath As String)
_hModule = LoadLibrary(dllPath)
If _hModule = IntPtr.Zero Then
Throw New System.ComponentModel.Win32Exception(Marshal.GetLastWinError())
End If
End Sub
‘ Windows APIを安全に呼び出すメソッド
Public ReadOnly Property Handle As IntPtr
Get
Return _hModule
End Get
End Property
End Class
明示的なリソース解放(IDisposableパターン)との統合
上記のコードを見て「APIハンドルは誰が解放するのか?」と気づいた読者は優秀だ。`ReadOnly` フィールドに保持された非マネージドリソースは、GC(ガベージコレクション)の対象外であるため、必ず `IDisposable` を実装し、手動で解放しなければならない。
Public NotInheritable Class WinApiConnector
Implements IDisposable
Private _hModule As IntPtr
Private _disposed As Boolean = False
Public Sub New(dllPath As String)
_hModule = LoadLibrary(dllPath)
‘ …初期化処理…
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
Private Sub Dispose(disposing As Boolean)
If Not _disposed Then
If _hModule <> IntPtr.Zero Then
‘ 非マネージドリソースの明示的解放
FreeLibrary(_hModule)
_hModule = IntPtr.Zero
End If
_disposed = True
End If
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
MyBase.Finalize()
End Sub
End Class
※注:`ReadOnly` フィールド自体はコンストラクタ以外で再代入不可だが、フィールドが指すオブジェクト内部の状態変更や、非マネージドハンドルの破棄自体は `Dispose` パターンで行う必要がある点に注意されたい。
—
3. メソッド内状態保持 `Static`:VBAの遺産からモダン.NETのキャッシュ戦略へ
VBA開発者にとって `Static` は馴染み深いだろう。VBAではプロシージャ終了後も値を保持する変数を定義するために使われていた。
VB.NETにおいてもメソッド内で `Static` を使用すると、その変数はメソッドのローカルスコープでありながら、アプリケーションのライフサイクル全体(正確には型がロードされている間)を通じて単一のインスタンスとしてヒープ領域に保持される。
【警告】マルチスレッド環境における `Static` の罠
VB.NETの `Static` 変数は、スレッドセーフではない。複数のスレッドから同時に `Static` 変数を持つメソッドが呼び出された場合、値の競合(Data Race)が発生し、予期せぬバグを引き起こす。
以下のコードは、スレッドアンセーフなカウンターの悪例と、それを排他制御で守る模範解答である。
Public Class UnsafeIdGenerator
‘ 【危険】マルチスレッド環境では値が破損する
Public Function GetNextIdUnsafe() As Long
Static currentId As Long = 0
currentId += 1
Return currentId
End Function
Private ReadOnly _lockObj As New Object()
Private _threadSafeId As Long = 0
‘ 【推奨】Staticの代わりにクラスレベルのフィールドとSyncLockを使用する
Public Function GetNextIdSafe() As Long
SyncLock _lockObj
_threadSafeId += 1
Return _threadSafeId
End SyncLock
End Function
End Class
それでも `Static` が輝くユースケース:シングルスレッド処理とメモ化(Memoization)
逆に、UIスレッド上でのイベントハンドラや、シングルスレッドで動作することが保証されているバッチ処理の高速化(キャッシュ)においては、`Static` はコードを簡潔にし、インスタンス生成のオーバーヘッドを排除する優れた手段となる。
Public Class HeavyCalculator
Public Function Compute(input As Integer) As Double
‘ 高価な計算結果をキャッシュするStatic変数
Static cache As New Dictionary(Of Integer, Double)()
If cache.ContainsKey(input) Then
‘ キャッシュヒット:爆速で返す
Return cache(input)
End If
‘ 重い処理のシミュレーション
Dim result As Double = Math.Sqrt(input) Math.PI
cache(input) = result
Return result
End Function
End Class
—
4. 総括:使い分けの黄金律
どのキーワードを選択すべきか、シニアアーキテクチャとしての判断基準を以下にまとめる。
| キーワード | スコープ | ライフサイクル | 主な用途・メリット | 注意点 |
| :— | :— | :— | :— | :— |
| `Const` | クラス / メソッド | コンパイル時〜永続 | 絶対に変更されない数学定数や設定値。インライン展開による最高速のパフォーマンス。 | アセンブリを跨ぐ変更時の再コンパイル漏れに注意。 |
| `ReadOnly` | クラス(フィールド) | インスタンス / アプリ起動中 | イミュータブルなオブジェクト設計。スレッドセーフな値の保持。 | 非マネージドリソースの解放は `IDisposable` で別途担保が必要。 |
| `Static` | メソッド内(ローカル) | アプリ起動中(型生存期間) | メソッド内での簡易的な状態保持、単一スレッドでのキャッシュ。 | マルチスレッド非対応。競合によるバグの温床になりやすい。 |
レガシーシステムの保守からクラウドネイティブなモダン開発まで、コードの「寿命」と「変更可能性」をコントロールすることは、エンジニアリングの根幹である。
`Const` で硬く締め、`ReadOnly` で安全な境界線を引き、`Static` はその特性を熟知した上でピンポイントで採用する。この規律を身につけた時、あなたの書くVB.NETコードは、世代を超えて耐えうる「真に堅牢なシステム」へと昇華する。
