和風スパゲティのレシピ

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

引数を好きな個数渡せる関数を作る-ParamArray

Sub、Functionなどのプロシージャを作成するとき、
引数の数を固定せず、任意の数の引数を渡せるようにする方法を解説します。

やりたいこと

ワークシート関数の「SUM」をイメージしてください。

A5 =SUM(A1:A4)
A5 =SUM(A1,A2,A3,A4)

これはどちらもA1~A4の4セルを合計します。


このようにSUM関数は、自由な数の引数を渡すことができます。

これと同じ様に、引数の数を関数側で決めるのではなく、
呼び出すときに好きな数だけ渡せるプロシージャを作成する方法を解説します。

目的関数のサンプル

Debug.Print 足し算(1,2,3) ' ← 6を返す
Debug.Print 足し算(1,3,5,7) ' ← 16を返す
Debug.Print 足し算(1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1) ' ← 16を返す

こんな関数を例に説明していきます。もちろん渡す引数の数は自由です。

先に代替案から -Oprtionalでゴリ押しする

本題に入る前に、先に簡単な代替案から紹介します。


引数を省略可能にする、「Optionalキーワード」というものがあります。

これを使って、自由な数の引数を受け取る関数を↓のように作れます。

Function 足し算(数値1 As Long, Optional 数値2 As Long = 0 _
    , Optional 数値3 As Long = 0, Optional 数値4 As Long = 0 _
    , Optional 数値5 As Long = 0) As Long

    足し算 = 数値1 + 数値2 + 数値3 + 数値4 + 数値5

End Function

Debug.Print 足し算(1,2,3) ' ← 6を返す
Debug.Print 足し算(1,3,5,7) ' ← 16を返す
Debug.Print 足し算(1,1,1,1,1,1,1,1) ' ← エラー「引数の数が一致していません」


まあ見ればわかる通り、まったく自由ではありません(笑)
引数の数は「1個~5個」しか許していませんからね。


ただ、実務ではこれで十分なことも多いんですよ。
自由な数とはいっても、実務上のだいたいの上限はありますからね。


これから説明する本命の「ParamArrayキーワード」は配列を使用するため、
配列に慣れていない方には、ハードルが高いかもしれません。

また、引数の型がVariantしか指定できないため、
データ型に気を使う場合は、意外と面倒があります。


実際に使う引数の数が、多くても5個とかくらいなら、
ちゃちゃっとOptionalで書いた方がいいことも多いでしょう。

6個必要になったら、その時関数を書き換えても、
それでも前の関数と互換性があるのがOptionalのいいところですしね。


まずはそれを念頭に置いた上で、本題に入りましょう。
(あるいはもうブラウザを閉じて、Optionalを書き始めてもOK)

引数の数を可変にする -ParamArrayキーワード

先ほどの「足し算」関数に、本当に「好きな数だけ引数を渡す」には、
ParamArrayキーワードを使用して以下の通り書きます。

Function 足し算(ParamArray pArr数値()) As Long
    
    足し算 = 0
    Dim i As Long
    For i = LBound(pArr数値) To UBound(pArr数値)

        足し算 = 足し算 + pArr数値(i)

    Next

End Function

Debug.Print 足し算(1,3,5,7,9,11,12) ' ← 48が返ります

 
コードの通り、関数を呼び出す側では複数の引数として渡すことができ、
プロシージャ内で実際に使うときは配列として扱うことができます。

任意の数の引数を、1つの配列に変換して関数内に渡しているイメージですね。


使う上での注意点ですが、前述の通り、ParamArrayはデータの型を指定できません。
常にVariant型の配列になります。

その他のルールとして、

  • ParamArrayは最後の引数でのみ指定可能
  • Optionalと併用不可
  • ByVal/Refの指定不可

などの制約があります。


制約が多すぎてもう上記サンプル以外で書けないくらいなので、
↑の足し算をテンプレートに、書き替えて使ってもいいくらいですね。


ちなみに、「ParamArray 数値」と書くとエラーになります。
配列を表す()がないとダメなので注意してください。

どうせそうとしか書けないんだから気を利かせて補完しろや。


ParamArray引数以外の引数も使用する場合は、
ParamArray引数の前に書けばよいです。

Function 足し算(数値1 As Long, ParamArray pArr数値()) As Long

 
ParamArray引数自体を省略する(1個も渡さない)こともできるため、
例えば「最低一つは数値を受け取る」仕様にしたいときは、
↑のように普通の引数を追加します。

ParamArrayを丸ごと省略した時の動き

