今日はFor Eachステートメントで遊んでみようと思います。
- Forステートメントのカウンタを途中で変更する
- Forステートメントの終了条件を途中で変更する
- For Eachステートメント中のシート変数を途中で変更する
- For Eachステートメントの取得先シートを消してみる
- For Eachで回しているシートを途中で増やしてみる
- 今までの検証をWorkbooksで試す
- 今までの検証を配列(Array)で試す
- 今までの検証をCollectionで試す。
- 今までの検証をDictionaryで試す
- 今までの検証をShapeで試す
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の実行中にループ変数や対象コレクションをいじってはいけない」
という身も蓋もない結論で締めたいと思います。
間違って編集してしまわないよう、注意してコーディングしていきましょう。