🚀The world's best VBA AI has evolved. ExcelMaster is now an autonomous Agent.Read more →
Back to Blog

Excel VBA の For Each — インデックスに触れずコレクションをループする(そして隠れた罠)

|

Excel VBA の For Each — インデックスに触れずコレクションをループする(そして隠れた罠)

TL;DRFor Each item In collection は、コレクションのメンバー全員を歩き、 1 つずつ手渡してくれます — カウンターも .Count も、1 つずれるバグもなし。 コレクションRangeWorksheets、配列)があって、そのすべての要素に 触りたいときに使います。For i = 1 To N に戻るのは、インデックスそのものが 必要なときだけ:逆順にたどる、Step で飛ばす、進みながら要素を削除する、 といった場合です。裏切られないための 3 つのルール:ループ変数は**オブジェクト か Variant**でなければならず、配列のループは読み取り専用で、走査中の コレクションから要素を取り除いてはいけない

Dim cell As Range
Dim total As Double

For Each cell In Sheets("Data").Range("B2:B11")
    total = total + cell.Value      ' VBA が各セルを順に渡してくれる
Next cell

Debug.Print total                   ' i も .Count も、間違えようのある境界もない

実際のマクロに登場するループの大半は、突き詰めれば同じことを言っています — 「この範囲のすべてのセルについて…」「このブックのすべてのシートについて…」 「このリストのすべての名前について…」。For Each は、その一文をほぼそのまま コードに書かせてくれる VBA の仕組みです。カウンター方式の For i = 1 To N でも 同じ考えは表現できますが、それはインデックスの管理をあなたに押しつけます — 開始値、終了値、.Count、そして端の 1 つ(fencepost)。For Each はその帳簿 付けを丸ごと肩代わりしてくれます。ただし代償として、インデックスを完全に 取り上げてしまい、それが本当に必要な操作もいくつかあるのです。

この記事で学べること

  • 考え方の軸 — For Each がたどるのは所属であって位置ではないこと
  • ループ変数が RangeWorksheetVariant でなければならず、決して Long ではない理由
  • 読み取り専用の罠:配列のループ変数に代入しても何も起きないこと
  • For Each の中で削除すると走査が無言で壊れる理由 — そして代わりに何をすべきか
  • For Each を選ぶべきときと For…Next に戻すべきときの、はっきりしたルール

考え方の軸:For Each がたどるのは所属であって位置ではない

カウンターループは位置で考えます — 「1 番の区画へ、次に 2 番、次に 3 番」。 For Each所属で考えます — 「このグループに属するものを、グループが保持 している順序のまま、1 つずつよこせ」。要素がどこにあるかを言うことは一切なく、 ただ受け取るだけです。

この発想の転換こそ、For Each が短く書け、大きな範囲ではわずかに速くなる理由 です。Cells(i, 1) では、VBA は反復のたびにアドレスを計算し、セルを解決しなけ ればなりません。For Each cell In rng なら、コレクション組み込みの列挙子 (enumerator)が、毎回すでに解決済みの Range オブジェクトを渡してくれます — 計算は不要です。インデックスの番号を手放す代わりに、より簡潔で速い走査を手に 入れるわけです。これ以降に続く話はすべて、そのインデックスを持たないことの 帰結です。

ループ変数の型を決めるルール:オブジェクトか Variant、決して Long ではない

これは多くの人が最初につまずく点です。カウンターループとは正反対だからです。 For i = 1 To 10 では iLong です。For Each では、ループ変数はコレク ションから要素まるごとを受け取るので、その要素を収められる型でなければなり ません。

' セルの Range -> 要素は Range
Dim cell As Range
For Each cell In Range("A1:A10")
    cell.Value = cell.Value * 2
Next cell

' Worksheets -> 要素は Worksheet
Dim ws As Worksheet
For Each ws In ThisWorkbook.Worksheets
    Debug.Print ws.Name
Next ws

範囲の走査に Dim cell As Long と宣言すると、VBA は Range を数値に押し込もう とした瞬間に**「オブジェクトが必要です (エラー 424)」**を投げます。ルールはこう です:ループ変数の型は、コレクションが保持しているものの種類に一致していなけ ればならない — セルなら Range、シートなら Worksheet、開いているブックなら Workbook

例外が 1 つあり、それこそ皆が忘れるものです — 配列です。VBA の配列は何でも 保持できるので、コンパイラは要素の型を事前に知ることができません。For Each で 配列をループするには Variant が必要です。

Dim names As Variant
names = Array("Alice", "Bob", "Carol")

Dim n As Variant          ' 配列の走査には Variant が必須
For Each n In names
    Debug.Print n
Next n

そこで Dim n As String とすると「型が一致しません」というエラーになります。 配列で迷ったら、ループ変数は Variant です。

誰もが一度ははまるルール:配列のループは読み取り専用

動いて当然に見えるのに動かない、というバグがこれです。

Dim nums As Variant
nums = Array(1, 2, 3)

Dim x As Variant
For Each x In nums
    x = x * 10            ' 各要素を掛けているように見えるが…
Next x

Debug.Print nums(0), nums(1), nums(2)   ' -> 1  2  3   (変わらない)

あなたが掛け算したのは x であって、配列ではありません。For Each配列を 走査するとき、ループ変数は要素のコピーであり、要素そのものへの生きた handle ではありません。x に代入するとコピーが変わるだけで、次の反復でそれは捨てられ ます。配列は一度も動きません。

これは For Each について知っておくべき最も重要な 1 点です。なぜなら無言で 失敗するからです — エラーは出ず、ただ古いままのデータが残ります。対処は、 インデックスで本物の要素を指し示すカウンターループに戻すことです。

Dim i As Long
For i = LBound(nums) To UBound(nums)
    nums(i) = nums(i) * 10     ' これなら本当に配列が変わる
Next i

非対称性に注意してください:For EachRange をループする場合は書き込み できますcell.Value = …Range オブジェクトを通り抜けて本物のセルに 届きます)。cell はコピーではなくオブジェクト参照だからです。For Each の下で 読み取り専用になるのは、まさに配列 — そして値型の要素 — なのです。ループで 配列の中身を書き換える必要があるなら、For i = LBound To UBound を使いましょう。

痛い目で学ぶルール:走査中のコレクションから削除しない

For Each はコレクションの列挙子に頼っており、その列挙子は、コレクションが自分の 足元で形を変えないことを前提にしています。走査の途中で要素を取り除くと、列挙子は 現在地を見失います — 要素を飛ばすか、エラーを起こすかのどちらかで、しかもどちらに なるかは教えてくれません。

' 壊れた例 — 削除すると列挙子の下でコレクションがずれる
Dim sh As Worksheet
For Each sh In ThisWorkbook.Worksheets
    If sh.Name Like "Temp*" Then sh.Delete   ' シートを飛ばす、予測不能
Next sh

同じ罠は、For Each の中でワークシートの行を削除したり、Collection から要素を 取り除いたり、Dictionary からキーを落としたりするときにも噛みついてきます。 ルールは絶対です:ループがコレクションの所属を変えるなら、For Each を使わ ない。 代わりにカウンターで逆順に走査すれば、要素を取り除いても、まだ訪れて いないインデックスが乱されることはありません。

' 正しい例 — 逆順に数えれば削除しても残りがずれない
Dim i As Long
For i = ThisWorkbook.Worksheets.Count To 1 Step -1
    With ThisWorkbook.Worksheets(i)
        If .Name Like "Temp*" Then .Delete
    End With
Next i

これは、スプレッドシートの行を安全に削除するときに使うのと同じ「逆順反復」の 作法です — Step -1 のパターンは VBA For Loop を参照して ください。指針はこうです:For Each は要素をその場で読み・書き換えるための もの。降順のカウンターループは、要素を追加・削除するためのもの。

For Each と For…Next の使い分け

どちらのループが「上」ということはありません — 答える問いが違うのです。タスクが インデックスに何を求めるかで選びましょう。

  • For Each を使う — コレクションがあって、そのメンバー全員を自然な順序で 触りたいとき:範囲を合計する、すべてのセルを書式設定する、各シート名を出力する、 各要素を一度ずつ処理する。読みやすく、間違える境界もなく、大きな範囲では速い です。
  • For i = 1 To N を使う — インデックスを値として必要とするとき:i 行目に 書き込む、逆順にたどる(Step -1)、飛ばす(Step 2)、要素 i を要素 i+1 と比べる、あるいは進みながら削除する。配列の要素を書き換えられる 唯一の方法でもあります。

見分けのコツ:.Count を呼んで、collection(i) に届くためだけに自前の i を こしらえている自分に気づいたら、たぶん欲しかったのは For Each です。現在の 要素のの要素に手を伸ばしているとき、あるいは要素を取り除いていると きは、カウンターが必要です。

ExcelMaster の活用

ループを選び、ループ変数を正しく型付けし、配列が読み取り専用であることと削除は 逆順でなければならないことを覚えておく — これはまさに、「やりたいことは分かって いる」と「マクロがそれを正しくやる」との間に立ちはだかる、機械的な判断の類です。

ExcelMaster なら、 代わりに目的を述べるだけで済みます。「Orders シートで、ステータスが Overdue の行を すべて強調して」あるいは「名前が Temp で始まるシートをすべて削除して」と言えば、 そのロジックを書いて実行してくれます — 素直な走査でよい場面では For Each を、 要素が取り除かれる場面では降順カウンターを選ぶので、削除順序のバグは決して起き ません。定期実行するマクロを自分で手書きするときは、これからもループは自分で 選ぶことになるでしょう。しかし「今日のデータにとにかくこれをやりたい」という 日々の作業なら、結果を説明する方が、列挙子のルールを一つひとつ手で正しく守るより 勝るのです。

よくある質問

VBA の For Each と For…Next の違いは何ですか?

For Each はコレクションを所属でたどります — コレクションの自然な順序で各要素を 渡してくれ、インデックスの管理は一切ありません。For i = 1 To Nカウンター 方式で、インデックスを自分で制御するので、Step -1 で逆順にしたり、Step 2 で 飛ばしたり、要素 i+1 に届いたりできます。RangeWorksheets・配列をまっすぐ 走査するなら For Each、インデックスが必要なとき・要素を削除したいとき・配列 要素を書き換えなければならないときは For…Next を使いましょう。

For Each ループで「オブジェクトが必要です (エラー 424)」が出るのはなぜですか?

ほぼ確実に、ループ変数を間違った型で宣言しているためです — コレクションがオブ ジェクトを保持しているのに、たいてい LongString になっています。 For Each cell In Range(...) には Dim cell As Range が、For Each ws In Worksheets には Dim ws As Worksheet が必要です。VBA の配列をループしているなら、ループ 変数は Variant でなければなりません。

For Each ループの中で配列の値を変更できますか?

できません。For Each が配列を走査するとき、ループ変数は各要素のコピーなので、 それに代入してもコピーが変わるだけで、配列は変わりません — しかもエラーは出ません。 配列を変更するには、カウンターループを使います:For i = LBound(arr) To UBound(arr) として arr(i) に代入します。For EachRange をループする場合は書き込み できます。ループ変数がセルへの生きたオブジェクト参照だからです。

For Each で範囲内のすべてのセルをループするには?

ループ変数を Range として宣言し、範囲を直接ループします: For Each cell In Range("A1:B10")。VBA はセルを行ごとにたどります。大きなブロック なら、これは入れ子の Cells(i, j) ループより速いですが、最速の方法は範囲を一度に 配列へ読み込み(arr = Range("A1:B10").Value)、その配列をループすることです — VBA Array を参照してください。

For Each の中で行やシートを削除すると一部が飛ばされるのはなぜですか?

For Each は、走査中にコレクションの所属が変わらないことを前提とする列挙子に 頼っているからです。要素を削除するとそれ以降がすべてずれ、列挙子は現在地を見失い — 無言で要素を飛ばします。For Each の中では決して削除しないでください。 降順に数えるカウンターループ(For i = .Count To 1 Step -1)を使えば、削除して もまだ到達していない要素が乱されません。

検証環境

検証環境: Excel 365 (Windows 11), VBA 7.1 — 最終確認 2026-08-01。

関連ガイド: VBA For Loop · VBA Collection · VBA With · VBA Array · VBA Dictionary