昨日、id:Infoment さんの「多重ループからの脱出」を読んで、たまに使っているテクニックを思い出したので、紹介するついでに思いつく限りの方法を整理してみた。
はじめに
一般的によく見かける多重ループからの(純粋な)脱出方法は、次の2つだと思う。
- 1.ラベルとGoTo文を使って抜け出す
- 2.脱出フラグを立てて段階的に抜け出す
だが、VB系の言語には特有の文法によって、第三の脱出方法が存在する。
- 3.本命の処理とは異なる繰り返しステートメントで囲って抜け出す(Exit Do)
(個人的におすすめできないが)次のような方法もある。
- 4.エラーをスローする(Err.Raise)
- 5.外側ループのカウンタを終了条件に変化させる
- 6.外側のループだけDoに変える(Exit Do)
ロジックによっては、純粋な多重ループからの脱出ではない方法で代用出来る場合もある
- 7.プロシージャから脱出する(Exit Sub / Exit Function)
- 8.実行中のVBAを終了させる(End)
というわけで、順番に見ていこう。
多重ループからの脱出方法
1.ラベルとGoTo文を使って抜け出す
この方法は、C言語時代から使われているので、誰が見ても分かりやすく完結に書ける点が優れている。
ラベルのインデントが左端に来てしまうのが気持ち悪いが、こればっかりはどうにもならない。
Sub 多重ループからブレイクする方法_1GoTo() Dim i As Long, j As Long '前処理1 For i = 1 To 3 '前処理2 For j = 1 To 3 '前処理3 Debug.Print i, j, i + j If i + j = 5 Then GoTo BreakLabel '後処理3-2 Next '後処理2-2 Next '後処理1-2(最後まで到達した時のみ実行) BreakLabel: '処理の続き End Sub
2.脱出フラグを立てて段階的に抜け出す
この方法もC言語時代から使われている有名な方法であり、あらゆる拡張に対応出来るという点が非常に優れている。
たとえば、全ての階層において脱出前に必要な後処理を実行させることだって出来る。
このテンプレートで対応出来ない処理があるとしたら、設計を見直したほうが良い気がする。
Sub 多重ループからブレイクする方法_2MustBreak() Dim i As Long, j As Long Dim MustBreak As Boolean '前処理1 For i = 1 To 3 '前処理2 For j = 1 To 3 '前処理3 Debug.Print i, j, i + j If i + j = 5 Then MustBreak = True '後処理3-1(脱出時も実行) If MustBreak Then Exit For '後処理3-2 Next '後処理2-1(脱出時も実行) If MustBreak Then Exit For '後処理2-2 Next '後処理1-1(脱出時も実行) If Not MustBreak Then: '後処理1-2(最後まで到達した時のみ実行) '処理の続き End Sub
3.本命の処理とは異なる繰り返しステートメントで囲って抜け出す(Exit Do)
本命の二重ループの外に、もう一段ループを重ねるという方法である。
今回の例では本命の二重ループがFor~Nextステートメント
で組まれているので、Do~Loopステートメント
で囲って三重ループとする。これで、どこからExit Do
を実行した場合でもLoop直後まで抜け出す事ができるようになる。
C言語などではForもDoもBreak
文を使って脱出するため、このような書き方は出来ない。VB系の言語専用と考えられる。
Sub 多重ループからブレイクする方法_3ExitDo() Dim i As Long, j As Long Do '前処理1 For i = 1 To 3 '前処理2 For j = 1 To 3 '前処理3 Debug.Print i, j, i + j If i + j = 5 Then Exit Do '後処理3-2 Next '後処理2-2 Next '後処理1-2(最後まで到達した時のみ実行) Loop While False '処理の続き End Sub
最後の部分は、単なるLoop
で終わらせてしまうと、Exit Do
が働かなかった場合に無限ループに陥ることになるため、絶対に繰り返しが起こらないような対処が必要となる。それが
Loop While False
か
Loop Until True
か
Exit Do Loop
である。
もし、本命の多重ループがDo~Loop
で統一されているならば、For
で囲って
Dim k As Long For k = 1 To 1
か
Exit For Next
という書き方で対応できる。
メリットは?
読者の皆さんは、この書き方にどんなメリットが有るのかと思うかもしれないので、はっきり言おう。
メリットはほぼ無い
所詮はただの遊びである。
それで終わりにしては意味がないので、もうちょっと頑張ってみる。
処理構造はGoTo ラベル
と全く同じなので、出来ることは何も変わっていない。だが開発者にとって有り難いことが2つもある。(無理矢理)
- 1.処理のブロックが明確になる
- 2.後続処理に同じようなループをコピペしたとき事故が起こらない
メリット1.処理のブロックが明確になる
GoToでは処理全体のブロックがはっきりしておらず、ちょっと分かりにくい。ラベルはインデント0の位置に書かなければならないので、インデントが気持ち悪いと感じるのは筆者だけではないはず。
しかし、Do~Loopならブロックの開始位置から終了までがインデントされるため、非常に美しいソースコードとなる。
下図がここまでの1、2、3、3インデント下げを並べてみたものである。
メリット2.後続処理に同じようなループをコピペしたとき事故が起こらない
実は1のGoToを使った方法で一番困るのは、プロシージャの中に複数回多重ループが現れて、どちらでも脱出したい時に、ラベルの名前が重複することだったりする。
コピペで済ませたくなるような、そっくりなループを2回、3回書いていたりする。すると・・下図のような出来事がよく起こる。(※こういうとき、たいてい綺麗なコードではない)
後ろに番号つけるなどして回避するだけなのだが、過去には誤ったラベルにジャンプさせる事故を何度か起こしている。
Do~Loop
のようにブロックをはっきりさせてしまえば、変更漏れがあってもバグが起こることは無い。
ただ、ネストが1段増える方が嫌だと言う人もいるので、使うかどうかは個人の好み次第だろう。
非推奨案
次の3つは個人的には使用は避けたいロジックである。
使うかどうかは個人の好みなので、読んで頂き参考になれば有り難い。
4.エラーをスローする(Err.Raise)
どう見ても1.ラベルとGoTo文を使って抜け出す
の劣化版である。
基本ロジックは1
と同じようにラベルを設置しておく必要があって、任意の場所で脱出出来るのは変わらないが、事前にOn Error GoTo ラベル
の実行が必要で、ジャンプさせるキッカケがGoTo
からErr.Raise
ステートメントに変わっている。
確かに脱出はできるが、どこででどんなエラーが起きようとジャンプしてしまう。
ジャンプした先で適切にエラー処理をしないと意図したとおりに動かなくなるので、素直にGoTo
で専用のラベルに飛ばしたほうが百倍マシである。
Sub 多重ループからブレイクする方法_4OnErrorGoToLabel() Dim i As Long, j As Long On Error GoTo ErrorLabel '前処理1 For i = 1 To 3 '前処理2 For j = 1 To 3 '前処理3 Debug.Print i, j, i + j If i + j = 5 Then Err.Raise 1234 '後処理3-2 Next '後処理2-2 Next '後処理1-2(最後まで到達した時のみ実行) ErrorLabel: If Not Err.Number = 1234 Then '本当のエラー処理 Resume End If On Error GoTo 0 '処理の続き End Sub
5.外側ループのカウンタを終了条件に変化させる
ループのカウンタを終了条件に変化させて、Exit
などをせずとも自然と多重ループからも抜けられる。というアイディアである。
この方法では、プログラムの構造を変化させることなく抜け出す事ができるため、1と同じくらいスッキリ書くことが出来る。ラベルという気持ち悪いものも存在しない。
代わりに、終了条件を再利用するため、変数・定数を準備しておく必要がある。もし終了条件が変数に格納済みのような場合は、非常に有効な手段となるだろう。
※ただし、筆者はFor
のカウンタ変数を書き換える行為を禁止しているので、この書き方は非推奨とした。
Sub 多重ループからブレイクする方法_5ForCounterAdd() Dim i As Long, j As Long '前処理1 Const i_End = 3 For i = 1 To i_End Step 1 '前処理2 Const j_End = 3 For j = 1 To j_End '前処理3 Debug.Print i, j, i + j If i + j = 5 Then i = i_End: j = j_End '後処理3-1 Next '後処理2-1 Next '後処理1-1 '処理の続き End Sub
6.内側と外側の繰り返しステートメントを変える
3.本命の処理とは異なる繰り返しステートメントで囲って抜け出す
の機能縮減版である。
二重ループがFor~Next For~Next
なら、Do~Loop For~Next
に置き換えれば3
と同じ理屈で抜け出せる!わざわざ外側にDoを増やさなくても良いよねって話である。
もし外側のループで、Doが適している場面なら良い方法だと思う。
ただ、元からFor~Next For~Next
と書かれているものは、Do~Loop
が適していない場面が多いのでコードが読みづらくなる恐れがあるので注意したい。
Sub 多重ループからブレイクする方法_6ExitDo() Dim i As Long, j As Long i = 1 Do '前処理 For j = 1 To 3 '前処理 Debug.Print i, j, i + j If i + j = 5 Then Exit Do '後処理 Next '後処理 i = i + 1 Loop Until i > 3 End Sub
純粋な脱出ではない実用的な方法
純粋な多重ループからの脱出ではないが、もっと上位から脱出する書き方もよく使われる。
7.プロシージャから脱出する(Exit Sub / Exit Function)
元の記事にもあった3つめの方法である。
適切に関数化されたソースコードでは、かなりの頻度で使用される記法である。
Forを抜けた後に処理が必要ない場合は、これほど簡潔に書ける方法は後に紹介する方法以外には存在しないだろう。
Sub 多重ループからブレイクする方法_7ExitSub() Dim i As Long, j As Long '前処理 For i = 1 To 3 '前処理 For j = 1 To 3 '前処理 Debug.Print i, j, i + j If i + j = 5 Then Exit Sub '後処理 Next '後処理 Next End Sub
8.実行中のVBAを終了させる(End)
実行中のVBAを完全に終了させるという、恐ろしい呪文である。
下手に使うとイロイロと問題が起こるので、この程度の処理では使うべきではない。
Endの解説記事は・・・まだ書いてなかったので、そのうち書くとして、上記のExit Sub
をEnd
に変えるだけなのでコードは省略する。
関連ツイート
発端のツイートはこちらを参照
https://twitter.com/KotorinChunChun/status/1274346091245256704?s=20
各種回答ツリー
https://twitter.com/EasyPea69404878/status/1274473011349864450?s=20
5.に関する回答
https://twitter.com/LimgTW/status/1274377034689507328?s=20
5.に関するちょっとした議論
https://twitter.com/LimgTW/status/1274624470859182080
まとめ
多重ループの脱出方法を整理してみました。
使えるものの選択肢は無いので、あまり迷うことはありませんが、何かのアイディアの元になれば幸いです。
以上
何か御座いましたらコメント欄、またはTwitterからどうぞ♪
それではまた来週♪ ちゅんちゅん(・8・)