和風スパゲティのレシピ

日本語でコーディングするExcelVBA

Sub/Functionの引数を省略可能にする-Optional

Sub、Functionなどのプロシージャを作成するとき、
引数を省略可能にするOptionalキーワードの使い方を解説します。

基本構文

Sub [関数名](Optional [引数名] As [型] = [省略時の値])

使用例

Function Get税込価格(価格 As Long, Optional 消費税率 As Double = 0.1) As Long
    Get税込価格 = 価格 * (1 + 消費税率)
End Function

Debug.Print Get税込価格(100) ' 110が表示される

Debug.Print Get税込価格(100, 0.08) ' 108が表示される

解説

直感的に分かりやすい構文のため、使用例を眺めるだけでも、なんとなく仕様がつかめるのではないかと思います。

「引数名 As データ型」という普段の宣言部分に、

  • 先頭にOptionalをつけると、その引数は省略可能になる
  • 後ろに「= ○○」をつけると、省略時の値を設定することができる

という風に、引数のカスタマイズができる機能ですね。


省略時の値を設定しなかった場合は、Longなら0、Stringなら""のような、
その型の既定の初期値が入ります。


Optionalは複数の引数につけることもできます。

Function 関数(A As Long, B As Long, Optional C As Long = -1 _
    , Optional D As String = "", Optional E As Boolean = False)

 
ただし、↑の例のように、一度Optionalをつけたらそれ以降のすべての引数につけなければいけません。

省略できるものと出来ないものが交互に出てきたら、
PCさんから見て、どれが省略されてるのかわからなくなりますから、当たり前ですね。

同じ理由でParamArray(引数の数を可変にする機能)との併用もできません。


あとは細かいですが、ByVal・ByRefは自由に指定可能です。

Optional ByVal 消費税率 As Double = 1.1

という順番で書きますが、ByVal Optional と書いても、
なんとVBEが勝手に反対にしてくれます。


書いたコードが、語順レベルで自動に書き換わるのは、
この機能くらいでしか、お目にかかったことがない気がします。
(endif ⇒ End If のようなスペース挿入くらいなら結構あるけど)


と、どうでもいい小ネタは置いておき、
もっとも重要な「省略時の動き」の解説に入ります。

Optionalを使う場合は必ず目を通しておいてください。

省略時の値に使えるのは定数のみ

省略時の値に設定できるのは、定数のみとなります。


例として、「日付を与えると、曜日をくれる関数」を、

Function Get曜日(日付 As Date) As String
    Get曜日 = Format(日付, "aaa")
End Function

