はじめに
どの現場でもExcelを触る機会って、結構あると思う。
色々な情報をExcelで管理してるもんだから、意図せずExcel VBA に詳しくなっていってしまう...
今回は、Excel VBA をある程度触れてきた人に向けての記事を書こうと思う。
経緯
開発してるVBAが思ってるより処理速度が遅くて悩んでいたのだが、
ログで見てみても、特段遅い処理は見つからない。
色々調べてみると、Collection
は値の取得が遅い なんて記事を見つけたもんだから、
じゃあCollection
自作しちゃおうかっていう思考に至って、今回の記事となった。
とはいえ、疑似的なCollection
作成なんてそんなに難しくもないよな~とか思って調べてみたけど、
意外と記事なんかは処理速度が遅そうな実装が多くて驚いた。
VBAなんてそもそも処理速度遅いんだから、本気で速度向上しないと不便なのにな~
開発方針
・Collection
の実態は動的配列とする
処理速度を上げたければ、配列を使用するのは定石。
ほかの記事でもやはり動的配列を実態としていた。
・使用感はほとんどCollection
クラスと同じとする
同じような仕様のほうがわかりやすいので。
・Remove
メソッドは実装しないこととする
動的配列は性質上、削除は苦手なんだよね~。
できなくはないけど、処理速度半端なく遅い場合があるから、今回は実装なしにしちゃう。
削除する場合はCollection
を使うほうがいいかな。
VBSの場合はScripting.Dictionary
で対応ね。
・配列変換が容易であること
せっかく実態が動的配列なら、その配列取得できないとメリットないよね?
ってことでこれは絶対外したくない。
実際、蓄積データをJoin
関数でくっつけたいとき多々あるし、あったら便利だよね。
2023/11/05 追記
コメントで、上部に記述すればするほど、若干処理速度が速くなるといただいたので、
使用頻度の高い順にメソッドを並び替えてます!
また、要素の追加において、単純にif文で実装したほうが、
エラーハンドリング張るより体感早かったので、
実装変えています!
ソースコード
ではでは早速ソースコードどーん!!!
Option Explicit
Private Const OBJECT_NAME As String = "ReaderCollection"
Private collectionArray() As String
Private rangeSize As Long
Private Count_ As Long
'要素の追加'
Public Sub Add(addItem As String)
Count_ = Count_ + 1
If Count_ > UBound(collectionArray) Then
ReDim Preserve collectionArray(1 To Count_ + rangeSize)
End If
collectionArray(Count_) = addItem
End Sub
'要素の取得'
Public Function Item(index As Long) As String
Item = collectionArray(index)
End Function
'要素の値更新 (Collectionにはないが、あったら便利かと思って実装)'
Public Sub Update(index As Long, updateItem As String)
collectionArray(index) = updateItem
End Sub
'要素数を返却'
Public Property Get Count() As Long
Count = Count_
End Property
'コンストラクタ'
Private Sub Class_Initialize()
Call Clear
End Sub
'指定要素数で初期化'
Public Sub Clear(Optional rangeSize_ As Long = 1024)
rangeSize = rangeSize_
ReDim collectionArray(1 To rangeSize)
Count_ = 0
End Sub
'配列の返却'
Public Function ToArray() As String()
ReDim Preserve collectionArray(1 To Count_)
ToArray = collectionArray
End Function
'コレクション内を文字列として取得 (ログ出力用)'
Public Function ToString() As String
ToString = "(" & OBJECT_NAME & ")[""" & Join(Me.ToArray(), """,""") & """]"
End Function
解説
メンバ変数
OBJECT_NAME
: ログ出力用のクラス名。別になくても問題ない。
collectionArray
: 実態の動的配列。今回はString型にしてみた。
rangeSize
: 動的配列の一回の拡張幅。Clear
メソッドで幅変更可能。
Count_
: 動的配列に追加した要素の数。
※動的配列の拡張回数を減らすため、要素を入れられなくなったタイミングで、
rangeSize
で指定の幅を追加していく。
そのため、Count_
は動的配列の実際の要素数とは異なる。
コンストラクタ
Clear
メソッドを呼び出して初期化を行う。
プロパティ
Count
: メンバ変数Count_
のGetter
。
※補足すると...
Getterとは、Private属性のメンバ変数にアクセスできるようにしたメソッド(VBAだとプロパティ)。
値の取得はできるが、書き換えができないようにしてる。
メソッド
Clear
: 動的配列の初期化を行う。引数を指定することで、配列拡張幅を指定できる。
最初から要素幅が確定してる場合は、引数指定してClear
メソッドを呼ぶことで、
不要な自動拡張を防げる。
Item
: 動的配列から要素を取得する。
Count_
以上の値を指定した場合に、エラーにならずに空文字が取得できてしまう
可能性があるので、使用時は注意すること。
今回は処理速度の向上を求めているので、不要なif文は加えていません。
通常、値の取得にはFor文でCountまで繰り返すため、if文は不要と考えました。
Add
: 引数に指定した値を要素に追加。動的配列の要素幅が足りない場合は自動拡張する。
自動拡張のためにif文を入れていますが、配列サイズを気にしなくていいのが
Collection
クラスの強みなので、ここではif文を入れざる負えませんでした。
Update
: 引数に指定されたインデックスの要素の値を、引数に指定した値で更新する。
Count_
以上の値を指定した場合に、エラーにならずに値更新できてしまう
可能性があるので、使用時は注意すること。
ToArray
: 動的配列のサイズをCount_
までに縮小し、返却する。
ToArray
使用後の値追加では必ず自動拡張されるので注意すること。
ToString
: 動的配列の内容を出力する。ログ取得したいときなんかにどうぞ。
ToString
メソッドはToArray
メソッドを実行するため、
次回値追加時に必ず自動拡張されるので注意すること。
処理速度
よく見かけるソースコードでは、配列拡張を値追加のたびに行っていたので、
多分めっちゃ遅いと思います。(違ってたら教えてください。)
今回は処理速度をめっちゃ考慮してるので、かなり早くなったはず。
てことで、既存のCollection
クラスと処理速度比較してみよか~。
比較データとして、3万データがある場合を想定しています。
以下が処理速度結果です。
自作のほうが値の蓄積が若干遅いように見えるけど、データ見るとCollection
も同じ時間かかってる時もあるし、この辺はほぼ差異なしとみていいと思う。
値の取得は段違いで早くなってますね。いや~よかったよかった。
配列の生成はほぼ無敵な速度ですね。
関数かませるときに配列変換したいときあると思うので、配列生成が早いのはうれしい誤算。
値を蓄積して、後で参照する。みたいなときは自作のほうがいいですね。
逆に追加と削除を繰り返す場合は、通常のCollection
のほうがいいかも
さいごに
もっと早い方法があるよ~とか、まちがってるよ~っていうことがあれば、ジャンジャン教えてください。