VB6やVBAでの配列とかコレクションのお話

プログラムを作成する上で欠かせないプログラムの機能の一つが、複数の値をまとめて扱える配列やコレクションです。
しかし、VB6 やVBA では配列の扱いに癖があり、ハマってしまったり、汚いプログラムになりがちです。
今回は、そんなVB6 やVBA での配列やコレクションの使い方について解説します。

# 1.配列の使い方
基本的な使い方自体は難しくありません。

配列を使うと、10の値を1つの変数にまとめて扱ったり、配列自体の長さを変更することで数の分からないデータでも効率よく扱うことができます。
```VisualBasic
Private Sub cmdArrayTest_Click()
    'あらかじめ配列数を指定して配列を作成
    '配列1(0)～配列1(5)までの6個の値を保持することができる
    Dim 配列1(5)    As Long
    Dim LoopIndex   As Long
    '配列の下限はLBound([配列])、配列の上限はUBound([配列])で取得できる
    'なぜ下限を取得する必要があるかというと、「Option Base 1」が指定された場合、下限が1から始まるようになるため
    For LoopIndex = LBound(配列1) To UBound(配列1)
        配列1(LoopIndex) = LoopIndex + 1
    Next
    '固定 1 2 3 4 5 6
    Call WriteLog("固定", 配列1)
    
    '配列数は決めず後で決める
    Dim 配列2()     As Long
    'ReDimで配列の領域を確保
    ReDim 配列2(5)
    For LoopIndex = LBound(配列2) To UBound(配列2)
        配列2(LoopIndex) = LoopIndex + 1
    Next
    '可変 1 2 3 4 5 6
    Call WriteLog("可変", 配列2)
    
    '後から決める場合、一度作った配列の大きさを変えられる
    'Redimで再度サイズを変更した場合、既存の配列の中身はクリアされる
    ReDim 配列2(8)
    '可変(Redim) 0 0 0 0 0 0 0 0 0
    Call WriteLog("可変(Redim)", 配列2)

    For LoopIndex = LBound(配列2) To UBound(配列2)
        配列2(LoopIndex) = LoopIndex + 1
    Next
    '中身をそのままで大きさを変えたい場合は、Redim Preserveとする。
    '小さくする場合は、新しいサイズ以降の中身は切り捨てられる
    ReDim Preserve 配列2(10)
    '可変(Redim Preserve) 1 2 3 4 5 6 7 8 9 0 0
    Call WriteLog("可変(Redim Preserve)", 配列2)
End Sub

Private Sub WriteLog(ByRef Caption As String, ByRef Values() As Long)
    Debug.Print Caption
    
    Dim LoopIndex   As Long
    For LoopIndex = LBound(Values) To UBound(Values)
        '最後に";"をつけると改行なしになる
        Debug.Print Values(LoopIndex) & " ";
    Next
    Debug.Print ""
End Sub
```
# 2.コレクションを使う
コレクションは配列の高機能版のような存在で、途中に要素を挿入したり、途中の要素だけを削除したりすることができます。

