VBA Collection を自作して処理速度を向上してみた

# はじめに
どの現場でもExcelを触る機会って、結構あると思う。
色々な情報をExcelで管理してるもんだから、意図せずExcel VBA に詳しくなっていってしまう...
今回は、Excel VBA をある程度触れてきた人に向けての記事を書こうと思う。

# 経緯
開発してるVBAが思ってるより処理速度が遅くて悩んでいたのだが、
ログで見てみても、特段遅い処理は見つからない。
色々調べてみると、**`Collection`は値の取得が遅い** なんて記事を見つけたもんだから、
じゃあ`Collection`自作しちゃおうかっていう思考に至って、今回の記事となった。
とはいえ、疑似的な`Collection`作成なんてそんなに難しくもないよな～とか思って調べてみたけど、
意外と記事なんかは処理速度が遅そうな実装が多くて驚いた。
VBAなんてそもそも処理速度遅いんだから、本気で速度向上しないと不便なのにな～

# 開発方針
##### ・`Collection`の実態は動的配列とする
処理速度を上げたければ、配列を使用するのは定石。
ほかの記事でもやはり動的配列を実態としていた。
##### ・使用感はほとんど`Collection`クラスと同じとする
同じような仕様のほうがわかりやすいので。
##### ・`Remove`メソッドは実装しないこととする
動的配列は性質上、削除は苦手なんだよね～。
できなくはないけど、処理速度半端なく遅い場合があるから、今回は実装なしにしちゃう。
削除する場合は`Collection`を使うほうがいいかな。
VBSの場合は`Scripting.Dictionary`で対応ね。
##### ・配列変換が容易であること
せっかく実態が動的配列なら、その配列取得できないとメリットないよね？
ってことでこれは絶対外したくない。
実際、蓄積データを`Join`関数でくっつけたいとき多々あるし、あったら便利だよね。

# 2023/11/05 追記
コメントで、上部に記述すればするほど、若干処理速度が速くなるといただいたので、
使用頻度の高い順にメソッドを並び替えてます!
また、要素の追加において、単純にif文で実装したほうが、
エラーハンドリング張るより体感早かったので、
実装変えています!

# ソースコード
ではでは早速ソースコードどーん!!!
``` ReaderCollection.cls

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万データがある場合を想定しています。
以下が処理速度結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3113190/fabdd71b-b527-4897-e139-dc35a612d482.png)
自作のほうが値の蓄積が若干遅いように見えるけど、データ見ると`Collection`も同じ時間かかってる時もあるし、この辺はほぼ差異なしとみていいと思う。
値の取得は段違いで早くなってますね。いや～よかったよかった。
配列の生成はほぼ無敵な速度ですね。
関数かませるときに配列変換したいときあると思うので、配列生成が早いのはうれしい誤算。
値を蓄積して、後で参照する。みたいなときは自作のほうがいいですね。
逆に追加と削除を繰り返す場合は、通常の`Collection`のほうがいいかも

# さいごに
もっと早い方法があるよ～とか、まちがってるよ～っていうことがあれば、ジャンジャン教えてください。