17
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Delphi で多重ループを抜ける方法

Last updated at Posted at 2020-06-22

はじめに

ことりちゅん氏の『VBAで多重ループから脱出する方法の全て』という記事を読んだので、Delphi ではどういった手法があるのか調べてみる事にしました (ついでに標準 Pascal での手法も考えてみます)。

Delphi で多重ループを抜ける

処理はこんな感じです。

ExitLoop.dpr
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 文を使う

一番シンプルな方法です。

ExitLoop.dpr
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.

Delphigoto手続き内 goto (intraprocedural gotos) なので、手続き/関数の外にはジャンプできませんし、ループの内側へもジャンプできません。

標準 Pascal だとこういう書き方になります。

ExitLoop.pas
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:

■ 脱出フラグを使う

脱出用のフラグを使って抜ける方法です。

ExitLoop.dpr
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() が使えないため、次のようなコードとなります。

ExitLoop.pas
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 ループで置き換えた方が処理を追いやすいコードになるかもしれません。

ExitLoop.pas
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:

■ 関数内関数を使う

ループを外に追い出すやり方です。Pascal だと関数内関数 (ネストルーチン) が使えるので、あまりコードが遠くに行きません。

ExitLoop.dpr
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.

関数にすると多重ループをどうやって抜けたのかを判別できます。

ExitLoop.dpr
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:

■ 無名メソッドを使う

関数内関数でやっていたものを無名メソッドで置き換えたものです。処理が関数内関数よりも遠くに行かないので見通しが良くなります。

ExitLoop.dpr
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.

無名関数にすると多重ループをどうやって抜けたのかを判別できます。

ExitLoop.dpr
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:

■ インクルードファイルを使う

まず、このようなインクルードファイルを用意します。

loop.inc

{$IFNDEF LOOP_PASS1}
  (procedure
   begin
{$ENDIF}

{$IFDEF LOOP_PASS1}
   end)();
{$ENDIF}

{$IFDEF LOOP_PASS1}
  {$UNDEF LOOP_PASS1}
{$ELSE}
  {$DEFINE LOOP_PASS1}
{$ENDIF}

本体のコードはこうなります。

ExitLoop.dpr
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() を使い、サイレント例外を発生させて抜けます。

ExitLoop.dpr
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() を呼ぶとアプリケーションを終了させられます。

ExitLoop.dpr
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 だと一般的には goto を使うのがセオリーかと思いますが、標準 Pascal とは異なり、多重ループを抜ける方法がイロイロありますね。

多重ループを抜ける方法に何か漏れがありましたらコメント欄でご指摘ください m(_ _)m

ちなみに、ヴィルト先生が Pascal の次に作った Modula-2 には goto がありません。

ExitLoop.mod
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 では、無限ループ LOOPENDEXIT を使ってループを一気に抜ける事ができます。逆に FOR 文等の条件ループは EXIT で抜ける事ができません。この LOOPEND を実装した Pascal 処理系もあります。

Delphi にも欲しいな、この無限ループ (と終了手続き)。

See also:

17
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?