和風スパゲティのレシピ

日本語でコーディングするExcelVBA

For Each文の中身を途中でいじるとどうなるか

今日はFor Eachステートメントで遊んでみようと思います。

Forステートメントのカウンタを途中で変更する

本題のFor Eachに行く前に、簡単なFor文でも遊んでみます。
まずは途中でカウンタを足してみましょう。

Sub Forステートメントのカウンタを途中で変更する()
    
    Dim i As Long
    For i = 1 To 5
        
        Debug.Print i
        If i = 3 Then i = 7
        
    Next
    
    Debug.Print i
    
End Sub

結果:1, 2, 3, 8

これだけでも面白い結果ですね。

カウンタを増やすと、ちょうど5でなくても超えればFor文は終了し、
しかも終了時の最後の+1は実行されるため最終結果が8となっています。

Forステートメントの終了条件を途中で変更する

続いて、カウンタではなく終了条件の方を途中で変えてみましょう。

Sub Forステートメントの終了条件を途中で変更する()
    
    Dim 終了条件 As Long
    終了条件 = 5
    Dim i As Long
    For i = 1 To 終了条件
        
        Debug.Print i
        終了条件 = 10
        
    Next
    
    Debug.Print i
    
End Sub

結果:1, 2, 3, 4, 5, 6

終了条件の方は途中で変えても反映されませんでした。

より正確には、終了条件という変数を見ているのではなく、
For文が始まった段階で値を取得して動いているため、
そのあとで変数が変わっても関係がないということですね。

これは予想しやすかったかもしれません。

For Eachステートメント中のシート変数を途中で変更する

さて本題(?)のFor Eachステートメントです。

シート名「1」「2」「3」「4」からなるブックで以下のコードを実行してみます。

Sub ForEachステートメントの変数を途中で変更する()
    
    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Worksheets
    
        Debug.Print ws.Name
    
        If ws.Name = "3" Then
            Set ws = ThisWorkbook.Worksheets("2")
        End If
    
    Next
    
End Sub

結果:1, 2, 3, 4

途中でシートを「3」から「2」に変えてみましたが、
それでも次のシートには「4」が選ばれていますね。

For Eachステートメントはスタートした段階で処理順が決まっており、
途中で変数を変えようとも、Nextごとの処理オブジェクトは決まっているようです。

For Eachステートメントの取得先シートを消してみる

上記の通り「ForEachの順番は最初に決まっている」ようでした。

ではForEachの取得先を先に消したらどうなるか見てみましょう。

Sub ForEachステートメントの次の取得先を削除する()
    
    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Worksheets
    
        Debug.Print ws.Name
    
        If ws.Name = "3" Then
            Application.DisplayAlerts = False
            ThisWorkbook.Worksheets("4").Delete
            Application.DisplayAlerts = True
        End If
    
    Next
    
End Sub

結果:1, 2, 3 にてループ終了

これはやや意外な結果になりました。

第3シートの処理中に第4シートを削除したため、
今までの流れからいうと「オートメーションエラー」が起きそうなものですが、
エラーなく3週でループが完了しています。


試しに第5シートまで作って、途中で4シート目を削除しても、

結果:1, 2, 3, 5 にてループ終了

となり、特にエラーなく削除した以外のシートが処理されます。


順番は最初に決まっているものの、
途中でいなくなったオブジェクトはエラーではなく無視する仕様のようですね。

For Eachで回しているシートを途中で増やしてみる

続いて途中でシートを増やすパターンです。

Sub ForEachステートメントの対象コレクションを途中で増やす()
    
    Dim ws As Worksheet
    For Each ws In ThisWorkbook.Worksheets
    
        Debug.Print ws.Name
    
        If ws.Name = "2" Then
            ThisWorkbook.Worksheets.Add
        End If
    
    Next
    
End Sub

結果:1, 2, 3 にてループ終了

こちらは追加されたシートを取得しませんでした。


走査するコレクションに変更があっても反映はされないようです。

今までの検証をWorkbooksで試す

上記のコードをWorkbooksに変えて試してみたところ、

  • 途中でブックを閉じてもエラーにならず
  • 途中で開いたブックは処理対象にならず

ということで、Worksheetsと同じ結果になりました。

一応コードを下記しておきます。

Sub ForEach中の次のブックを閉じる()
    
    Dim wb As Workbook
    For Each wb In Workbooks
    
        Debug.Print wb.Name
    
        If wb.Name = "PERSONAL.XLSB" Then
            Workbooks("Book1").Close False
        End If
    
    Next
    
End Sub

Sub ForEachループ中にブックを開く()
    
    Dim wb As Workbook
    For Each wb In Workbooks
    
        Debug.Print wb.Name
    
        If wb.Name = "PERSONAL.XLSB" Then
            Workbooks.Open "C:\Users\wfsp\Desktop\Book1.xlsx"
        End If
    
    Next
    
End Sub

今までの検証を配列(Array)で試す

同じく配列で試してみると。。。

Sub ForEach中の配列をReDimする()
    
    Dim Arr
    Arr = Array(1, 2, 3)
    Dim x
    For Each x In Arr
    
        Debug.Print x
        
        If x = 1 Then
            ReDim Preserve Arr(3)
        End If
        
    Next
    