ParamArray引数を1つも渡さなかった場合は、「配列の上限Uboundが-1で、要素はなし」という、いわゆる空の配列(≒Array())がセットされます。


今回の足し算の場合は、

For i = LBound(pArr数値) To UBound(pArr数値)

この部分が For 0 To -1 となりますね。

Forステートメントには、「初期値より最終値が小さい場合は1度も処理しない」という性質がありますので、エラーにはならずにしっかりと0は返ります。


Forの性質を使うのではなく、普通にParamArray引数が省略されたか調べたい場合は、

Function 足し算(ParamArray pArr数値()) As Long
    If UBound(pArr数値) = -1 Then
    ' ↕ どちらかで判定
    If IsMissing(pArr数値) Then
        
        MsgBox ("最低1つは数値を渡してください。")
        Exit Function
        
    End If

このように、Variant引数が省略されたか調べる関数「IsMissing関数」か、
先ほどの「UBound = -1」を判定して例外処理を書いてください。

使いどころ

同じ型の値をたくさん受け取り、それをまとめるような処理に長けています。

例えば、

Function カンマ区切り(ParamArray pArrテキスト()) As String
    
    カンマ区切り = ""
    Dim i As Long
    For i = LBound(pArrテキスト) To UBound(pArrテキスト)

        カンマ区切り = カンマ区切り & "," & pArrテキスト(i)

    Next

    カンマ区切り = Mid(カンマ区切り, 2) ' 頭の,を消す
End Function

Debug.Print カンマ区切り("みかん","りんご","いちご") ' ←"みかん,りんご,いちご" が返る

こんな汎用関数を作れます。

すごく単純な関数ですが、「 & "," & 」これをたくさん書くのに辟易とした経験がある方は、結構便利に感じると思います。

入力規則のリストだったり、並び替えの順序指定など、Excelには結構カンマ区切りテキストを使うプロパティがありますからね。


ちなみにこのカンマ区切り関数を作る場合は、↓のように、

Function カンマ区切り(ParamArray pArrテキスト()) As String
    カンマ区切り = Join(pArrテキスト, ",")
End Function

と、配列を引数に持つ関数(今回はJoin)を使うこともできます。


これでも「"みかん,りんご,いちご"」が返りますので、
カンマ区切りの汎用関数を持っていきたい方はこちらをどうぞ。


このように、ParamArrayキーワードは、
同じ型の変数をまとめるような汎用関数の作成に便利です。

おまけ:Variant型であることの注意点

先ほどのカンマ区切り関数を、

セルテキストの取得

↑こんなシートで、

Debug.Print カンマ区切り(Range("A1"), Range("A2"), Range("A3"))

こう呼んでみましょう。

"みかん,りんご,いちご" が返り、
一見さっきと同じ様に動いているように見えます。


が、実際の関数の中では、先ほどとは全く違う動きをしています。


何が違うかわかりますか?


正解は、Variantの引数にオブジェクトを渡したことになっているため、
[A1セル, A2セル, A3セル]という、3つのRangeオブジェクトを格納した配列が渡されています。

[String, String, String]という配列を渡したように錯覚してしまいがちですが、違うんですね。

実際に"みかん"という文字列=Valueプロパティが取得されるのは、
引数を渡したときではなく、関数内で参照されたときです。


これで何か実害があるかというと微妙なラインですが、

Function カンマ区切り(ParamArray pArrテキスト()) As String
    Range("A1").Value = "いよかん"
    カンマ区切り = Join(pArrテキスト, ",")
End Function

こんな関数を作った時に、

Debug.Print カンマ区切り(Range("A1").Value, Range("A2").Value, Range("A3").Value)
' ↑ みかん,りんご,いちご が返る

Debug.Print カンマ区切り(Range("A1"), Range("A2"), Range("A3"))
' ↑ いよかん,りんご,いちご が返る

と、.Valueの有無で結果が変わるようになります。

(ついでに、Join関数がオブジェクトの配列にもうまく動いてくれていることもわかります)


まあこんな書き方をして実害に遭遇することはめったにないでしょうけどね。

一応心の片隅に置いておくと、いつか何かあった時に解決の糸口になるかもしれません。


ちなみにVariantにセルを渡すとRangeオブジェクトとして渡ったのは、
「関数の引数」だったからです。


普通にコード上で代入する場合は、

Dim x As Variant

x = Range("A1") ' TypeName(x) = String

Set x = Range("A1") ' TypeName(x) = Range

と、Setキーワードの有無で、オブジェクトが渡されるか、既定のプロパティが渡されるかが決まります。

この仕様も、心の片隅に置いておきましょう。