5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-27

はじめに

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の時点でのローカルウィンドウの中身を確認すると、以下の画像のようになっています。
image.png

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

ちなみに、配列変数の型宣言から()を抜き、Dim arr As Variantとすると、arr(i)が変数的なものを示す保証が無くなるため、コンパイルエラーになります。
image.png

これが何の役に立つのか?

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

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

VBAでは変数的なものへの代入時に、代入するものがオブジェクトの場合はSet、それ以外はLetを指定する必要があります(Letは省略可)。
そのため、代入する前にどちらか判定した上で代入方法を切り替える必要があります。

参考:VBA 個人的汎用処理 - Qiita

しかし、配列の要素を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

その他

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?