End Sub

「この配列は固定されているか、または一時的にロックされています」
エラーとなりました。

ForEach中の配列はReDim(Preserve)による変更を受け付けないようです。

面白いですね。

今までの検証をCollectionで試す。

続いてコレクションで試してみます。

Sub ForEach中のCollectionをAddする()
    
    Dim コレクション As New Collection
    コレクション.Add 1
    コレクション.Add 2
    コレクション.Add 3
    コレクション.Add 4

    Dim x
    For Each x In コレクション
    
        Debug.Print x
        
        If x = 1 Then
            コレクション.Add 5
        End If
        
    Next
    
End Sub

結果:1, 2, 3, 4, 5

こちらはなんとAddを反映し、5まで表示されました。


これはおそらくCollectionのメモリの格納方式に由来しており、
コレクションが数珠つなぎのようにデータを保持する仕組みであることが理由です。


コレクションは配列と違い格納されるデータの大きさがItem毎に異なるため、
第n要素がどこにあるかはメモリに記憶されておらず、
例えば3番目の要素にアクセスする際も1,2,3と順にアクセスして探します。


この仕様のため、コレクションは、

For i = 1 To コレクション.Count

このインデックスを利用したループ文で処理しようとすると、
ForEach文よりかなり処理が遅くなる
特徴があります。


この仕様と同じ理由で、ForEachの処理順も最初に決まるわけではないため、
途中のAddもループに反映されたと考えられます。


同じくRemoveを試してみると、

Sub ForEach中のCollectionをRemoveする()
    
    Dim コレクション As New Collection
    コレクション.Add 1
    コレクション.Add 2
    コレクション.Add 3
    コレクション.Add 4

    Dim x
    For Each x In コレクション
    
        Debug.Print x
        
        If x = 1 Then
            コレクション.Remove 3
        End If
        
    Next
    
End Sub

結果:1, 2, 4

やはりRemoveも反映してくれますね。

今までの検証をDictionaryで試す

続いてDictionaryで試してみましょう。

Sub ForEach中のDectionaryをAddする()
    
    Dim 辞書 As New Dictionary
    辞書.Add 1, 1
    辞書.Add 2, 2
    辞書.Add 3, 3
    辞書.Add 4, 4

    Dim x
    For Each x In 辞書
    
        Debug.Print x
        
        If x = 1 Then
            辞書.Add 5, 5
        End If
        
    Next
    
End Sub

結果:1, 2, 3, 4, 5

なんと反映されました。

この理屈はちょっとわからないというか、
そもそも

For Each x In 辞書

でKeyをループしたことになるというのがDictionary特有の仕様で、
For Eachの挙動もDictionary専用の動きをします。

この書き方ができるのがそもそもDiciotnaryだけなので、
そういうものなんだなと思うことにしましょう。


なお、

For Each x In 辞書.Keys

とした場合は第5要素は反映されません。

これはDictionaryの仕様という訳ではなく、
単にKeysメソッドが「配列を生成する」メソッドのためです。


For Eachが始まった段階でDictionaryとは関係のない、
新しく生成された配列になっています。

そのあとでDicitonaryに変更を加えても反映されないのは、
当然と言えば当然ですね。

今までの検証をShapeで試す

最後にシート内の図形オブジェクト(Shapes)で試してみます。

まずはAdd。

Sub ForEach中のShapeをAddする()

    Dim shp As Shape
    For Each shp In ActiveSheet.Shapes

        Debug.Print shp.Name
        
        If shp.Name = "Rectangle 1" Then
            ActiveSheet.Shapes.AddShape msoShapeSun, 0, 0, 10, 10
        End If

    Next
    
End Sub

結果:Rectangle 1, Rectangle 2, Rectangle 3

こちらは図形の追加を反映しませんでした。

オブジェクトのCollectionに対するForEachは、
WorksheetやWorkbookと同様スタート時に順番が決まる
ようです。


続いてDeleteを試します。

Sub ForEach中のShapeをDeleteする()

    Dim shp As Shape
    For Each shp In ActiveSheet.Shapes

        Debug.Print shp.Name
        
        If shp.Name = "Rectangle 1" Then
            ActiveSheet.Shapes("Rectangle 3").Delete
        End If

    Next
    
End Sub

こちらはなんと、「オブジェクトが必要です」エラーとなりました。

試しに「Is Nothing」の判定をするとFalseになり、
ObjPtrでメモリアドレスを調べるとちゃんと返してくれるため、
「該当のメモリにあるはずのオブジェクトがない」となったようです。


オブジェクトのForEachとしてはこちらの方が自然な挙動な気がしますので、
WorkbooksとWorksheetsだけ特別な仕様になっていると推測されます。

個人的にはWorksheet/bookでオートメーションエラーにならないことが意外でしたので。


以上でFor Eachステートメントの検証を終わります。

なかなか興味深い挙動が多く、検証していて面白かったですが、
「For Eachの実行中にループ変数や対象コレクションをいじってはいけない」
という身も蓋もない結論で締めたいと思います。


間違って編集してしまわないよう、注意してコーディングしていきましょう。