🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

Excel VBA のグローバル変数 — Public 変数、スコープ、そして値が消える理由

|

Excel VBA のグローバル変数 — Public 変数、スコープ、そして値が消える理由

TL;DR — 標準モジュール の先頭、すべてのプロシージャより上に Public gTaxRate As Double と書けば、ブック内のどのプロシージャからでも読み書きできます。ここまでは簡単なほうの半分です。難しいほうの半分は、グローバル変数が値を覚えているのは VBA プロジェクトが読み込まれている間 だけであり、そのプロジェクトは思っているよりずっと頻繁にリセットされる、ということです。End ステートメント、リセットボタン、ユーザーが「終了」を押した未処理のエラー、VBA エディターでの編集、ブックを閉じること。リセットされると、すべてのグローバル変数は何の警告もなく 0、""、Nothing に戻ります。ですから、直前に確認していないグローバル変数を読んではいけません。空なら作り直す関数を経由してアクセスし、セッションを超えて残すべきものはブック自体に保存しましょう。

' Module1 - 一番上、どの Sub や Function よりも前に書く
Option Explicit
Public gTaxRate As Double            ' このプロジェクトのすべてのモジュールから見える

Sub SetUp()
    gTaxRate = 0.19
End Sub

Sub UseIt()
    Debug.Print gTaxRate             ' 0.19 - プロジェクトがリセットされるまでは。その後は 0
End Sub

これは 値がどこに住むのか を扱う三部作の第一回です。すべての変数には スコープ(有効範囲:誰から見えるか)と 有効期間(いつまで値を覚えているか)があります。グローバル変数 はそのどちらも最も広く、プロジェクト全体から見え、プロジェクトがリセットされるまで生きています。Static 変数 は長い有効期間はそのままに、スコープを一つのプロシージャに絞ります。省略可能な引数 は、その両方を呼び出し一回分にまで絞り、値は呼び出し側から渡されます。三部作を通じた私のルール:動く中で一番狭いものを選ぶ。

この記事で学べること

  • 考え方の軸:スコープは誰から見えるか、有効期間はいつまで続くか
  • グローバル変数の宣言方法と、モジュール先頭での Public・Global・Dim の違い
  • グローバル変数をリセットするもの、そして値がエラーもなく消える理由
  • リセットを無害にする「自己修復型」の取得関数
  • グローバル変数を黙って隠してしまうローカルの Dim と、名前があいまいだというエラー
  • シートモジュール、ThisWorkbook、ユーザーフォームのモジュールでの Public 変数
  • ブックを閉じた後も値を残す方法

考え方の軸:スコープと有効期間は別々の問い

変数をどこで宣言するかで、二つの問いに同時に答えが出ます。スコープ は誰から見えるか。有効期間 はいつまで値を保持するか。VBA には三つの段階があります。

宣言する場所 キーワード スコープ 有効期間
Sub や Function の中 Dim そのプロシージャだけ プロシージャが終わるまで
モジュールの先頭 Dim または Private そのモジュールのすべてのプロシージャ プロジェクトがリセットされるまで
標準 モジュールの先頭 Public(または Global) プロジェクトのすべてのプロシージャ プロジェクトがリセットされるまで

一行目は Dim のガイド で扱っています。この記事のテーマは最後の行で、肝心な事実は最後の列にあります。モジュールレベル変数と Public 変数は、Excel を閉じるまで生きているわけでは ありません し、値を設定したプロシージャが終わっても消えません。生きているのはプロジェクトの状態が保たれている間だけで、その状態は見た目よりずっと壊れやすいのです。

グローバル変数の宣言:Public、Global、そして書く位置

グローバル変数は、標準モジュールの 宣言セクション に置く必要があります。Option Explicit の後、最初のプロシージャの前です。同じ行を Sub の中に書くと、VBA は受け付けません。プロシージャの中では Public を使えないからです。

Option Explicit

Public gUserName As String           ' プロジェクト全体
Global gRunCount As Long             ' 同じ意味の古いキーワード
Private mCache As Object             ' このモジュールだけ
Dim mLastRow As Long                 ' これもこのモジュールだけ

Sub Demo()
    ' ここに Public x As Long と書くとコンパイルエラーになる
End Sub

Global は Public の古い書き方です。標準モジュールでしか使えず、何の利点もないので、Public を使いましょう。モジュールレベルでは Dim と Private は同じ意味です。意図がはっきり伝わる Private をおすすめします。グローバル変数には g、モジュールレベル変数には m のような接頭辞を付けるのはタダですし、後の節で紹介するバグを防いでくれます。

値がまったく変わらないなら、それはそもそも変数ではありません。Public Const VAT_RATE As Double = 0.19 を使いましょう。Const のガイド で示しているとおり、定数は値がずれることも、リセットされることもありません。

