【テクニカル・上級編】モジュールレベル変数と静的変数(Static)の使い分け:状態保持の最適解 – Excel VBA解析バイブル

スポンサーリンク

モジュールレベル変数と静的変数(Static)の完全調停:VBAにおける状態保持の極限設計

VBA(Visual Basic for Applications)の現場において、「プロシージャの実行を跨いで値を保持したい」という要件は、実務開発の初期段階から必ず直面する壁である。

多くの初学者、あるいは場当たり的なコーディングに終始するプログラマーは、この壁に直面した瞬間、思考停止で`Public`修飾子を付けたグローバル変数を作成する。あるいは、ワークシートのセルを「簡易データベース」として代用し、I/Oの嵐を引き起こす。

シニアエンジニアや社内システムアーキテクトが求められるのは、メモリのライフサイクル、スコープの不可侵性、そしてマルチユーザーや非同期イベント(Excelの計算やタイマー割り込みなど)が絡む複雑な環境下での「状態の完全なカプセル化」である。

本稿では、プロシージャ終了後も値を保持するための2大アプローチである 「モジュールレベル変数(Private)」「静的変数(Static)」 の本質的な違いを解剖し、レガシーシステムの保守性をも担保する最適な設計解を提示する。

1. ライフサイクルとスコープの深層理解

まず、VBAのランタイム(VBA7 / 64bit環境を含む)がメモリ上で変数をどのように扱っているか、その根本的な挙動を整理する。

| 特性 | モジュールレベル変数 (`Private`) | 静的変数 (`Static`) | グローバル変数 (`Public`) |
| :— | :— | :— | :— |
| スコープ(可視性) | 宣言されたモジュール内のみ | 宣言されたプロシージャ内のみ | アプリケーション全体 |
| ライフサイクル | モジュールの存続期間中(ブックが開いている間) | ブックが開いている間(永続) | ブックが開いている間 |
| 初期化タイミング | モジュールロード時 / リセット時 | 初回プロシージャ実行時 | プロジェクトロード時 / リセット時 |
| カプセル化 | 高い(プロパティ経由で制御可能) | 最高(外部から完全に隠蔽) | 壊滅的(スパゲッティコードの元凶) |

致命的な罠:VBAのリセット(Endモード)による状態喪失

VBAの大きな特徴(そして脆弱性)として、以下の操作が行われた瞬間、すべてのモジュールレベル変数とStatic変数は初期化(破棄)される

1. コード内で `End` ステートメントが実行された。
2. 実行時エラーが発生し、デバッグモードで「リセット」ボタン(■)が押された。
3. コードのコンパイル状態が変更された(実行中のコード書き換え)。

この挙動を前提とせず、変数が「常に保持されている」という甘い設計に依存していると、突然の状態消失による NullReferenceException や予期せぬロジック破綻を引き起こす。このリスクをハンドリングするアーキテクチャこそがプロの仕事である。

2. 実装パターン:モジュールレベル変数による状態管理

モジュールレベル変数は、「同一モジュール内の複数のプロシージャ間で状態を共有しつつ、外部からは隠蔽したい場合」に採用する。

特によくある実務のユースケースとして、Windows APIの呼び出し状態のキャッシュや、重いCOMオブジェクト(データベース接続など)のインスタンス保持がある。

‘ =================================================================
‘ モジュール名: clsApiManager (または標準モジュール)
‘ 概要: モジュールレベル変数によるAPI呼び出し状態とキャッシュの管理
‘ =================================================================
Option Explicit

‘ Windows APIの宣言(ハンドルやプロセスの管理)
If VBA7 Then
Private Declare PtrSafe Function GetTickCount Lib “kernel32” () As LongPtr
Else
Private Declare Function GetTickCount Lib “kernel32” & Alias “GetTickCount” () As Long
End If

‘ 【モジュールレベル変数】外部からは隠蔽し、同一モジュール内からのみアクセスを許可
Private m_InitializationTime As LongPtr
Private m_ExecutionCounter As Long
Private m_IsInitialized As Boolean

Public Sub InitializeSystem()
‘ 多重初期化の防止と状態の保持
If m_IsInitialized Then
Debug.Print “システムは既に初期化されています。キャッシュを再利用します。”
Exit Sub
End If

m_InitializationTime = GetTickCount()
m_ExecutionCounter = 0
m_IsInitialized = True

Debug.Print “System Initialized at Tick: ” & m_InitializationTime
End Sub

Public Sub ExecuteTask()
‘ 初期化チェックを強制
If Not m_IsInitialized Then
Call InitializeSystem
End If

m_ExecutionCounter = m_ExecutionCounter + 1
Debug.Print “タスク実行回数: ” & m_ExecutionCounter & ” (経過Tick: ” & (GetTickCount() – m_InitializationTime) & “)”
End Sub

Public Sub ResetSystem()
‘ 明示的なメモリ解放・リセット処理
m_InitializationTime = 0
m_ExecutionCounter = 0
m_IsInitialized = False
Debug.Print “システム状態をリセットしました。”
End Sub