Debug.Print Get曜日(#2020/12/1#) ' 火 と表示される

こんな風に作ったとします。


この時、「省略時は今日の曜日を返す」という仕様にするとしましょう。
なんか汎用関数っぽくていいですね。

ですが、これを

Function Get曜日(Optional 日付 As Date = Date) As String
    Get曜日 = Format(日付, "aaa")
End Function

と書くと、「定数式が必要です」というエラーになってしまいます。
「=○○」の部分は、定数しか書けないため、Dateのような関数は書けません。


ただ、この問題は割と簡単に解決することができ、

Function Get曜日(Optional 日付 As Date = -1) As String
    
    If 日付 = -1 Then 日付 = Date
    
    Get曜日 = Format(日付, "aaa")

End Function

Debug.Print Get曜日 ' 今日の曜日が表示される

とすることでうまくいきます。

省略時の値をとりあえず適当に設定しておき、直後のIf文で省略されたか判定する
ことで、関数や計算値を事実上の省略時の値としているわけですね。


ちなみにこの時の適当な値は何でもいいのですが、
0などにすると、偶然0が指定された時に省略されたと誤認される危険があります。

  • 正の値を想定しているLongなら「-1」
  • 文字列なら「""」ではなく「"省略時"」

など、ありえない値にしておき、事故を防ぎましょう。


分かりやすい様に「Get曜日」関数を題材にしましたが、
よく見ると「日付に1899/12/29を渡すと事故が起きる」のが分かります。

Optionalによる省略を扱う場合は、常にこのような事故を想定する必要があります。

省略時の値にはオブジェクトも指定できない

省略時の値は定数限定と言うことで、オブジェクトも指定できません。

Sub データを集計する(Optional 集計シート As Worksheet = ActiveSheet) ' ←エラーになる

 
これも定数の時と同様に、

Sub データを集計する(Optional 集計シート As Worksheet)
    If 集計シート Is Nothing Then Set 集計シート = ActiveSheet

と、オブジェクト変数の省略時の初期値であるNothingかどうかを判定し、
そこで実際にSetしたい省略時のオブジェクトを指定します。


定数でやった時と違い、「ありえない値にしておく」ことはできません。

オブジェクト変数の初期値であるNothingしか判定できませんので、
「偶然Nothingを渡してしまい、ActiveSheetに想定しない処理がされてしまう」
ことにならないよう、定数の時以上に注意する必要があります。

Optionalキーワードの使いどころ

どんなマクロでも使えるような、汎用関数の作成時に便利です。
↑のGet曜日などがそうですね。


汎用関数を、かゆいところに手が届く関数にしたいとき、
省略可能な引数で微妙な動きの違いを持たせる」のはすごく便利です。

たとえば先ほどのGet曜日関数では、

Function Get曜日(日付 As Date, Optional isカッコをつける As Boolean = False) As String
    
    If isカッコをつける Then
        Get曜日 = Format(日付, "(aaa)")
    Else
        Get曜日 = Format(日付, "aaa")
    End If

End Function

Debug.Print Get曜日(#2020/12/1#) ' ← 火 をくれる
Debug.Print Get曜日(#2020/12/1#,True) ' ← (火) をくれる

こんな感じで、関数にちょっとした機能をつけると、
汎用関数の使い勝手がすごく良くなります。

まさに「オプション機能」ですね。



逆に普通のマクロでは、あまりOptionalを乱用するのはやめましょう。

Optionalを使用してメインコードの分岐をコントロールすると、
改修などがあった際に、訳が分からなくなったり、バグの温床になります。

 

  • 処理がストレートでわかりやすい
  • 省略時の動きもわかりやすい

ような関数で使うと、威力を発揮する機能です。
ご利用は計画的に。

おまけ:Optionalと相性の良い機能・関数

Optionalキーワードを使用する際に、
組み合わせて使うと便利なものを紹介しておきます。

ユーザー辞書

よく使う人は、「おp⇒Optional 」の辞書変換を登録しておきましょう。
ほぼByValで運用する引数ですし、「おpv⇒Optional ByVal 」とかも。

最後に半角スペースを入れておくのがミソ。

IIf関数

IIf関数とは、

IIf(判定文, Trueの時の値, Falseの時の値)

と書く関数で、早い話「ワークシート関数IFのVBA版」です。


Optionalで省略可能にする引数は、メインで処理する変数ではなく、
プロパティの引数などに渡すだけの、サブ的なものが多いです。

このプロパティの設定にIIf関数を使うと、コードが見やすくなります。


例えば先ほどの曜日関数では、

Function Get曜日(日付 As Date, Optional isカッコをつける As Boolean = False) As String
    
    Get曜日 = Format(日付, IIf(isカッコをつける, "(aaa)", "aaa"))

End Function

こんな風にたった一行になって、中身も読みやすくなります。


プロパティや関数の、ある1つのパラメータだけを場合分けしたいとき」は、
IFステートメントで全部2回ずつ書くよりも、IIf関数でそのパラメータだけ分岐
した方が、見やすくて便利なことが多いです。

この機会に覚えてしまいましょう。

Boolean引数

さっきの「isカッコをつける」のように、ちょっとだけ違う動きをする関数を、Optional Boolean引数で表現するのはおすすめです。

読みやすいですし、選択肢が2値しかないので省略時の動きで混乱することも少ないです。


例えば冒頭の消費税の関数を、

Function Get税込価格(価格 As Long, Optional 消費税率 As Double = 0.1) As Long
' ⇩
Function Get税込価格(価格 As Long, Optional is軽減税率を適用する As Boolean = False) As Long

と変えることで、分岐の意図もわかりやすくなりますし、
いちいち0.08と打つ必要もなくなります。


あと他にぱっと思いつくものとして、
「xより大きいデータを抽出するような関数」を作ったとき、

Optional is境界値を含む As Boolean = False

という引数を使って、

条件式 = IIf(is境界値を含む, ">=", ">") & x

みたいに不等号をコントロールするのは、結構気に入っています。


余談と見せかけてこのブログのメインテーマですが、
この「is軽減税率を適用する」「is境界値を含む」のような関数の挙動を制御するBoolean変数を、

If is軽減税率を適用する Then

と、まるでただの文章のように読めるのは、日本語変数の強みです。


英語でやるには書き手にも読み手にも語学力を要しますが、母国語なら楽勝ですね。

是非使ってみてください。

名前付き引数

Debug.Print Get曜日(#2020/12/1#, True)

↑これを

Debug.Print Get曜日(#2020/12/1#, isカッコをつける:=True)

↑こう書くと、何をやっているかすごくよくわかります。

Boolean引数で関数を制御するときは、名前付き引数も積極的に使用してみてください。

おまけ:本当に引数が省略されたか調べる方法

さて、実は本文に書くかおまけに書くか迷った機能があります。
不用意に使ってほしくないという想いを込めて、おまけに書きます。
 

Sub データを集計する(Optional 集計シート As Worksheet)
    If 集計シート Is Nothing Then Set 集計シート = ActiveSheet

この「集計シートがNothingである」という状況が、
「引数が省略されたのか、Nothingが渡されたのかを注意すべし」
と書いてきました。

これをちゃんと調べる方法は無いのでしょうか?


実はこの機能はあり、「IsMissing」関数をつかうと、実際に引数が省略されたか調べることができます。

↓こんな感じ

Sub データを集計する(Optional 集計シート As Variant)
    If IsMissing(集計シート) Then Set 集計シート = ActiveSheet

 
さて、「Is Nothing」が「IsMissing」に変わった以外に、
何が変わっているか分かりますか?



正解は「引数の型」です。

Optional 集計シート As『Variant』になっていますね。


IsMissing関数は、Variantの引数しか判定できません

逆に言えば、IsMissing関数を使うには、型をVariantにする必要があるということです。


「省略されたのか、Nothingが渡されたのかの注意を怠った事故」より、
「Variantにしたことで、予期せぬオブジェクトが渡される事故」の方が怖い気がするので、IsMissingを使う場合は、しっかりと設計を考えた上で使ってください。

くれぐれも、適当にVariantにしてしまうことが無いようにしてくださいね。