はじめに
VBAでは For Each ステートメントにより、配列や列挙可能なオブジェクトの要素に対して反復を行うことができます。
この時、ループ毎に各要素が格納される変数(下記コードのelement
)は、変数的なものなら何でも指定でき、ループの都度評価されている、というのがこの記事の内容になります。
For Each element In group
statements
Next
> [各...Next ステートメント (VBA) | Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/for-eachnext-statement "各...Next ステートメント (VBA) | Microsoft Docs")
> [For Each...Next ステートメントを使用する (VBA) | Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/language/concepts/getting-started/using-for-eachnext-statements "For Each...Next ステートメントを使用する (VBA) | Microsoft Docs")
## 「変数的なもの」の定義
この記事中では「変数的なもの」は以下のように定義します。
```text
ByRef が指定されたプロシージャの引数に指定したとき、プロシージャ内で変更された内容が反映される任意の式
具体的には、任意の変数、配列の要素、ユーザー定義型のフィールドなどが該当します。
プロパティとして振る舞うもの(Property
プロシージャで定義されたもの、標準モジュール以外のモジュールのPublic
な変数を.
経由でアクセスしたもの)は含みません。
Type SampleUDT
Value As Variant
End Type
Public varPublic As Variant
Sub VariableExpression()
Dim arr() As Variant
arr = VBA.[_HiddenModule].Array()
'任意の変数。
Dim varLocal As Variant
For Each varLocal In arr
Next varLocal
For Each varPublic In arr
Next varPublic
'配列の要素。
Dim varArr(0 To 0) As Variant
For Each varArr(0) In arr
Next varArr(0)
'ユーザー定義型のフィールド。
Dim varUdt As SampleUDT
For Each varUdt.Value In arr
Next varUdt.Value
'以下はNG
'For Each propValue In arr
'Next
End Sub
Public Property Get propValue() As Variant
End Property
Public Property Let propValue(inValue As Variant)
End Property
実際に確認
「配列の要素」を指定してFor Each
ループを行うサンプルコードです。
Sub SampleForEach()
'VBA.Collection を新規インスタンスして、適当に中身を設定する。
Dim col As VBA.Collection
Set col = New VBA.Collection
'文字列・整数・小数・日付・オブジェクト・Null を設定。
col.Add "ABC"
col.Add 123
col.Add 3.14
col.Add VBA.DateTime.DateSerial(2021, 3, 27)
col.Add Err
col.Add Null
'Variant 型の配列を宣言し、VBA.Collection の要素数 + 1 だけ領域を確保する。
Dim arr() As Variant
ReDim arr(1 To col.Count() + 1)
'配列の末尾に「Hoge」を設定する。
arr(UBound(arr)) = "Hoge"
Dim i As Long
i = LBound(arr)
For Each arr(i) In col
i = i + 1
Next arr(i)
Stop
End Sub
このコードにはFor Each arr(i) In col
という行があり、element
として配列の一要素が設定されていますが、For Each
内部でi
がインクリメントされているため、ループ毎に配列の違う箇所を示します。
実際にStop
の時点でのローカルウィンドウの中身を確認すると、以下の画像のようになっています。
arr(1 To 6)
までは、col
の中身がそのまま格納されており、col
の列挙完了時にはi
が7
になっているため、初期化されてEmpty
となっています。
ちなみに、配列変数の型宣言から()
を抜き、Dim arr As Variant
とすると、arr(i)
が変数的なものを示す保証が無くなるため、コンパイルエラーになります。
これが何の役に立つのか?
変わった挙動のため、基本的には役立ててはいけないものだとは思います。
無理やり使い道を考えると、「値とオブジェクトが混ざったコレクションをVariant 配列に格納する」という用途では、一応使えなくは無いです。
VBAでは変数的なものへの代入時に、代入するものがオブジェクトの場合はSet
、それ以外はLet
を指定する必要があります(Let
は省略可)。
そのため、代入する前にどちらか判定した上で代入方法を切り替える必要があります。
しかし、配列の要素をFor Eachの変数として指定すれば、VBA内部で代入を行ってくれるため、判定処理を書く必要がなくなり、(自分が試した範囲では)処理速度も向上しました。
Sub SpeedTest()
Const LoopCount = 1 * 10 ^ 7
Dim col As VBA.Collection
Set col = New VBA.Collection
Dim i As Long
For i = 1 To LoopCount
col.Add i
Next i
Dim tim As Single
Debug.Print "普通に配列へ代入",
tim = VBA.DateTime.Timer
Dim arr1() As Variant
ReDim arr1(1 To col.Count)
i = LBound(arr1)
Dim v As Variant
For Each v In col
'Object かそれ以外かで、代入方法を切り替える。
'この方法では問題になることがあるが、あまりない話なので省略。
'https://qiita.com/nukie_53/items/bde16afd9a6ca789949d#%E5%A4%89%E6%95%B0%E3%81%B8%E4%BB%A3%E5%85%A5
If VBA.Information.IsObject(v) Then
Set arr1(i) = v
Else
Let arr1(i) = v
End If
i = i + 1
Next v
Debug.Print VBA.Strings.Format$(VBA.DateTime.Timer - tim, "0.000")
Debug.Print "For Eachで配列に代入",
tim = VBA.DateTime.Timer
Dim arr2() As Variant
ReDim arr2(1 To col.Count)
i = LBound(arr1)
Dim limitIndex As Long
limitIndex = UBound(arr2)
For Each arr2(i) In col
'For Each が正常終了すると、最後の要素が Empty になってしまうことへの対策。
If i >= limitIndex Then Exit For
i = i + 1
Next arr2(i)
Debug.Print VBA.Strings.Format$(VBA.DateTime.Timer - tim, "0.000")
End Sub
その他
#VBAクイズ
— いみひと (@nukie_53) October 14, 2020
リプに下げている画像のコードを実行したとき、
Stop ステートメントの位置で、配列変数 b の中身はどうなっているでしょうか?