アーキテクチャのポイント

  • 変数をすべて `Private` にすることで、他の標準モジュールやシートモジュールからの直接書き換えを物理的に禁止している。
  • 状態の変更は必ずプロシージャ(メソッド)を経由させるカプセル化(オブジェクト指向のカプセル化のVBA的アプローチ)を徹底する。

3. 実装パターン:静的変数(Static)による極限のカプセル化

「状態を保持したいが、その状態はたった一つのプロシージャからしか参照されない」というケースにおいて、モジュールレベル変数を使用するのはスコープの汚染(無駄に広い可視性)につながる。

ここで真価を発揮するのが `Static` キーワードである。

‘ =================================================================
‘ 概要: Static変数を用いた、外部から完全に隔離されたカウンターとキャッシュ
‘ =================================================================
Option Explicit

Public Sub ProcessHeavyCalculation(ByVal InputValue As Double)
‘ 【静的変数】プロシージャ終了後も値は保持されるが、
‘ このプロシージャの外からは絶対にアクセスできない。
Static s_CallCount As Long
Static s_LastInput As Double
Static s_CachedResult As Double

s_CallCount = s_CallCount + 1

‘ 前回と同じ入力値であれば、重い計算をスキップしてキャッシュを返す(メモ化パターン)
If s_CallCount > 1 And s_LastInput = InputValue Then
Debug.Print “【キャッシュヒット】計算をスキップします。結果: ” & s_CachedResult
Exit Sub
End If

‘ — ここに重い処理(例: 複雑なループや外部API連携)が走ると仮定 —
Dim CalculatedResult As Double
CalculatedResult = InputValue 1.15 + Sqr(InputValue) ‘ ダミー計算

‘ 状態をStatic変数に保存
s_LastInput = InputValue
s_CachedResult = CalculatedResult

Debug.Print “【新規計算】 実行回数: ” & s_CallCount & ” | 結果: ” & s_CachedResult
End Sub

Static変数の設計思想

1. 情報の不可侵性: `s_CallCount` や `s_CachedResult` は、`ProcessHeavyCalculation` の外の世界からは存在すら見えない。デバッグ時であっても、外部から誤って書き換えられるリスクがゼロである。
2. メモ化(Memoization)によるパフォーマンス最適化: レガシーなシステム連携や、Excelのセル走査を伴う重い処理において、前回の結果を保持するStatic変数は、実行速度を劇的に改善するキラーソリューションとなる。

4. チーフアーキテクトが警鐘を鳴らす「バグの温床」とアンチパターン

数々の現場で破綻したVBAコードを見てきた経験から、状態保持に関する「やってはいけない設計」を明記する。

アンチパターン1:グローバル変数(Public)によるモジュール間結合

‘ 悪夢の始まり(標準モジュールA)
Public g_CurrentUserID As String

このようなコードは、どのタイミングで誰がその書き換えを行ったのか追跡が不可能になる(スパゲッティコード)。システム間連携やマルチスレッド的挙動(Excelの非同期処理やアドイン)において、競合やデータ汚染の最大の原因となる。Public変数は原則として「定数(Const)」以外には使わない、という規約をチームに課すべきである。

アンチパターン2:オブジェクト変数の不適切な保持とメモリリーク

モジュールレベル変数やStatic変数に、ExcelのRangeや外部COMオブジェクト(Word.ApplicationやADODB.Connectionなど)を長期間保持し続ける設計は、メモリリークやExcelのプロセス残存(ゾンビプロセス)を引き起こす。

‘ 【危険なコード】オブジェクトを保持し続けたままプロシージャが終了する
Private m_WorkSheet As Worksheet

Public Sub BadStoreObject()
Set m_WorkSheet = ActiveSheet ‘ 参照を持ち続ける
‘ 明示的な解放(Set … = Nothing)が行われないままブックが閉じられると危険
End Sub

オブジェクトを保持する場合は、クラスモジュールの `Class_Terminate` イベントを利用するか、確実に解放(`Set x = Nothing`)するライフサイクル管理をコードに組み込まなければならない。

5. 結論:現場で迷ったときの選択基準

シニアエンジニアとして、チームメンバーや後継者に提示すべき「状態保持の選択基準」は以下の極めてシンプルかつ厳格なルールである。

1. その状態は複数のプロシージャから共有される必要があるか?

  • Yes $\rightarrow$ モジュールレベル変数 (`Private`) を採用し、アクセサメソッド(プロパティや関数)経由で操作をカプセル化する。
  • No $\rightarrow$ 次のステップへ。

2. その状態は単一のプロシージャ内だけで完結し、外部から隠蔽されるべきか?

  • Yes $\rightarrow$ 静的変数 (`Static`) を採用し、スコープを最小限に絞る。
  • No $\rightarrow$ 設計の根本的な見直しが必要(クラスモジュールの導入を検討せよ)。

3. グローバル変数 (`Public`) を使おうとしていないか?

  • 即座に手を止めろ。 それは設計の怠慢であり、将来のバグ負債を前借りしているに過ぎない。

VBAという成熟しきった、しかし現場の基幹を支え続けるレガシーな言語において、コードの美しさと堅牢性は「変数のスコープとライフサイクルの制御」の正確さに直結する。本稿の知見を武器に、保守性に優れ、破綻しない堅牢なアーキテクチャを構築してほしい。

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