ディクショナリは、Key とValue というペアで値を管理することができ、Key を指定して対応するValue を取得したり、Key が存在するかを確認したりすることができます。
```VisualBasic
Private Sub cmdCollection_Click()
    Dim コレクション As Collection
    Set コレクション = New Collection
    Call コレクション.Add("値1")
    Call コレクション.Add("値2")
    Call コレクション.Add("値3")
    Call コレクション.Add("値4")
    Dim LoopIndex   As Long
    '配列と同じようにForでアクセスできます。
    'コレクション(For) 値1 値2 値3 値4
    Debug.Print "コレクション(For)";
    For LoopIndex = 0 To コレクション.Count - 1
        Debug.Print " " & コレクション(LoopIndex + 1);
    Next
    Debug.Print ""
    
    Dim Item        As Variant
    'For Eachを使って要素を順番に取り出すことができます。
    'コレクション(For Each) 値1 値2 値3 値4
    Debug.Print "コレクション(For Each)";
    For Each Item In コレクション
        Debug.Print " " & Item;
    Next
    Debug.Print ""
    
    '途中に要素を挿入
    Call コレクション.Add("値5", , 2)
    'コレクション(値5を挿入) 値1 値5 値2 値3 値4
    Debug.Print "コレクション(値5を挿入)";
    For Each Item In コレクション
        Debug.Print " " & Item;
    Next
    Debug.Print ""
    
    '途中の要素を削除
    Call コレクション.Remove(3)
    'コレクション(値2を削除) 値1 値5 値3 値4
    Debug.Print "コレクション(値2を削除)";
    For Each Item In コレクション
        Debug.Print " " & Item;
    Next
    Debug.Print ""
    
    '「プロジェクト(P)」→「参照設定(N)...」で、「Microsoft Scripting Runtime」を追加
    Dim ディクショナリ As Dictionary
    Set ディクショナリ = New Dictionary
    Call ディクショナリ.Add("Key1", "値1")
    Call ディクショナリ.Add("Key2", "値2")
    Call ディクショナリ.Add("Key3", "値3")
    Call ディクショナリ.Add("Key4", "値4")
    
    'For Each(Pair) Key1=値1 Key2=値2 Key3=値3 Key4=値4
    Debug.Print "For Each(Pair)";
    For Each Item In ディクショナリ.Keys
        Debug.Print " " & Item & "=" & ディクショナリ(Item);
    Next
    Debug.Print ""
    
    'Keyが存在するかを取得
    'True,False
    Debug.Print ディクショナリ.Exists("Key1") & "," & ディクショナリ.Exists("Key5")
End Sub
```

# 3.VB6, VBA での配列の問題点
VB6, VBA の配列の最大の問題点は、配列が確保されていない状態での扱いずらさです。

