アナログCPU:5108843109

ゲームと音楽とプログラミング(酒と女とロックンロールのノリで)

('ω') < イザユケエンジニャー

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を忘れずに。マジで。