TL;DR —
Optionalを使うと、呼び出し側は引数を省略できます。Sub Export(ByVal path As String, Optional ByVal addDate As Boolean = True)は、Export "C:\Out"ともExport "C:\Out", Falseとも呼び出せます。型を指定した Optional にはすべて既定値を付けておけば、それで完了です。知っておくべきルールは一つだけです。型を指定した Optional は「渡されなかった」と「既定値が渡された」を区別できません。 そしてIsMissingが機能するのは、既定値のないVariantだけです。引数を省略することが、その値を渡すのと同じ意味なら既定値を使います。省略することが、どんな値でも表せない意味を持つときだけ、VariantとIsMissingを使います。さらに、プロシージャに新しい入力が必要になったら、それを末尾に Optional として追加し、これまでの動作を既定値にしましょう。そうすれば既存の呼び出し元はすべて動き続けます。
Sub ExportReport(ByVal folder As String, Optional ByVal addDate As Boolean = True)
Dim fileName As String
fileName = "Report"
If addDate Then fileName = fileName & " " & Format(Date, "yyyy-mm-dd")
Debug.Print folder & "\" & fileName & ".pdf"
End Sub
' ExportReport "C:\Out" -> C:\Out\Report 2026-10-01.pdf
' ExportReport "C:\Out", False -> C:\Out\Report.pdf
これで 値がどこに住むのか を扱う三部作は完結です。グローバル変数 は、リセットされるまでプロジェクト全体で共有されます。Static 変数 は、一つのプロシージャだけの記憶です。省略可能な引数 は、そのどれよりも狭い存在です。呼び出し側が一回の呼び出しのために値を渡すか、既定値に任せるかのどちらかです。そして、一番信用しやすいのもこれです。値がどこから来たのかは、呼び出しを読めばわかるからです。
この記事で学べること
- 考え方の軸:Optional は、既定値があらかじめ印刷された申込書の記入欄
- 宣言のルールと、既定値が定数でなければならない理由
- 唯一のルール:型を指定した Optional は省略と既定値を区別できない
- Variant と IsMissing、そしてそれが正解になる場面
- 引数の飛ばし方と、名前付き引数がパラメーター名を変えられなくする理由
- 任意の個数の値を受け取る ParamArray と、受け渡しの落とし穴
- みんながすでに呼んでいるプロシージャを安全に変える方法としての Optional
考え方の軸:既定値が印刷された記入欄
必須の引数は、呼び出し側が必ず埋めなければならない空欄です。省略可能な引数は、あらかじめ値が印刷されている空欄です。呼び出し側はそれを書き換えても、そのままにしてもかまいません。どちらにしても、プロシージャは必ず値を受け取ります。このイメージで、この後のルールがすべて説明できます。印刷される値は、誰かが記入する前に決まっていなければなりません。必須の欄より後ろに置かなければなりません。そして一度印刷されてしまえば、プロシージャには、呼び出し側がそのままにしたのか、同じ値を上から書いたのかがもうわからないのです。
省略可能な引数の宣言:ルール
Function Discount(ByVal amount As Double, _
Optional ByVal rate As Double = 0.1, _
Optional ByVal roundTo As Long = 2) As Double
Discount = Round(amount * (1 - rate), roundTo)
End Function
OptionalはByValやByRefの前に置き、既定値は型の後ろに= 0.1のように書きます。- Optional はすべて、必須の引数の 後ろ に置きます。一つの引数を Optional にしたら、それより後ろの引数もすべて Optional でなければなりません。
- 既定値は 定数式 でなければなりません。数値、文字列、
True、自分で宣言した定数などです。Optional asOf As Date = Dateはコンパイルエラー(Constant expression required)になります。Dateは後で実行される関数だからです。既定値は書かずに、プロシージャの先頭で補いましょう。
Sub Snapshot(Optional ByVal asOf As Date)
If asOf = 0 Then asOf = Date ' 0 は「指定なし」の意味 - そのときは今日の日付にする
Debug.Print Format(asOf, "yyyy-mm-dd")
End Sub
既定値のないオブジェクト型の引数は Nothing として届きます。そのため「特に指定がなければアクティブシートを使う」は簡単に書けます。Optional ws As Worksheet と宣言し、If ws Is Nothing Then Set ws = ActiveSheet とするだけです。
ワークシート関数でも、省略可能な引数は同じように振る舞います。=Discount(A2) は両方の既定値を使い、=Discount(A2, 0.2) は一つを上書きします。ユーザー定義関数 (UDF) 設計のそれ以外の話は Function のガイド で扱っています。
唯一のルール:型を指定した Optional は省略と既定値を区別できない
もう一度 Snapshot を見てください。呼び出し側が何も渡さなければ、asOf は 0 です。呼び出し側がわざと 0 を渡しても、asOf はやはり 0 です。プロシージャには両者を区別する手段がありません。これは型を指定したすべての Optional に当てはまります。省略された Long は 0、String は ""、Boolean は False、あるいは宣言した既定値になります。
たいていの場合、それこそが望む動作です。「省略する」と「既定値を渡す」は同じ意味であるべきですし、既定値があればプロシージャの動作は最初の一行から明らかです。問題になるのは、呼び出し側が 省略すること が、その型のどの値でも表せない意味を持たなければならないときです。典型的なのは、既定で「フィルターなし」になるフィルターです。0 や "" も、誰かがフィルター条件にしたい正当な値かもしれないからです。
IsMissing:省略そのものに意味を持たせたいとき
その場合は、引数を 既定値なし の Variant として宣言し、IsMissing に問い合わせます。
Function CountWhere(rng As Range, Optional ByVal match As Variant) As Long
Dim c As Range, n As Long
For Each c In rng
If IsMissing(match) Then
If Not IsEmpty(c.Value) Then n = n + 1 ' フィルターなし: 入力済みのセルを数える
ElseIf c.Value = match Then
n = n + 1 ' フィルターあり。0 や "" でも絞り込む
End If
Next c
CountWhere = n
End Function
IsMissing には二つの条件があり、どちらか一方でも破ると、黙って永遠に False を返すようになります。
- 引数は
Variantでなければなりません。LongやStringに対しては、IsMissingは常にFalseです。 - 引数には 既定値があってはいけません。
Optional match As Variant = ""は決して省略扱いになりません。既定値が埋めてしまうからです。
私の判断:IsMissing を使うのは、「省略されている」ことが本当に第三の状態であるときだけです。プロシージャの先頭で既定値を補うためだけに使っているなら、代わりに型付きの既定値を宣言しましょう。そうすれば宣言そのものが動作を説明してくれますし、型はコンパイラーがチェックしてくれます。
引数の飛ばし方と名前付き引数
途中の Optional を飛ばすには、その位置を空けておくか、渡す引数に名前を付けます。
Debug.Print Discount(200, , 0) ' 既定の割引率、小数 0 桁に丸める
Debug.Print Discount(200, roundTo:=0) ' 同じ意味で、読みやすい
Debug.Print Discount(amount:=200, rate:=0.25)
名前付き引数は := と引数名を使い、順番は自由です。Optional が二つ以上あるプロシージャでは、名前付き引数が読みやすい選択です。, , 0 では、どの値が何なのかがまったく伝わらないからです。ただし、見落とされがちな帰結が一つあります。どこかの呼び出し元が roundTo:= を使った時点で、引数名はインターフェイスの一部になる のです。roundTo を decimals に名前変更しても、プロシージャ自体は問題なくコンパイルされますが、その名前を指定していた呼び出し元はすべて「Named argument not found」(名前付き引数が見つかりません)で壊れます。Optional の名前は、プロシージャ名と同じくらい慎重に選びましょう。
ParamArray:任意の個数の値
呼び出し側に好きなだけ値を渡させたいときは、ParamArray を使います。最後の引数でなければならず、Variant の配列でなければならず、同じプロシージャの中で Optional と組み合わせることはできません。
Function SumAll(ParamArray values() As Variant) As Double
Dim i As Long, total As Double
For i = LBound(values) To UBound(values)
total = total + values(i)
Next i
SumAll = total
End Function
' SumAll(1, 2, 3) -> 6
' SumAll() -> 0 (UBound が -1 なので、ループは一度も実行されない)
落とし穴は受け渡しです。SumAll が受け取った値を別の ParamArray プロシージャに渡そうとして Other values と書いても、値は展開されません。Other が受け取るのは、配列全体という 一つ の引数です。補助のプロシージャは、配列を入れた普通の Variant を受け取るように書き、その中でループしましょう。空の配列を安全に扱うための境界の話は UBound のガイド で扱っています。
判断の分かれ目:Optional は壊さずにプロシージャを育てる方法
Optional の一番役に立つ点は、便利さではありません。互換性 です。ExportReport folder が十二個のボタンと三つの別のマクロから呼ばれていて、今度一つのレポートだけ日付を付けないようにしたくなったとします。シグネチャに新しい必須の引数を足せば、十五の呼び出し元が壊れます。Optional ByVal addDate As Boolean = True を追加すれば、一つも壊れません。既定値は、これまでの呼び出し元が受け取っていた動作そのものであり、新しい呼び出し元一つだけが False を渡すからです。
私のルール:
- 新しい入力は末尾に、Optional として、これまでの動作を既定値にして追加する。
- 「指定なし」が本当に第三の状態でない限り、
IsMissingより型付きの既定値を選ぶ。 - Optional が二つか三つを超えたら、呼び出し側は名前付き引数を使うべきです。そして、そのプロシージャが二つの仕事をしていて、Sub のガイド の考え方に沿って二つのプロシージャに分けるべきではないかを検討する価値があります。
- Optional として受け取れるはずの設定を、グローバル変数 でプロシージャに渡してはいけない。引数は呼び出しの中に見えますが、グローバル変数はどこにも見えません。
ExcelMaster の活用
省略可能な引数のバグは、呼び出しの中に隠れています。String に対して IsMissing を問い合わせたせいで一度も効かないフィルター、名前付き引数を使っていたすべての呼び出し元を壊す名前変更、必須の引数が増えたせいで呼び出し元のボタンが壊れたプロシージャ。
ExcelMaster には、変更内容をそのまま伝えられます。たとえば「月次レポートのときは、このエクスポートで日付を付けないようにして」。すると、これまでの動作を既定値にした Optional として入力を追加するので、既存の呼び出し元は動き続けます。さらに、プロジェクト内のすべての呼び出し箇所もチェックします。
よくある質問
VBA で引数を省略可能にするには?
宣言の中でその引数の前に Optional を付け、既定値を与えます。Optional ByVal rate As Double = 0.1 のように書きます。必須の引数すべての後ろに置く必要があり、それより後ろの引数もすべて Optional でなければなりません。これで呼び出し側はその引数を省略でき、プロシージャは既定値を受け取ります。
IsMissing が常に False を返すのはなぜですか?
IsMissing が機能するのは、省略可能な引数が既定値のない Variant である場合だけです。Long や String のような型を指定した引数や、既定値付きの Variant では、引数が省略されると VBA が値を補うので、省略扱いになることはありません。
省略可能な引数の既定値を今日の日付にできますか?
直接はできません。既定値は定数でなければならず、Date は関数だからです。既定値なしで Optional asOf As Date と宣言し、プロシージャの先頭で If asOf = 0 Then asOf = Date と書きます。
途中の省略可能な引数を飛ばすには?
Discount(200, , 0) のようにカンマでその位置を空けるか、Discount(200, roundTo:=0) のように名前付き引数を使います。名前付き引数のほうがわかりやすく、順番も自由に指定できます。
VBA の ParamArray とは何ですか?
ParamArray を使うと、Function SumAll(ParamArray values() As Variant) のように、プロシージャが任意の個数の引数を一つの Variant 配列として受け取れます。最後の引数でなければならず、省略可能な引数と一緒には使えません。LBound と UBound でループし、引数が一つも渡されなかったときは UBound が -1 になります。
検証環境
検証環境: Excel 365 (Windows 11)、VBA 7.1 — 最終確認 2026-10-01。
関連ガイド: VBA グローバル変数 · VBA Static 変数 · VBA Function · VBA Sub · VBA ByRef と ByVal · VBA UBound · VBA Format · VBA PDF として保存
