VBAの配列操作アレコレ
定期的にVBAでツールの作成依頼が来るのですが、大体は外部ファイルを読み込んで処理してExcelワークシートなりCSVに出力するような機能のものが多いです。
そこで頻出するのが配列に関する処理。
配列を使用すると、しない場合に比べて「応答なし」になる確率が下がり、処理時間も短くなることが多いです。
だけどVBAの配列は、本職Java屋の私にはちょっと癖がある!ので、汎用的な関数を作ってはため込んで再利用しています。
この記事では、よく使う配列操作用の自作関数をまとめました。
更新や追加等メンテは、随時行っていく予定です。
配列の要素数を取得
VBAには、配列の要素数を取得する関数が存在しません。UBound(配列)
関数は、配列の末尾の添え字(インデックス)を取得する関数であり、要素数の取得ではありません。
例えば、arr(0 to 3)
の配列に対してUBound(arr)
の結果は「3」です。
ですが、添え字は「0、1、2、3」で、要素数は「4」になります。arr(1 to 3)
であればUBound(arr)
の結果は「3」で要素数と一致しますが、先端の添え字をLBound(arr)
で取得して確認する必要があります。
また、LBound(arr)
、UBound(arr)
は、宣言したての動的配列(Dim arr()
)に対しては、実行時エラー「9」が発生します。
上記仕様により配列の要素数の取得には色々考慮が必要となるので、このような関数を作成しています。
' 配列の要素数を取得(要素数0対応)FunctionLength(ByValarrAsVariant)AsLongOnErrorGoToEMPTY_ARRIfIsArray(arr)ThenLength=UBound(arr)+(1-LBound(arr))ElseLength=-1EndIfExitFunctionEMPTY_ARR:IfErr.Number=9Then' インデックス範囲外エラーLength=0EndIfEndFunction
戻り値「-1」のケースについて
動的配列の宣言は、Dim arr()
でもDim arr
(またはDim arr As Variant
)でも可能です。
ポイントはDim arr
で、宣言した後にRedim
で配列型として初期化が可能です。
この場合、初期化前は配列ではありません。配列としてLBound(arr)
等の処理は実行時エラー「13」(型不正)となります。
そのため、配列でない(初期化前の)引数の場合は、「-1」を返しています。(必要に応じて戻り値を「0」にしても)
引数に初期化前の配列変数を渡さないことを保証できるのであれば不要な分岐ですが、汎用さを確保するためには必要です。Function Length(Byval arr() As Variant) As Long
のように配列型で宣言できればいいんですけども。
動的配列の最後に要素を追加
動的配列はRedim
で要素数を変更できます。
無条件でRedim arr(0)
等できない場合、初めて配列に要素を追加するタイミングで要素数を増やす必要があります。
新規に配列を作り直すわけではないので、Function
ではなくSub
にし、操作したい配列はByRef
で参照渡しにしています。
※この記事で紹介しているLength
関数を組み合わせて使っています。
' 動的配列の最後に要素を追加' arr:追加先の配列' value:追加する要素SubAddLast(ByRefarrAsVariant,ByValvalueAsVariant)IfIsEmpty(arr)OrLength(arr)<1Then' 初期化前または要素数=0の場合ReDimarr(0)ElseReDimPreservearr(UBound(arr)+1)EndIfarr(UBound(arr))=valueEndSub
初期化前、または要素数が0の場合について
空の配列変数に対して、LBound
関数およびUBound
関数は、実行時エラー「9」が発生します。なので空判定には使用できません。Dim arr()
で宣言している配列変数は、Redim
で初期化前でもIsEmpty(arr)
の結果がFalse
になります。
そのため、IsEmpty(arr)
だけではなく、Sgn(arr) = 0
または(Not Not arr) = 0
でも判定が必要です。
これで解決!と思いうじゃないですか。
でも、要素数が1以上の()
なし(型指定なしまたはVariant型)で宣言した引数を渡すと、実行時エラー「13」が発生します…
そんなわけで上で説明しているLength
関数を使用しているのですが、ループ内で大量に呼ぶと何回かに1回4秒くらいかかって激重・激遅になります。
(大量に呼び出して処理時間をDebug.Printしてみたら、0秒 Or 3.9秒がランダムに出力された…何でばらつく??)
ですので、実際使用する場合は、呼び出し元の配列変数の宣言時の型と引数の配列の空判定方法の組み合わせを、以下のどちらかのパターンに寄せたほうがいいです。
- 配列変数を
Dim arr As Variant
のように()
なしのVariant型で宣言し、空判定をIsEmpty(arr)
で行う - 配列変数を
Dim arr() As 型
のように()
ありで宣言し、空判定をSgn(arr) = 0
または(Not Not arr) = 0
で行う
私は大体1ですね。IsEmpty(arr)
は見た目で何やってるかわかりやすいので。
しかし何つー仕様だMicr○soft。Variant型の配列としての空判定は必ずOn Error GoTo ~
使えってか…()
なしならRedim
で配列として初期化できないようにしろよお…
可変長配列は、セルに直接代入できないけどCollection
オブジェクト、またはVB.NetのArrayList
オブジェクト使うって手もあるんですけどもね。これらはNew
したらAdd
で追加できるので、もろもろの考慮不要です。
ジャグ配列(入れ子の配列)を二次元配列に変換
ループでセルひとつひとつに出力すると重いので、セルに出力する処理は、配列を作って1回でセルに出力することが多いです。
そんな時に使用する多次元配列。VBAのいう多次元配列は、Java等の多次元配列と違い、ジャグ配列とは異なります。(ジャグ配列だとセルにそのまま出力不可)
ただ、VBAの多次元配列って、可変長なのは末尾の次元(ジャグ配列でいう入れ子の最下層)のみなんですよ。
CSVを読み込んで処理して必要な行だけセルに出す、なんてありがちな処理は、予め出力行数がわかってないと配列を宣言できないし、かといってCSVがクソデカファイルだとFileSystemObject
を使用して行数だけを取得するにしても、処理時間がかかる。
なので、可読性と速さのバランスを取ると、ジャグ配列で処理後、セルへの出力時に二次元配列に変換するのが一番わかりやすいなって気がします。そんな時に使用するのがこの関数です。
' ジャグ配列を二次元配列に変換FunctionConv2DemintionalArray(ByValsrcArrAsVariant)AsVariantDimresultAsVariant:ReDimresult(UBound(srcArr),UBound(srcArr(0)))DimiAsLong:i=0DimrowAsVariant:ForEachrowInsrcArrDimjAsLong:Forj=0ToUBound(srcArr(0))result(i,j)=row(j)Nexti=i+1NextConv2DemintionalArray=resultEndFunction
VBA書くなら配列の処理は頻出だと思うので、どうにか覚えていくしかないんですよね…