Collection と For Each
VBA では、要素数が未知の場合にまずVBA.Collection
を使いますね。
そんなVBA.Collection
ですが、先頭要素からの線形リストになっているらしく、以降の要素にもアクセスする場合はひとつひとつ添え字の指定をするのは非効率なのでFor Each
で順次アクセスするのが一般的です。
ただしVBA.Collection
には、C#のyield
のように要素を1つ求めた時点で取得処理に処理を戻すような機構がありません。
そのため、要素を一括で作成してからFor Each
を開始するような書き方をよくします。
Dim TestColl As New VBA.Collection
Dim i As Long
For i = 1 To 5
Call TestColl.Add("Value" & Format(i, "00"))
Next i
Dim vItem As Variant
For Each vItem In TestColl
Debug.Print vItem
Next vItem
この場合、例えば要素が100個のものに対して使うのが30個までだった場合などに、残りの70個の作成処理が無駄になってしまいます。
Collection の末尾の判定
そんなVBA.Collection
ですが、For Each
の中でループを継続するかどうかの判定は毎回行っているため、For Each
の途中で要素を追加・削除することができます。
Dim TestColl As New VBA.Collection
Call TestColl.Add("A")
Call TestColl.Add("B")
Dim vItem As Variant
For Each vItem In TestColl
Debug.Print vItem
Select Case vItem
Case "A"
Call TestColl.Add("C")
Call TestColl.Add("D")
Case "C"
Call TestColl.Add("E")
End Select
Next vItem
実行結果
A
B
C
D
E
ただし、末尾の要素までたどりついてしまうとそこから末尾に足してもループの継続は行われなくなります。
Dim TestColl As New VBA.Collection
Call TestColl.Add("A")
Call TestColl.Add("B")
Dim vItem As Variant
For Each vItem In TestColl
Debug.Print vItem
Select Case vItem
Case "A"
Call TestColl.Add("C")
Case "C"
Call TestColl.Add("D")
End Select
Next vItem
実行結果
A
B
C
##ジョブキューとしての Collection
ループの中で要素を追加していく例としてジョブキューを考えてみます。
Private Enum JobType
Job_A
Job_B
Job_C
End Enum
Sub Main()
Dim JobQueue As New Collection
Call JobQueue.Add(Job_B)
Call JobQueue.Add(Job_C)
Dim CurJob As Variant 'As JobType
For Each CurJob In JobQueue
Select Case CurJob
Case Job_A
Call JobA(JobQueue)
Case Job_B
Call JobB(JobQueue)
Case Job_C
Call JobC(JobQueue)
End Select
Call JobQueue.Remove(1)
Next CurJob
End Sub
Private Sub JobA(ByVal JobQueue As Collection)
Debug.Print "Job.A"
Call JobQueue.Add(Job_C)
End Sub
Private Sub JobB(ByVal JobQueue As Collection)
Debug.Print "Job.B"
Call JobQueue.Add(Job_C)
Call JobQueue.Add(Job_A)
End Sub
Private Sub JobC(ByVal JobQueue As Collection)
Debug.Print "Job.C"
End Sub
実行結果
Job.B
Job.C
Job.C
Job.A
Job.A の後には Job.C が続いてほしいのですが、Job.A に差し掛かった時点で For Each
が末尾に到達したために、続きの Job.C が足されたことが認識されませんね。
こういう場合は、要素を追加したときにダミーでさらにもう1要素足しておくような使い方がいいのかもしれません。
Private Enum JobType
NOP
Job_A
Job_B
Job_C
End Enum
Sub Main()
Dim JobQueue As New Collection
Call JobQueue.Add(Job_B)
Call JobQueue.Add(Job_C)
Dim CurJob As Variant 'As JobType
For Each CurJob In JobQueue
Select Case CurJob
Case NOP
'
Case Job_A
Call JobA(JobQueue)
Case Job_B
Call JobB(JobQueue)
Case Job_C
Call JobC(JobQueue)
End Select
Call JobQueue.Remove(1)
Next CurJob
End Sub
Private Sub JobA(ByVal JobQueue As Collection)
Debug.Print "Job.A"
Call JobQueue.Add(Job_C)
Call JobQueue.Add(NOP)
End Sub
Private Sub JobB(ByVal JobQueue As Collection)
Debug.Print "Job.B"
Call JobQueue.Add(Job_C)
Call JobQueue.Add(Job_A)
Call JobQueue.Add(NOP)
End Sub
Private Sub JobC(ByVal JobQueue As Collection)
Debug.Print "Job.C"
End Sub
実行結果
Job.B
Job.C
Job.C
Job.A
Job.C
ジョブキューの場合は途中にダミー要素があってもほとんど影響ありませんが、作られたCollection
の要素一式に意味があるものの場合は、その内容に応じて末尾の要素での追加を避けるしくみを考える必要がありますね。