配列が確保されていない状態でUBoundで配列のサイズを知ろうとするとエラーになってしまいます。
```VisualBasic
Private Sub cmdArrayTest2_Click()
    Dim 配列1()     As Long
    'エラー「9:インデックスが有効範囲にありません。」
    Debug.Print UBound(配列1)
End Sub
```
そこでよく見られるのが以下のような方法
```VisualBasic
Private Sub cmdTest2_Click()
    'Indexを別変数で管理(2つの変数を管理しなければならずバグの元になる)
    Dim 配列1のIndex    As Long
    配列1のIndex = -1
    Dim 配列1()         As Long
    If 配列1のIndex < 0 Then
        配列1のIndex = 0
        ReDim 配列1(配列1のIndex)
    End If
    
    '初期化されているかフラグを設ける(Indexと同様に2つの変数を管理しなければならない)
    Dim Is配列2Init     As Boolean
    Dim 配列2()         As Long
    If Not (Is配列2Init) Then
        ReDim 配列2(0)
    End If
    
    '0を使わない(0番目の扱いに注意しなければならない)
    Dim 配列3()         As Long
    ReDim 配列3(0)
    If UBound(配列3) = 0 Then
        ReDim 配列3(UBound(配列3) + 1)
    End If
    
    
    'On Errorで判定(エラー無視するのは行儀がいい行為ではないのと、エラートラップオプションで「エラー発生時に中断」に設定されているとそこで処理が中断されるためデバッグしずらい)
    On Error Resume Next    'エラーが発生しても次のステートメントへ進む
    Dim 配列4のIndex    As Long
    Dim 配列4()         As Long
    '通常はここでエラーが発生して動作が停止するが、On Error Resume Nextによって次のステートメントに処理が移る
    配列4のIndex = UBound(配列4)
    'エラーが発生していたらまだ未初期化の状態でアクセスしたことになる
    If Err.Number <> 0 Then
        配列4のIndex = -1
    End If
    On Error GoTo 0         'On Error Resume Nextを元に戻す
End Sub
```
### 3.1.型別回避方法
この問題に対する回避方法として、以下の方法を使うことで確保されていない状態でUboundを取得すると、戻り値が-1になる配列を作ることができます。
[VBA標準機能で作成できる要素数0の配列](https://qiita.com/nukie_53/items/9a7a1eb07eff50ae1e8b)

例えば、こんな標準モジュールを定義しておくことで、配列の初期化が行えます。
```VisualBasic:mdlArrayInit.bas
Option Explicit
Private Declare Function SafeArrayAllocDescriptor Lib "oleaut32" (ByVal cDims As Long, ByRef ppsaOut() As Any) As Long

Public Function InitByte() As Byte()
    'String型は内部的にはByte型の配列として扱われており、相互に代入が可能らしい
    InitByte = vbNullString
End Function

Public Function InitBoolean() As Boolean()
    Call SafeArrayAllocDescriptor(1, InitBoolean)
End Function

Public Function InitInteger() As Integer()
    Call SafeArrayAllocDescriptor(1, InitInteger)
End Function

Public Function InitLong() As Long()
    Call SafeArrayAllocDescriptor(1, InitLong)
End Function
    
Public Function InitSingle() As Single()
    Call SafeArrayAllocDescriptor(1, InitSingle)
End Function
    
Public Function InitDouble() As Double()
    Call SafeArrayAllocDescriptor(1, InitDouble)
End Function
    
Public Function InitCurrency() As Currency()
    Call SafeArrayAllocDescriptor(1, InitCurrency)
End Function
    
Public Function InitDate() As Date()
    Call SafeArrayAllocDescriptor(1, InitDate)
End Function

Public Function InitString() As String()
    'Split関数に長さ0の文字列を渡す
    InitString = Split(vbNullString, vbNullChar)
End Function

Public Function InitVariant() As Variant()
    'Array関数を引数なしで実行
    InitVariant = Array()
End Function
```
```VisualBasic
Option Explicit
Private Sub cmdArrayTest2_Click()
    Dim 配列Byte()      As Byte
    配列Byte = InitByte
    'Byte():0 To -1
    Debug.Print TypeName(配列Byte) & ":" & LBound(配列Byte) & " To " & UBound(配列Byte)
    
    'Boolean
    Dim 配列Boolean()   As Boolean
    配列Boolean = InitBoolean
    'Boolean():0 To -1
    Debug.Print TypeName(配列Boolean) & ":" & LBound(配列Boolean) & " To " & UBound(配列Boolean)
    
    'Integer
    Dim 配列Integer()   As Integer
    配列Integer = InitInteger
    'Integer():0 To -1
    Debug.Print TypeName(配列Integer) & ":" & LBound(配列Integer) & " To " & UBound(配列Integer)
    
    'Long
    Dim 配列Long()      As Long
    配列Long = InitLong
    'Long():0 To -1
    Debug.Print TypeName(配列Long) & ":" & LBound(配列Long) & " To " & UBound(配列Long)
    
    'Single
    Dim 配列Single()    As Single
    配列Single = InitSingle
    'Single():0 To -1
    Debug.Print TypeName(配列Single) & ":" & LBound(配列Single) & " To " & UBound(配列Single)
    
    'Double
    Dim 配列Double()    As Double
    配列Double = InitDouble
    'Double():0 To -1
    Debug.Print TypeName(配列Double) & ":" & LBound(配列Double) & " To " & UBound(配列Double)
    
    'Currency
    Dim 配列Currency()  As Currency
    配列Currency = InitCurrency
    'Currency():0 To -1
    Debug.Print TypeName(配列Currency) & ":" & LBound(配列Currency) & " To " & UBound(配列Currency)
    
    'Date
    Dim 配列Date()      As Date
    配列Date = InitDate
    'Date():0 To -1
    Debug.Print TypeName(配列Date) & ":" & LBound(配列Date) & " To " & UBound(配列Date)
    
    Dim 配列String()    As String
    配列String = InitString
    'String():0 To -1
    Debug.Print TypeName(配列String) & ":" & LBound(配列String) & " To " & UBound(配列String)
    
    Dim 配列Variant() As Variant
    配列Variant = InitVariant()
    'Variant():0 To -1
    Debug.Print TypeName(配列Variant) & ":" & LBound(配列Variant) & " To " & UBound(配列Variant)
End Sub
```

# 4.まとめ
VB やVBA で地味に残念なのがこの配列の仕様です。
これが無ければ今まで見てきたVB6 のコードもだいぶマシだったのではないかと思ってしまいます。