痛い目を見るルール:グローバル変数はプロジェクトと運命を共にする

誰もが一度は出くわす失敗があります。最初のクリックでマクロが gTaxRate を設定し、二つ目のボタンがそれを使う。午前中はずっと問題なく動きます。ところが昼休みの後、二つ目のボタンが 0 で計算するようになります。エラーは一つも出ていません。その間にプロジェクトがリセットされたのです。リセットは、すべてのモジュールレベル変数と Public 変数を空の値に戻します。

  • どこかで End ステートメントが実行された。End のガイド で示しているとおり、これはすべての状態を一度に消し去ります。
  • VBA エディターで リセット(四角いボタン)を押した、または[実行]→[リセット]を選んだ。
  • 処理されない実行時エラーが表示され、ユーザーがダイアログの 終了 をクリックした。
  • VBA エディターでコードを編集した。宣言を変えたりモジュールを追加したりすると、再コンパイルが必要になってプロジェクトがリセットされることがあります。中断モードなら Excel が先に確認してきますが、そうでなければ黙ってリセットされます。
  • コードが入っているブックを閉じた。

逆に、リセット されない のは次の場合です。値を設定したプロシージャが正常に終わったとき、自分の On Error ハンドラー が捕まえたエラー、そして Exit Sub。この非対称性こそ、このバグがランダムに見える理由です。誰かが未処理のエラーを踏んだ日や、自分のパソコンでコードを編集した直後に現れるのです。

解決策:自己修復型の取得関数を経由してアクセスする

リセットを防ごうとしてはいけません。防ぐことはできないからです。代わりに、リセットされても困らないようにします。変数は Private にしておき、空になっていれば作り直す関数を公開します。

Option Explicit
Private mRates As Object             ' Scripting.Dictionary、必要になったときに作る

Public Function Rates() As Object
    If mRates Is Nothing Then
        Dim r As Range
        Set mRates = CreateObject("Scripting.Dictionary")
        For Each r In ThisWorkbook.Worksheets("Rates").Range("A2:A50")
            If Len(r.Value) > 0 Then mRates(r.Value) = r.Offset(0, 1).Value
        Next r
    End If
    Set Rates = mRates
End Function

Sub PriceOrder()
    Debug.Print Rates()("EUR")       ' どんなリセットの後でも動く - 取得関数が読み直す
End Sub

呼び出し側はすべて、mRates を直接読む代わりに Rates() に問い合わせます。リセットで中身が空になっていても、リセット後の最初の呼び出しが読み直しのコストを払うだけで、間違った答えを目にする人はいません。単純な値でも同じ形が使えます。0 や "" になっていないかを確かめ、なっていれば元のデータから読み直すのです。キャッシュに使うオブジェクトそのものは Dictionary のガイド で解説しています。

静かな落とし穴:同じ名前のローカルの Dim

二つ目の定番バグには、リセットはまったく関係ありません。

' Module1
Public gTotal As Double

Sub AddSales()
    Dim gTotal As Double             ' グローバル変数を隠す「新しい」ローカル変数
    gTotal = gTotal + 100            ' 変わるのはローカルのほうだけ
End Sub

Sub ShowTotal()
    MsgBox gTotal                    ' 0 のまま
End Sub

グローバル変数と同じ名前の Dim をプロシージャ内で宣言すると、グローバル変数は 隠されて(シャドーイングされて) しまいます。AddSales の中では、gTotal という名前はローカルのほうを指すのです。VBA はこれを文句一つ言わずに受け入れますし、Option Explicit も助けになりません。どちらの変数も宣言されているからです。解決策は命名の習慣です。グローバル変数は g で始め、プロシージャの中では g で始まる名前を決して Dim しないことです。

逆向きの衝突ではエラーになります。Module1 と Module2 の両方で Public Total を宣言すると、三つ目のモジュールで Total と書いた時点で、コンパイル時に Ambiguous name detected(あいまいな名前が検出されました)が出ます。Module1.Total のように修飾するか、もっと良いのは、グローバル変数ごとに持ち主のモジュールを一つに決めておくことです。

シートモジュール、ThisWorkbook、ユーザーフォームのモジュールでの Public

シートモジュール、ThisWorkbook、ユーザーフォームの先頭に書いた Public は、グローバル変数にはなりません。作られるのは そのオブジェクトのプロパティ なので、他のモジュールからアクセスするにはオブジェクト名を付ける必要があります。

' Sheet1 のコードモジュール内
Public Threshold As Double

' Module1 内
Sub Check()
    Sheet1.Threshold = 500           ' 修飾付き - これは動く
    ' Threshold = 500                ' Variable not defined (Option Explicit がある場合)
