VBA謎仕様：For Each で指定する変数は、変数的なものなら何でも指定でき、ループの都度評価される

# はじめに

VBAでは For Each ステートメントにより、配列や列挙可能なオブジェクトの要素に対して反復を行うことができます。

この時、ループ毎に各要素が格納される変数(下記コードの`element`)は、変数的なものなら何でも指定でき、ループの都度評価されている、というのがこの記事の内容になります。

>```vb
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`な変数を`.`経由でアクセスしたもの)は含みません。

```vb:変数的なもの
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`ループを行うサンプルコードです。

```vb
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`の時点でのローカルウィンドウの中身を確認すると、以下の画像のようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142842/c07ee905-c0a1-de55-b761-e12fe8df7d99.png)

`arr(1 To 6)`までは、`col`の中身がそのまま格納されており、`col`の列挙完了時には`i`が`7`になっているため、初期化されて`Empty`となっています。

ちなみに、配列変数の型宣言から`()`を抜き、`Dim arr As Variant`とすると、`arr(i)`が変数的なものを示す保証が無くなるため、コンパイルエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142842/a4654eee-95d8-3326-4d1b-e304d1eec24b.png)


# これが何の役に立つのか？

変わった挙動のため、基本的には役立ててはいけないものだとは思います。

無理やり使い道を考えると、「値とオブジェクトが混ざったコレクションをVariant 配列に格納する」という用途では、一応使えなくは無いです。

VBAでは変数的なものへの代入時に、代入するものがオブジェクトの場合は`Set`、それ以外は`Let`を指定する必要があります(`Let`は省略可)。
そのため、代入する前にどちらか判定した上で代入方法を切り替える必要があります。
> 参考：[VBA 個人的汎用処理 - Qiita](https://qiita.com/nukie_53/items/bde16afd9a6ca789949d#%E5%A4%89%E6%95%B0%E3%81%B8%E4%BB%A3%E5%85%A5 "VBA 個人的汎用処理 - Qiita")

しかし、配列の要素をFor Eachの変数として指定すれば、VBA内部で代入を行ってくれるため、判定処理を書く必要がなくなり、(自分が試した範囲では)処理速度も向上しました。

```vb
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
```

# その他

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/VBA%E3%82%AF%E3%82%A4%E3%82%BA?src=hash&amp;ref_src=twsrc%5Etfw">#VBAクイズ</a><br>リプに下げている画像のコードを実行したとき、<br>Stop ステートメントの位置で、配列変数 b の中身はどうなっているでしょうか？</p>&mdash; いみひと (@nukie_53) <a href="https://twitter.com/nukie_53/status/1316326797412245506?ref_src=twsrc%5Etfw">October 14, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>