3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【VBA】可変長引数の罠

Last updated at Posted at 2020-07-20

可変長引数とは

可変長引数とは、引数の個数が固定ではなく、任意の個数を渡せる引数です。
例を挙げると2016で追加されたワークシート関数のCONCAT関数。この関数は渡された引数を全て文字列結合した値を返します。
VBAで可変長引数のプロシージャを実装したい場合、引数をByValByRefではなく、ParamArrayを付けて宣言します。
渡されたn個の引数は、ParamArrayで宣言した引数にまとめられて、プロシージャの中では配列として扱うことができます。

ParamArrayのおやくそく

  • Variant型の配列として宣言する(ParamArray 引数名() As Variantのように明示的に型まで指定を推奨)
  • ひとつのプロシージャにつきひとつだけ宣言できる
  • 他にも引数がある場合、末尾でのみ宣言できる
  • ParamArrayに先行する引数は、省略可能にできない(Optionalを付けた引数とは共存できない)
  • 配列またはByRefVariant型を想定している別のプロシージャには、引数として渡せない(一度Variant型の変数に格納してから渡す必要がある)
  • ParamArrayの引数に対して、EraseまたはRedimを使用できない
  • ParamArrayの引数の配列は、Option Base 数値の影響を受けない(LBound関数の戻り値は必ず「0」)
  • 渡された引数が0個の場合、ParamArrayの引数の配列のUBound関数の戻り値は、「-1」となる

ParamArrayで可変長引数を受け取る

Aさんは、下記の指示を受けて、Concatプロシージャを実装しました。

複数の文字列を指定した区切り文字を挟んで連結した文字列を返却するプロシージャを実装する。
渡す文字列の個数は任意に設定できること。
渡した文字列が0個の場合は、空文字列を返却すること。

Option Explicit

Sub Test()
    Debug.Print "値なし" & Concat("/")
    Debug.Print Concat("/", "あああ", "いいい", "ううう")
End Sub

' 指定された区切り文字を挟みながら渡された文字列を全て結合する
Private Function Concat(ByVal separator As String, ParamArray values() As Variant) As String
    Concat = vbNullString
    Dim value As Variant: For Each value In values
        If 0 < Len(Concat) Then
            Concat = Concat & separator
        End If
        Concat = Concat & value
    Next
End Function

実行結果

値なし
あああ/いいい/ううう

ParamArrayの扱われ方

ConcatConcat = vbNullStringにブレークポイントを設定し、実行中の引数をローカルウィンドウで表示してみましょう。
呼出し時の引数が、先頭インデックス0の配列としてvaluesにすべて渡されたのが確認できます。
image.png

ParamArrayからParamArrayへ

先ほどのConcatプロシージャですが、以下の要望が出ました。

区切り文字は大抵決まった文字でしか呼び出さないので、省略したい。
Optionalを使えば固定の1種類なら実現できるけど、「,」と「-」の2種類から選べるようにしたい。

ParamArrayで受け取った引数を別のプロシージャのParamArrayの引数に渡す

Aさんは上記の要望を受けて、下記のようなプロシージャを2つ追加することにしました。

' 区切り文字「,」で文字列結合
Private Function ConcatComma(ParamArray values() As Variant) As String
    ConcatComma = Concat(",", values)
End Function

' 区切り文字「-」で文字列結合
Private Function ConcatHyphen(ParamArray values() As Variant) As String
    ConcatHyphen = Concat("-", values)
End Function

Testプロシージャにも上記のプロシージャを追加して…

Sub Test()
    Debug.Print "値なし" & Concat("/")
    Debug.Print Concat("/", "あああ", "いいい", "ううう")
    Debug.Print ConcatComma("かかか", "ききき", "くくく")
    Debug.Print ConcatHyphen("さささ", "ししし", "すすす")
End Sub

実行結果

image.png
コンパイルエラーは起きなかったけど、実行エラーが発生してしまいました。
エラーはConcatプロシージャのConcat = Concat & valueの行で発生しています。
String型しかないのにどうして?

こんな時は、ローカルウィンドウで変数を確認してみましょう。
image.png
ParamArrayの引数valuesジャグ配列(入れ子の配列)になってる…
そうです。ParamArrayで受け取った引数をParamArrayに渡すと、受け取った側はもう一度配列でラップしてしまうのです。
これではParamArrayで渡すたびに配列の入れ子が深くなってしまう…
エラー内容としては、文字列と配列を&演算子で結合しようとしているから。
だから型が一致しないエラーが発生していたんですね。

解決方法

解決方法は、2つあります。

1. Concatプロシージャの引数をParamArrayではなく、普通の配列にする

Private Function Concat(ByVal separator As String, ByVal values As Variant) As Stringに変更します。
この解決法は簡単ですが、呼び出し側で必ず配列にしてから渡すことになり、ひと手間増えます。
直接Concatプロシージャを呼び出している箇所は、すべてArray関数で括って配列に変換するように修正する必要が出てきます。
既存のコードに呼び出し箇所がたくさんある場合、大変ですね…

2. ParamArrayの引数について、配列の入れ子を解消する

ParamArrayの引数を持つプロシージャでは、本来の処理を行う前に、配列のネストが一番深い配列を取り出して入れ子を解消する必要があることがわかりました。
下記の実装例では、複数回ParamArrayで渡された場合の考慮として再帰呼出しを行うことで、ネストの一番深い配列を返却できるようにしました。
Do~Loopで実装できなくもないのですが、VBAはショートサーキット演算子がないので。
誤って配列じゃない引数は配列でラップして返却、明らかにParamArray以外での入れ子の配列は未編集で返却します。

' 可変長引数を可変長引数として渡した場合の対応
Private Function NormalizeParamArray(ByVal values As Variant) As Variant
   NormalizeParamArray = values
    If Not IsArray(values) Then
        ' 配列以外の場合、ラップして配列で返却
        NormalizeParamArray = Array(values)
        Exit Function
    End If
    If Not (UBound(values) = 0 And LBound(values) = UBound(values)) Then
        ' 要素数1以外、または先頭インデックスが「0」以外の場合、そのまま返却
        Exit Function
    End If
    If IsArray(values(0)) Then
        ' 入れ子の配列の場合、再帰呼出し
        NormalizeParamArray = NormalizeParamArray(values(0))
        Exit Function
    End If
End Function

ConcatプロシージャでもConcatCommaConcatHyphenプロシージャに追加するのでも構いませんが、今回はConcatプロシージャに追加します。
ParamArrayの引数は、ByValとしてしか扱えません。1
副作用がありませんので、変換結果をvaluesに上書き代入しています。

Private Function Concat(ByVal separator As String, ParamArray values() As Variant) As String
    values = NormalizeParamArray(values)	' ここに追加
    Concat = vbNullString
    Dim value As Variant: For Each value In values
        If 0 < Len(Concat) Then
            Concat = Concat & separator
        End If
        Concat = Concat & value
    Next
End Function

実行結果

エラーも発生せず、意図した通りの実行結果になりました。

値なし
あああ/いいい/ううう
かかか,ききき,くくく
さささ-ししし-すすす

多言語、例えばJavaは、可変長引数を別のメソッドにそのまま渡しても、配列でラップされません。
VBAでは、割と多言語の知識が邪魔をしますね…

  1. ParamArrayで渡されたのが値の場合。渡されたのが配列丸ごとである場合、ByRefとなり副作用が発生する。ByValとして渡したい場合、ParamArrayに限ったことではないが、各配列(の変数名)を()で括る。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?