End Sub

修飾せずに書いた最後の行は、Option Explicit があると「Variable not defined」(変数が定義されていません)というコンパイルエラーになります。これらのモジュールはオブジェクトモジュールなので、Public メンバーとして宣言できないものがいくつかあります。配列、定数、固定長文字列、ユーザー定義型は、どれもコンパイルエラーになります。ユーザーフォームには独自の有効期間もあります。UserForm1.SomeValue はフォームがアンロードされた瞬間に失われます。これは ユーザーフォームのガイド で Unload Me と Me.Hide の違いとして紹介しています。本当にプロジェクト全体で使う変数なら、標準モジュールに置きましょう。

ブックを閉じた後も値を残す

ブックを閉じた後まで生き残る変数はありません。最終実行日、ユーザーの選択、カウンターなど、来週もそこにあってほしい値は、ファイルに書き込みます。

Sub RememberLastRun()
    ThisWorkbook.Worksheets("Settings").Range("B2").Value = Now
End Sub

Function LastRun() As Date
    LastRun = ThisWorkbook.Worksheets("Settings").Range("B2").Value
End Function

シートの非表示のガイド にあるように、設定シートを「完全に非表示(very hidden)」にしておけば邪魔になりません。非表示の 名前付き範囲 でもかまいません。どちらにしても、記憶しているのはファイルであって、変数はその高速なコピーにすぎません。これは前述の取得関数と同じ考え方です。

判断の分かれ目:設定とキャッシュにはグローバル変数、データには引数

グローバル変数は悪ではありません。共有されているだけです。ただ、共有された状態は追いかけるのが難しいのです。どのプロシージャもグローバル変数を書き換えられるので、間違った値が入っていたら、誰が書いたのかをプロジェクト全体から探す羽目になります。私のルール:グローバル変数は、プロジェクト全体が読み、書く側はほぼいないものに使う。 一度だけ読み込む設定や、取得関数の裏にあるキャッシュなどです。あるプロシージャから別のプロシージャへデータを渡すときは、ByVal か ByRef を意図して選んだうえで引数として渡します。一つのプロシージャだけが呼び出しの間で何かを覚えておけばよいなら、Static 変数 が、共有せずにその記憶を与えてくれます。

ExcelMaster の活用

グローバル変数のバグは、グローバル変数のバグには見えないものです。今朝は動いていたのに今はゼロで計算するボタンや、ローカルのコピーが更新を横取りしたせいでいつまでも変わらない合計として現れます。

ExcelMaster には、マクロにさせたいことをそのまま伝えられます。たとえば「レートを一度だけ読み込んで、すべての価格計算マクロで使って」。すると、共有する状態をリセットに強い取得関数の裏に置き、データは引数で受け渡すコードを書き、既存のコードの中でグローバル変数を隠しているローカルの Dim も見つけ出します。

よくある質問

VBA でグローバル変数を宣言するには?

標準モジュールの先頭、最初の Sub や Function より前に、Public gTaxRate As Double のように Public に続けて名前と型を書きます。これで、プロジェクト内のすべてのプロシージャがその変数を読み書きできます。プロシージャの中で宣言するとコンパイルエラーになります。

VBA の Public と Global の違いは何ですか?

実質的な違いはありません。Global は古いキーワードで、標準モジュールでしか使えません。Public は同じ役割を果たし、クラス、シート、ユーザーフォームのモジュールでも使えます。Public を使いましょう。

VBA のグローバル変数の値が消えてしまうのはなぜですか?

VBA プロジェクトがリセットされたからです。End ステートメント、エディターのリセットボタン、ユーザーが「終了」をクリックした未処理のエラー、一部のコード編集、そしてブックを閉じること。これらはすべて、モジュールレベル変数と Public 変数を一つ残らずクリアします。空になっていれば読み直す関数を経由して値にアクセスしましょう。

ブックを閉じた後も変数の値を残すには?

変数ではなくブックに保存します。非表示の設定シートのセル、非表示の名前定義、ドキュメントプロパティなどです。ブックを開くとき、たとえば Workbook_Open イベントの中で読み戻します。

ユーザーフォームからグローバル変数を使えますか?

使えます。標準モジュールで Public として宣言すれば、フォームのコードから読み書きできます。ユーザーフォーム自身のモジュールの中で宣言した Public 変数はフォームのものであり、フォームがアンロードされると失われます。

検証環境

検証環境: Excel 365 (Windows 11)、VBA 7.1 — 最終確認 2026-10-01。

関連ガイド: VBA Static 変数 · VBA 省略可能な引数 · VBA Dim · VBA Const · VBA End · VBA ByRef と ByVal · VBA Dictionary · VBA Option Explicit