プログラムを作成する上で欠かせないプログラムの機能の一つが、複数の値をまとめて扱える配列やコレクションです。
しかし、VB6 やVBA では配列の扱いに癖があり、ハマってしまったり、汚いプログラムになりがちです。
今回は、そんなVB6 やVBA での配列やコレクションの使い方について解説します。
1.配列の使い方
基本的な使い方自体は難しくありません。
配列を使うと、10の値を1つの変数にまとめて扱ったり、配列自体の長さを変更することで数の分からないデータでも効率よく扱うことができます。
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 が存在するかを確認したりすることができます。
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で配列のサイズを知ろうとするとエラーになってしまいます。
Private Sub cmdArrayTest2_Click()
Dim 配列1() As Long
'エラー「9:インデックスが有効範囲にありません。」
Debug.Print UBound(配列1)
End Sub
そこでよく見られるのが以下のような方法
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の配列
例えば、こんな標準モジュールを定義しておくことで、配列の初期化が行えます。
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
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 のコードもだいぶマシだったのではないかと思ってしまいます。