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にしてしまうことが無いようにしてくださいね。