ExcelVBA入門 #12:繰り返し処理を極める
シリーズもくじはこちら
ExcelVBA入門 もくじ - アナログCPU:5108843109
以前、「For」を使った繰り返し構文について書きましたが、
ExcelVBA入門 #4:エンドレスエイトに学ぶ繰り返し処理 - アナログCPU:5108843109
実は他にも同じ処理を繰り返すための書き方があります。
今回はそれらを紹介しつつ、繰り返し処理全般の扱い方をまとめていきます。
繰り返す回数がわかっているときのための「For」
こちらは以前紹介したものなので簡単に書きますが、
「10回繰り返したい!」というふうに、回数が分かっているときに使いやすいものです。
基本の書き方
' iは好きな変数、0と9は開始値と終了値 For i = 0 To 9 '(繰り返したい処理) Next
サンプル
' 10回繰り返す(イミディエイトウインドウに0から9まで表示される) Public Sub ForLoopTest() Dim i As Long For i = 0 To 9 Debug.Print i Next End Sub
この書き方の場合、Forの中身を繰り返している間に変数iは1ずつ増加していきますが、
「Step」で増減量を指定することもできます。
' iを1ずつカウントダウンしながら10回繰り返す '(イミディエイトウインドウに9から0まで表示される) Public Sub ForLoopTest() Dim i As Long For i = 9 To 0 Step -1 Debug.Print i Next End Sub
' iを2ずつカウントアップしながら5回繰り返す '(イミディエイトウインドウに0,2,4,6,8が表示される) Public Sub ForLoopTest() Dim i As Long For i = 9 To 0 Step 2 Debug.Print i Next End Sub
あるオブジェクトの中身を全部処理したいときのための「For Each」
オブジェクトの中身のすべての要素に対して処理するための書き方がこれ。
「配列」や「複数のセルの範囲」といったよく使うものはもちろん、
「選択したシートに対して…」「シート状の図形オブジェクトに対して…」など、
アイデア次第で様々な使い道があります。
基本の書き方
' parentが処理したいオブジェクト変数、childはその中身を一時的に格納する変数 For Each child In parent '(繰り返したい処理) Next
サンプル
まずは指定した範囲のセルすべてに対しての処理を行うときの書き方。
' A1~C3のセル(3×3の9個)すべてに対して処理を行う '(イミディエイトウインドウに各セルの中身が表示される:順序はA1→A2→A3→B1→…→C3) Public Sub ForEachLoopTest() Dim rngDat As Range Dim rngLoop As Range Set rngDat = ThisWorkbook.Sheets("Sheet1").Range("A1:C3") For Each rngLoop In rngDat Debug.Print rngLoop.Value Next End Sub
ここで、rngDatは「A1~C3のセルのかたまり」、
rngLoopはループ内で「A1セル」→「A2セル」→…と移り変わっています。
つまりrngLoopは、このループ内では「Range("A1")」などと同じように扱えます。
(だから表示するときに「rngLoop.Value」という書き方をしているのです)
そして次は配列に対して処理を行うときの書き方。
こちらも書き方自体は同じです。
Public Sub ForEachLoopTest() ' 配列に対して処理を行う '(イミディエイトウインドウに配列の中身が表示される:順序は(0)→(1)→(2)→…) Dim lDat(0 To 2) As Long Dim vLoop As Variant lDat(0) = 1 lDat(1) = 2 lDat(2) = 3 For Each vLoop In lDat Debug.Print vLoop Next End Sub
ここで、オブジェクトの中身が入る変数の「vLoop」には
もちろん元の配列「lDat」の中身である数値の1や2が入るのですが、
Variant型でないとエラーになってしまいます。
「配列に対してFor Eachするときの変数はVariant型」、覚えておきましょう。
尚、多次元配列でも書き方は全く同じです。
Public Sub ForEachLoopTest2() ' 配列に対して処理を行う '(イミディエイトウインドウに配列の中身が表示される: ' 順序は(0, 0)→(1, 0)→(2, 0)→…→(0, 1)→(1, 1)→(2, 1)→…) Dim lDat(0 To 2, 0 To 1) As Long Dim vLoop As Variant lDat(0, 0) = 1 lDat(1, 0) = 2 lDat(2, 0) = 3 lDat(0, 1) = 4 lDat(1, 1) = 5 lDat(2, 1) = 6 For Each vLoop In lDat Debug.Print vLoop Next End Sub
処理順には注意しましょう。
条件を満たしている間処理し続けたいときのための「Do」
繰り返し回数は分からないしオブジェクトの中身を全部処理したいというわけでもない、
けれど繰り返したい条件は分かっている、というときにはこちら。
「ある条件を満たしている間繰り返す」「ある条件を満たしていない間繰り返す」という
二つの書き方ができます。
基本の書き方
ある条件を満たしている間繰り返す方法(While)
Do While [条件] '(繰り返したい処理) Loop
ある条件を満たしていない間繰り返す方法(Until)
Do Until [条件] '(繰り返したい処理) Loop
この方法だと、While条件を最初から満たしていない(Until条件を最初から満たしている)場合には
Do~Loop内の処理は一度も実行されませんが、
一度だけは必ず実行するという方法もあります。
Do '(繰り返したい処理、初回は必ず実行される) Loop While [条件]
Do '(繰り返したい処理、初回は必ず実行される) Loop Until [条件]
このように、Loopのあとに条件を付ければOKです。
サンプル
まずはWhileについて。
よくある使い方の例としては、
「セルを上から順に見ていって、空のセルに当たったらやめる」です。
' A1セル(.Cells(1, 1))から1つずつ下方向にチェックし、 ' 「中身が空でない」という条件を満たす場合のみ繰り返す ' (イミディエイトウインドウにセルの中身が表示される) Public Sub DoWhileLoopTest() Dim wsSheet As Worksheet Dim lRow As Long Set wsSheet = ThisWorkbook.Sheets("Sheet1") lRow = 1 Do While wsSheet.Cells(lRow, 1).Value <> "" Debug.Print wsSheet.Cells(lRow, 1).Value lRow = lRow + 1 Loop End Sub
ループの外側であらかじめ初期値を設定し(lRow = 1)
ループの内側でそれを1ずつ増やしながら繰り返します。
「セルの中身が空でない」という条件を満たしている間のみ、
Do~Loop内の処理を行います。
条件を満たしてさえいればループを繰り返し続けるので、
無限ループが起こらないよう注意が必要です。
次にUntilについて。
これもWhileと同じく、
「セルを上から順に見ていって、空のセルに当たったらやめる」という処理を書いてみます。
' A1セル(.Cells(1, 1))から1つずつ下方向にチェックし、 ' 「中身が空である」という条件を満たさない場合のみ繰り返す ' (イミディエイトウインドウにセルの中身が表示される) Public Sub DoUntilLoopTest() Dim wsSheet As Worksheet Dim lRow As Long Set wsSheet = ThisWorkbook.Sheets("Sheet1") lRow = 1 Do Until wsSheet.Cells(lRow, 1).Value = "" Debug.Print wsSheet.Cells(lRow, 1).Value lRow = lRow + 1 Loop End Sub
ご覧のとおり、Whileとほぼ同じ形ですね。
条件の書き方が逆になるだけですので、
WhileとUntilを使い分ける必要が出る局面は決して多くないと思います。
また、先述のとおり、Loopの後に条件を書くことで
条件を満たすかどうかに関わらず、一度はかならず実行させることができます。
まれにそういうロジックが必要になることもあるので、
そういうときはこの書き方を思い出してみてください。
Public Sub DoWhileTest() Do While 1 = 0 '絶対に満たされない条件 MsgBox "これは絶対に一度も実行されない" Loop End Sub
Public Sub LoopWhileTest() Do MsgBox "これは絶対に一度だけ実行される" Loop While 1 = 0 '絶対に満たされない条件 End Sub
ループを途中で抜ける方法
所定のループ回数を終えていなくとも、オブジェクトを全部見ていなくとも、
Whileの条件を満たしていても、途中で繰り返しを終えたいケースもあります。
(他の言語だと「break」などに相当するものですね)
そういうときは「Exit」を使うとループを抜けることができます。
' For(Each) の場合 Exit For ' Doの場合 Exit Do
例えばエラー処理などですね。
' 指定範囲のセルの値をイミディエイトウインドウに表示する ' ただし、5より大きい数値があった場合はメッセージを表示して以降の処理を打ち切る Public Sub ExitTest() Dim rngDat As Range Dim rngLoop As Range Set rngDat = ThisWorkbook.Sheets("Sheet1").Range("A1:C3") For Each rngLoop In rngDat If rngLoop.Value > 5 Then MsgBox "セルには5以下の数値を入力してください。" Exit For End If Debug.Print rngLoop.Value Next End Sub
Doループの条件を常に満たすようにしておき、
ループ内で制御する、といった使い道もあります。
Doループの条件を常に満たすためには、
Whileなら「True」、Untilなら「False」にするだけでOK。
(無限ループには注意)
' 無限ループ内で「現在時刻表示・1秒停止」を繰り返す ' ただし、00秒になったら終了 Public Sub InfiniteLoopTest() Do While True ' 今の時刻を表示 Debug.Print Format(Time, "hh:mm:ss") ' 00秒になったらループを抜ける If Format(Time, "ss") = "00" Then Exit Do End If ' 1秒待つ ' (Application.Waitは指定時刻まで停止する関数) Application.Wait Now() + TimeValue("00:00:01") Loop End Sub
ループ内の処理をスキップする方法
今見ているループ内の処理をすべてスキップして、次のループの先頭へ進んでほしい、というケースもあります。
例えば上から1行ずつ見ているとき、「1列目の値が○○なら無視して次の行へ行ってほしい」という感じですね。
他の言語だと「continue」などにあたるもの…なのですが。
実は、VBAにはそのための命令が用意されていません。
なので、少し考えてやる必要があります。
ごく簡単な処理しかないループであれば、If文を入れてやればOKです。
' カウンタ用変数が2の倍数のときだけイミディエイトウィンドウに表示する Public Sub ContinueTest() Dim i As Long For i = 0 To 9 If i Mod 2 = 0 Then Debug.Print i End If Next End Sub
ですが、コードが長くなればなるほど、If文だけで対応するのは大変になります。
解説は置いておいて、
結論から言うと、次のように「GoTo」と「ラベル」を使って対応するとよいでしょう。
' カウンタ用変数をイミディエイトウインドウに表示する ' ただし、2の倍数でないときは実行せずにループの最後へ Public Sub ContinueTest() Dim i As Long For i = 0 To 9 If i Mod 2 <> 0 Then GoTo CONTINUE '「CONTINUE:」の部分にジャンプする End If Debug.Print i CONTINUE: 'ラベル(ただの目印なので、最後にコロンが付いていれば好きな名前でOK) Next End Sub
If文だけで対応すると大変なケースを考えてみましょう。
例えば以下のような仕様だといかがでしょうか。
- シートの住所録(1行に1人)に対して以下の処理を行いたい
まずはIf文だけでなんとかしてみましょう。
row = 0 Do While True If (row行の特定セルが空) Then Exit Do '処理終了 End If If [日本でない] Then [セルに色をつける] Else If [北海道ではない] Then If [東京である] Then [「都」をつける] ElseIf [大阪か京都である] Then [「府」をつける] Else [「県」をつける] End If If [沖縄県ではない] Then '( ... ) End If End If End If row = row + 1 Loop
これを、GoTo+ラベル戦法で書き直すとこんな感じ。
row = 0 Do While True If [row行の特定セルが空] Then Exit Do '処理終了 End If If [日本でない] Then [セルに色をつける] GoTo CONTINUE End If If [北海道である] Then GoTo CONTINUE End If If [東京である] Then [「都」をつける] ElseIf [大阪か京都である] Then [「府」をつける] Else [「県」をつける] End If If [沖縄県である] Then GoTo CONTINUE End If '( ... ) CONTINUE: row = row + 1 Loop
いかがでしょうか。
そんなに長くないプログラムですが、
If文だけだと、階層(ネスト)がどんどん深くなっていってしまいますね。
それに、コードだけを読んで「住所が日本でない場合はどうするのか?」を理解しようと思ったら、
そのIf文の終端の先も読む必要が出てしまいます。
一方、ラベルを使用すれば「GoTo CONTINUE」と明示することになりますので、
「CONTINUEラベルはループの最後」というお約束さえ分かれば、
「ああ、もうこのループの中身で処理されることはないんだな」とわかり、非常に見やすくなります。
ネストも浅いままで済み、見やすいコードにまとまる可能性も高いです。
仕様やコードの長さによっても読みやすさは変わってくるので、
必ずラベルが良い!というわけではありませんが、一考の価値はあるのではないかと思います。
Nextの後の変数、書く? 書かない?
このブログでは簡単に
For i = 0 To 9 ... Next
という書き方を紹介していますが、
For i = 0 To 9 ... Next i
と、Nextの後にカウンタ用変数を記載しているサイトも多くあります。
明記することで、多重ループでも分かりやすくなる(かもしれない)というメリットはありますが、
まあ、ただの好みと思ってよいでしょう。
' 見やすい…のか…? For i = 0 To 9 For j = 0 To 9 ... Next j Next i
個人的には、ループ終端でカウンタ用変数を意識する必要を感じていませんし、
インデントさえしっかりしていれば可読性もまったく問題ないと思います。
まとめ
長くなってしまいましたが、いかがでしょうか。
いろいろな書き方を紹介しましたが、
個人的には「まずはFor...To...NextとDo(While) ...Loopを覚えればOK」だと思います。
適切な書き方やパフォーマンスの良い書き方、読みやすい書き方はケースバイケースですが、
とりあえずその2つがあれば大抵の繰り返し処理はどうにかなります。
そしてしつこいようですが無限ループにはお気をつけて。
実行前には一度読み直し、Ctrl+Sを忘れずに。マジで。
次回予告
何書こうかな。