TL;DR — プロシージャの中で
Dimの代わりにStaticと書くと、プロシージャが終わっても変数は値を保持します。Static clicks As Longは、今回の呼び出しだけでなく、すべての呼び出しを数えます。これは 自分だけの記憶 です。プロシージャの外からは誰も見えませんが、グローバル変数と同じだけ、つまり VBA プロジェクトがリセットされるまで生き続けます。それが Static の強みであり、罠でもあります。Static は自分からは決して忘れないので、エラーでクリアする行が飛ばされたせいで詰まったままのフラグも、その後データが変わってしまったのに作られたままのキャッシュも、覚え続けます。記憶の持ち主が一つのプロシージャなら Static を使いましょう。別のプロシージャがそれを読んだりリセットしたりする必要が出てきた瞬間に、モジュールレベルのPrivateへ移します。
Sub CountClicks()
Static clicks As Long ' 呼び出しの間も保持される
clicks = clicks + 1
Application.StatusBar = "Clicked " & clicks & " times"
End Sub
これは 値がどこに住むのか を扱う三部作の第二回です。グローバル変数 はプロジェクト全体から見え、プロジェクトがリセットされるまで生きています。Static 変数はその有効期間を保ったまま、一つのプロシージャからしか見えません。省略可能な引数 の寿命は呼び出し一回分です。三つを通じたルールは、役目を果たせる中で一番狭いスコープを選ぶことです。
この記事で学べること
- 考え方の軸:Static はローカルのスコープとモジュールの有効期間を併せ持つ
- Static・Dim・モジュールレベルの Private の比較
- Static が得意な四つの仕事:カウンター、トグル、再入防止フラグ、キャッシュ
- Static が覚えすぎる二つのパターン:詰まったフラグと古いキャッシュ
- ワークシート関数を呼ぶすべてのセルが一つの Static を共有する理由
- Static Sub と、Static をプロシージャの外へ格上げするタイミング
考え方の軸:ローカルのスコープ、モジュールの有効期間
プロシージャ内の Dim はメモ用紙です。プロシージャが始まるときに作られ、終わるときに捨てられるので、次の呼び出しはゼロから始まります。プロシージャ内の Static は そのプロシージャだけが鍵を持つ引き出し です。プロシージャはそこに何かを入れたまま終了し、次の呼び出しでそれを取り出せます。
| 宣言 | 誰から見えるか | いつまで値を保持するか |
|---|---|---|
プロシージャ内の Dim n As Long |
そのプロシージャ | プロシージャが終わるまで |
プロシージャ内の Static n As Long |
そのプロシージャ | プロジェクトがリセットされるまで |
モジュール先頭の Private n As Long |
そのモジュールのすべてのプロシージャ | プロジェクトがリセットされるまで |
標準モジュール先頭の Public n As Long |
プロジェクト全体 | プロジェクトがリセットされるまで |
狭いスコープと長い有効期間を兼ね備えているのは、二行目だけです。「プロジェクトがリセットされるまで」の意味はグローバル変数の場合と同じです。End、リセットボタン、ユーザーが「終了」を押した未処理のエラー、一部のコード編集、ブックを閉じること。詳しくは グローバル変数のガイド で解説しています。
Static の宣言と、最初の呼び出しでの値
Static は、プロシージャの中で Dim の代わりにしか使えません。
Function NextInvoiceNo() As Long
Static lastNo As Long ' 一番最初の呼び出しでは 0
lastNo = lastNo + 1
NextInvoiceNo = lastNo
End Function
VBA には初期化子がないので、Static lastNo As Long = 1000 は構文エラーです。最初の呼び出しでは、Static はその型の空の値、つまり 0、""、Empty、Nothing を持っています。本当の初期値は自分で設定します。0 が正当な値になりうる場合は、二つ目の Static で最初の呼び出しかどうかを記録します。
Function NextInvoiceNo() As Long
Static lastNo As Long, started As Boolean
If Not started Then
lastNo = ThisWorkbook.Worksheets("Settings").Range("B3").Value
started = True
End If
lastNo = lastNo + 1
NextInvoiceNo = lastNo
End Function
このコードが何を認めているかに注目してください。Static は高速なコピーにすぎず、本当の番号はブックの中にあるのです。ファイルを閉じた後も残すべきカウンターは、ブックに書き戻さなければなりません。プロジェクトより長生きする変数はないからです。
Static が得意な四つの仕事
トグル。 何かをオン・オフ切り替える一つのボタンは、前回の状態を知っている必要があります。
Sub ToggleGridlines()
Static isOff As Boolean
isOff = Not isOff
ActiveWindow.DisplayGridlines = Not isOff
End Sub
カウンターや累計。 上の請求書番号のように、一つのルーチンに属するものです。
再入防止フラグ。 監視しているシートを自分で変更するイベントハンドラーに使います。Worksheet_Change の中でセルに書き込むと Worksheet_Change が再び発生しますが、Static のフラグがそのループを止めます。
Private Sub Worksheet_Change(ByVal Target As Range)
Static busy As Boolean
If busy Then Exit Sub
busy = True
On Error GoTo Done
Target.Offset(0, 1).Value = Now ' このイベントを再び発生させてしまう
Done:
busy = False
End Sub
キャッシュ。 作るのに時間がかかり、一つのプロシージャだけが必要とする検索表に使います。
Function RegionOf(ByVal code As String) As String
Static map As Object
If map Is Nothing Then
Dim r As Range
Set map = CreateObject("Scripting.Dictionary")
For Each r In ThisWorkbook.Worksheets("Regions").Range("A2:A500")
map(r.Value) = r.Offset(0, 1).Value
Next r
End If
If map.Exists(code) Then RegionOf = map(code)
End Function
If map Is Nothing の判定は一人二役です。最初の呼び出しでキャッシュを作り、プロジェクトのリセットでクリアされた後には作り直します。これは グローバル変数のガイド で紹介した自己修復パターンそのもので、キャッシュをそれを使う一つの関数の中に閉じ込めた形です。
痛い目を見るルール:Static は自分からは決して忘れない
Static のバグはすべて同じバグです。値が、それが正しかった状況よりも長生きしてしまうのです。
詰まったフラグ。 上の再入防止フラグから On Error GoTo Done の行を消してみてください。すると、busy = True の後に起きたあらゆるエラー(保護されたセルや型の不一致など)が、busy = False が実行される前にハンドラーを終わらせてしまいます。フラグは True のまま残り、それ以降は毎回一行目で抜けてしまいます。イベントハンドラーは、プロジェクトがリセットされるまで黙って死んだままです。ユーザーには、エラーメッセージもなく更新されなくなったシートが見えるだけです。再入防止フラグは、すべての終了経路でクリアしなければなりません。エラーハンドラーがクリアする行へジャンプするのはそのためです。もう一つのよくある防止策は EnableEvents のガイド で扱っており、ルールは同じです。
古いキャッシュ。 RegionOf は検索表を一度だけ作ります。その後で誰かがシートに地域を追加しても、データが変わったことを誰も知らせないので、関数は一日中古い検索表から答え続けます。しかも、関数の外からは知らせようがありません。Static は外から見えないからです。これこそ Static の本当の限界です。別のプロシージャからはリセットできないのです。
UDF の意外な挙動:すべてのセルが一つの Static を共有する
Static は、呼び出された場所ではなく、プロシージャに属します。ワークシート関数の場合、それを呼ぶすべてのセルが一つの変数を共有するということです。
Function CallCount() As Long
Static n As Long
n = n + 1
CallCount = n
End Function
' A1、A2、A3 に =CallCount() と入れても 1, 1, 1 にはならない -
' 計算順に応じて三つの異なる数値になる
計算順を決めるのは Excel であり、Excel はいつでもどのセルでも再計算できます。そのため、答えが Static に依存するユーザー定義関数 (UDF) は、勝手に変わる結果を返します。ワークシート関数が引数だけに依存すべき理由は Function のガイド で説明しています。RegionOf の検索表のような UDF 内の Static キャッシュ は問題ありません。変わるのは答えが返ってくる速さであって、答えそのものではないからです。UDF 内の Static カウンター や累計はバグです。
Static Sub と、ユーザーフォームの中の Static
プロシージャの前に Static を付けると、その中のすべてのローカル変数が Static になります。
Static Sub Tally()
Dim total As Double ' Static Sub なので、これも Static になる
total = total + 1
End Sub
この書き方は存在しますが、避けましょう。読む人は Dim を見て新しい変数だと思い込みますし、そうではないと告げる先頭のキーワード一つは見落としやすいのです。記憶が必要な変数に、個別に印を付けましょう。
ユーザーフォームのプロシージャ内の Static 変数は、フォームが読み込まれている間しか生きていません。Unload の後、次に Show したときは新しい値から始まります。ユーザーフォームのガイド で扱っているフォームの他の変数と同じ有効期間です。
判断の分かれ目:二つ目のプロシージャが必要とするまでは Static
Static は、プロシージャに記憶を与える最も狭い方法です。そして、狭いのは良いことです。他の何かがうっかり値を変えることはありませんし、宣言はそれを使うコードのすぐ隣にあります。私のルール:記憶を読み書きするプロシージャがちょうど一つで、他の誰もそれをクリアする必要がないなら Static を使う。 二つ目のプロシージャがそれを見る必要が出てきたら、あるいはキャッシュを空にする「更新」ボタンが必要になったら、変数をモジュールの先頭に Private として移し、リセット用の小さなプロシージャを追加します。
Private mMap As Object
Sub ResetRegionCache()
Set mMap = Nothing ' 次の検索で作り直される
End Sub
こうすれば、スコープを新しい要件が許す範囲で最も狭く保てますし、いきなり Public のグローバル変数に飛びつかずに済みます。
ExcelMaster の活用
Static のバグは幽霊のように見えます。目に見える理由もなく発生しなくなるイベントハンドラー、追加したばかりの行を無視する検索、シートを再計算するたびに数値がずれる UDF。
ExcelMaster には、マクロに覚えておいてほしいことをそのまま伝えられます。たとえば「新しい請求書には、前回使った番号の続きから番号を振って」。すると、その記憶にふさわしい置き場所(Static か、リセット付きのモジュールレベル変数か、ブックのセルか)を選び、再入防止フラグが詰まらないようにするエラー処理も加えます。
よくある質問
VBA の Static とはどういう意味ですか?
Static は、プロシージャの終了後も値を保持する変数を、プロシージャの中で宣言します。次の呼び出しでは、前回の呼び出しが残した値が見えます。その変数が見えるのはそのプロシージャだけで、値は VBA プロジェクトがリセットされるか、ブックを閉じると失われます。
VBA の Static と Dim の違いは何ですか?
プロシージャ内の Dim 変数は、呼び出しのたびに空から始まり、プロシージャが終わると消えます。Static 変数が空から始まるのは最初の呼び出しだけで、呼び出しの間も値を保持します。どちらも、そのプロシージャの中からしか見えません。
マクロの実行をまたいで変数の値を保持するには?
そのプロシージャだけが必要とするならプロシージャの中で Static として、複数のプロシージャが必要とするならモジュールの先頭で Private として宣言します。どちらもプロジェクトがリセットされるまで値を保持します。ブックを閉じた後も残すには、セルか名前定義に保存します。
Static 変数に初期値を与えられますか?
宣言の中では与えられません。VBA には初期化子がないので、Static n As Long = 5 は構文エラーです。変数は 0、""、Empty、Nothing から始まります。最初の呼び出しで初期値を設定し、最初の呼び出しが済んだかどうかは二つ目の Static の Boolean で判断します。
Static 変数をリセットするには?
変更できるのは宣言したプロシージャだけなので、そのプロシージャにリセットの手段を持たせるか、変数をモジュールレベルの Private に移して小さなリセット用プロシージャを追加します。VBA プロジェクト全体をリセットしてもクリアされますが、そのときは他のモジュールレベル変数や Public 変数もすべて一緒に消えます。
検証環境
検証環境: Excel 365 (Windows 11)、VBA 7.1 — 最終確認 2026-10-01。
関連ガイド: VBA グローバル変数 · VBA 省略可能な引数 · VBA Dim · VBA Function · VBA Worksheet Change · VBA EnableEvents · VBA Dictionary · VBA ユーザーフォーム
