はじめに
ことりちゅん氏の『VBAで多重ループから脱出する方法の全て』という記事を読んだので、Delphi ではどういった手法があるのか調べてみる事にしました (ついでに標準 Pascal での手法も考えてみます)。
Delphi で多重ループを抜ける
処理はこんな感じです。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
; // どうにかして抜ける
end;
end;
// ここに抜ける
Writeln('Done.');
Readln;
end.
この二重ループを抜ける方法を考えてみます。
See also:
■ goto 文を使う
一番シンプルな方法です。
program ExitLoop;
{$APPTYPE CONSOLE}
label
JP;
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
goto JP; // goto を使って抜ける
end;
end;
JP:
Writeln('Done.');
Readln;
end.
Delphi の goto は手続き内 goto (intraprocedural gotos) なので、手続き/関数の外にはジャンプできませんし、ループの内側へもジャンプできません。
標準 Pascal だとこういう書き方になります。
program ExitLoop(Input, Output);
label
99;
var
i, l: Integer;
begin
for i:=1 to 10 do
begin
for l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
goto 99;
end;
end;
99:
Writeln('Done.');
Readln;
end.
- 長所: シンプル。
- 短所: ラベルはラベル宣言部で事前に宣言する必要がある。
- 標準 Pascal: ラベルには 4 桁までの符号なし整数しか使えない (ラベルに識別子は使えない)。
See also:
■ 脱出フラグを使う
脱出用のフラグを使って抜ける方法です。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
var ExitFlg := False;
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
ExitFlg := True; // 脱出フラグを立てる
if ExitFlg then // 脱出フラグが立っていたら
Break; // ループを抜ける
end;
if ExitFlg then // 脱出フラグが立っていたら
Break; // ループを抜ける
end;
Writeln('Done.');
Readln;
end.
標準 Pascal では Break() が使えないため、次のようなコードとなります。
program ExitLoop(Input, Output);
var
ExitFlg: Boolean;
i, l: Integer;
begin
ExitFlg := False;
for i:=1 to 10 do
begin
if not ExitFlg then { 脱出フラグが立っていたらループ内を処理しない }
begin
for l:=1 to 10 do
begin
if not ExitFlg then { 脱出フラグが立っていたらループ内を処理しない }
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
ExitFlg := True; { 脱出フラグを立てる }
end;
end;
end;
end;
Writeln('Done.');
Readln;
end.
ちょっとお題からは外れますが、標準 Pascal なら While ループで置き換えた方が処理を追いやすいコードになるかもしれません。
program ExitLoop(Input, Output);
var
ExitFlg: Boolean;
i, l: Integer;
begin
ExitFlg := False;
i := 1;
while (i <= 10) and not ExitFlg do { ループカウンタが範囲内または脱出フラグが立っていない }
begin
l := 1;
while (l <= 10) and not ExitFlg do { ループカウンタが範囲内または脱出フラグが立っていない }
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
ExitFlg := True; { 脱出フラグを立てる }
l := l + 1; { ループカウンタ +1 }
end;
i := i + 1; { ループカウンタ +1 }
end;
Writeln('Done.');
Readln;
end.
- 長所: goto を使わなくていい。
- 短所: フラグの管理が面倒。
- 標準 Pascal: Break() がないため、ちょっと複雑なロジックになる。
See also:
- System.Break(Delphi)(DocWiki)
- System.Continue(Delphi)(DocWiki)
- (4.4.5.) break と continue - <4> 動作の概念 (標準 Pascal 範囲内での Delphi 入門) (Qiita)
■ 関数内関数を使う
ループを外に追い出すやり方です。Pascal だと関数内関数 (ネストルーチン) が使えるので、あまりコードが遠くに行きません。
program ExitLoop;
{$APPTYPE CONSOLE}
procedure Proc;
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Exit; // Proc() を抜ける
end;
end;
end; { Proc }
begin
Proc;
Writeln('Done.');
Readln;
end.
関数にすると多重ループをどうやって抜けたのかを判別できます。
program ExitLoop;
{$APPTYPE CONSOLE}
function Func: Boolean;
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Exit(False); // 途中で抜けたら False を返す
end;
end;
Exit(True); // 最後までループしたら True を返す
end; { Func }
begin
var ExitFlg := Func;
Writeln('Done. ', ExitFlg);
Readln;
end.
結果パラメータを伴う Exit() は Delphi 2009 以降で使えます。それ以前では Result 変数に結果を設定してからパラメータを伴わない Exit() で抜けます。
Result := False; // 途中で抜けたら False を返す
Exit;
...
Result := True; // 途中で抜けたら False を返す
Exit;
- 長所: スッキリとした処理を記述できる。
- 短所: ループ部分が実際の処理の場所から離れる。
- 標準 Pascal: Exit() はない。
See also:
- ネストルーチン(Delphi)(DocWiki)
- System.Exit (DocWiki)
- 0.4. 有効範囲 (スコープ) - <0> はじめに (標準 Pascal 範囲内での Delphi 入門) (Qiita)
- 結果 (Result) - <11> 手続きと関数 (標準 Pascal 範囲内での Delphi 入門) (Qiita)
■ 無名メソッドを使う
関数内関数でやっていたものを無名メソッドで置き換えたものです。処理が関数内関数よりも遠くに行かないので見通しが良くなります。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
(procedure
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Exit; // 無名手続きを抜ける
end;
end;
end)();
Writeln('Done.');
Readln;
end.
無名関数にすると多重ループをどうやって抜けたのかを判別できます。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
var ExitFlg := (function : Boolean
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Exit(False); // 途中で抜けたら False を返す
end;
end;
Exit(True); // 最後までループしたら True を返す
end)();
Writeln('Done.', ExitFlg);
Readln;
end.
- 長所: スッキリとした処理を記述できる。
- 短所: 無名メソッドは Delphi 2009 以降でないと使えない。IIFE は XE4 以降でないとエラーになるか実行されない。
- 標準 Pascal: 無名メソッドはない。
See also:
■ インクルードファイルを使う
まず、このようなインクルードファイルを用意します。
{$IFNDEF LOOP_PASS1}
(procedure
begin
{$ENDIF}
{$IFDEF LOOP_PASS1}
end)();
{$ENDIF}
{$IFDEF LOOP_PASS1}
{$UNDEF LOOP_PASS1}
{$ELSE}
{$DEFINE LOOP_PASS1}
{$ENDIF}
本体のコードはこうなります。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
{$I LOOP.INC} // 多重ループ開始
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Exit; // 無名手続きを抜ける
end;
end;
{$I LOOP.INC} // 多重ループ終了
Writeln('Done.');
Readln;
end.
無名手続きをインクルードファイルを使って隠蔽していて、あたかも多重ループを Exit() で抜けられるように書けます。
最初にインクルードすると、その場所に
(procedure
begin
が展開され条件シンボル LOOP_PASS1
が定義されます。二回目だと
end)();
が展開され、条件シンボル LOOP_PASS1
の定義が解除されます。
こうしてインクルードの回数によって展開されるコードがトグルするので、ネストしない限り繰り返し使う事ができます。
ただ、本来の文脈での Exit() だと program を抜けるハズなので、LOOP.INC
の中身を理解していない人に誤解を与えかねません。
- 長所: スッキリとした処理を記述できる。
- 短所: 何をやっているのか解りにくい。
- 標準 Pascal: インクルード指令や無名メソッドはない。
See also:
■ 例外処理を使う
Abort() を使い、サイレント例外を発生させて抜けます。
program ExitLoop;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
begin
try
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
Abort; // サイレント例外を発生させる
end;
end;
except
on E: EAbort do; // サイレント例外 (EAbort) をトラップして何もしない
end;
Writeln('Done.');
Readln;
end.
EAbort 以外の例外を握り潰さないので、ループ内で他のエラーが発生したらそこで止まります。
複雑な抜け方をしなくてはならない場合には、独自の例外クラスを定義し raise で例外を生成します。
- 長所: 割とスッキリとした処理になる。
- 短所: uses に System.SysUtils が必要 (例外を発生させるコストが高い)。
- 標準 Pascal: 例外処理はない。
See also:
■ アプリケーションを終了する
program 内のループで Exit() を呼ぶとアプリケーションを終了させられます。
program ExitLoop;
{$APPTYPE CONSOLE}
begin
for var i:=1 to 10 do
begin
for var l:=1 to 10 do
begin
Writeln(i, ',' , l);
if (i = 5) and (l = 5) then
begin
ExitCode := 0; // 終了コード = 0
Exit; // program から抜ける
end;
end;
ExitCode := 1; // 終了コード = 1
Exit; // program から抜ける
end;
Writeln('Done.'); // 実行されない
Readln;
end.
Exit() を呼んだ時点でプログラムを終了するので、後続の処理は実行されません。Exit() の代わりに Halt() を使うと強制終了になります。
なお、VCL や FireMonkey 等のフレームワークでは Application.Terminate を使ってアプリケーションを終了させられます。
- 長所: 問答無用で終了できる。
- 短所: 後続の処理を実行できない。
- 標準 Pascal: Exit() や Halt() はない。
See also:
- プログラムの制御(Delphi) (DocWiki)
- System.Exit (DocWiki)
- System.Halt (DocWiki)
- Vcl.Forms.TApplication.Terminate (DocWiki)
- FMX.Forms.TApplication.Terminate (DocWiki)
おわりに
Delphi だと一般的には goto を使うのがセオリーかと思いますが、標準 Pascal とは異なり、多重ループを抜ける方法がイロイロありますね。
多重ループを抜ける方法に何か漏れがありましたらコメント欄でご指摘ください m(_ _)m
ちなみに、ヴィルト先生が Pascal の次に作った Modula-2 には goto がありません。
MODULE ExitLoop;
IMPORT InOut;
VAR
c: CHAR;
i, l: INTEGER;
BEGIN
LOOP
FOR i:=1 TO 10 DO
FOR l:=1 TO 10 DO
InOut.WriteInt(i, 3); InOut.WriteString(', ');
InOut.WriteInt(l, 3); InOut.WriteLn;
IF (i=5) AND (l=5) THEN
EXIT; (* LOOP から抜ける *)
END;
END;
END;
END;
InOut.WriteString('Done.');
InOut.Read(c);
END ExitLoop.
Modula-2 では、無限ループ LOOP~END と EXIT を使ってループを一気に抜ける事ができます。逆に FOR 文等の条件ループは EXIT で抜ける事ができません。この LOOP~END を実装した Pascal 処理系もあります。
Delphi にも欲しいな、この無限ループ (と終了手続き)。
See also: