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

# はじめに
ことりちゅん氏の『[VBAで多重ループから脱出する方法の全て](https://www.excel-chunchun.com/entry/20200621-loop-break)』という記事を読んだので、**Delphi** ではどういった手法があるのか調べてみる事にしました (ついでに**標準 Pascal** での手法も考えてみます)。

# Delphi で多重ループを抜ける

処理はこんな感じです。

```pascal: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:**

 - [標準 Pascal 範囲内での Delphi 入門 (Qiita)](https://qiita.com/ht_deko/items/05af01a81f7c3bb5eeb4)


## ■ goto 文を使う

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

```pascal: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.
```

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

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

```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:**

 - [goto 文 - 宣言と文（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%AE%A3%E8%A8%80%E3%81%A8%E6%96%87%EF%BC%88Delphi%EF%BC%89#goto_.E6.96.87)
 - [4.7. goto 文 - <4> 動作の概念 (標準 Pascal 範囲内での Delphi 入門) (Qiita)](https://qiita.com/ht_deko/items/05af01a81f7c3bb5eeb4#47-goto-%E6%96%87)


## ■ 脱出フラグを使う

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

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

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

```pascal: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:**

 - [System.Break（Delphi）(DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Break)
 - [System.Continue（Delphi）(DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Continue)
 - [(4.4.5.) break と continue - <4> 動作の概念 (標準 Pascal 範囲内での Delphi 入門) (Qiita)](https://qiita.com/ht_deko/items/05af01a81f7c3bb5eeb4#445-break-%E3%81%A8-continue)


## ■ 関数内関数を使う

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

```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.
```
関数にすると多重ループをどうやって抜けたのかを判別できます。

```pascal: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() で抜けます。

```pascal
              Result := False; // 途中で抜けたら False を返す
              Exit;        
...
    Result := True; // 途中で抜けたら False を返す
    Exit;     
```


 - **長所**: スッキリとした処理を記述できる。
 - **短所**: ループ部分が実際の処理の場所から離れる。
 - **標準 Pascal**: Exit() はない。

**See also:**

 - [ネストルーチン（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%89%8B%E7%B6%9A%E3%81%8D%E3%81%A8%E9%96%A2%E6%95%B0%EF%BC%88Delphi%EF%BC%89#.E3.83.8D.E3.82.B9.E3.83.88_.E3.83.AB.E3.83.BC.E3.83.81.E3.83.B3)
 - [System.Exit (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Exit)
 - [0.4. 有効範囲 (スコープ) - <0> はじめに (標準 Pascal 範囲内での Delphi 入門) (Qiita)](https://qiita.com/ht_deko/items/afb3c008a83a20792169#04-%E6%9C%89%E5%8A%B9%E7%AF%84%E5%9B%B2-%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97)
 - [結果 (Result) - <11> 手続きと関数 (標準 Pascal 範囲内での Delphi 入門) (Qiita)](https://qiita.com/ht_deko/items/b93ac03bfee002f17137#%E7%B5%90%E6%9E%9C-result)

## ■ 無名メソッドを使う

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

```pascal: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.
```

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

```pascal: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:**

 - [Delphi での無名メソッド (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/Delphi_%E3%81%A7%E3%81%AE%E7%84%A1%E5%90%8D%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)
 - [IIFE (即時実行関数式) (MDN Web Docs 用語集)](https://developer.mozilla.org/ja/docs/Glossary/IIFE)


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

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

```pascal:loop.inc

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

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

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

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

```pascal: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() で抜けられるように書けます。

最初にインクルードすると、その場所に

```pascal
  (procedure
   begin
```

が展開され条件シンボル `LOOP_PASS1` が定義されます。二回目だと

```pascal
   end)();
```

が展開され、条件シンボル `LOOP_PASS1` の定義が解除されます。

こうしてインクルードの回数によって展開されるコードがトグルするので、ネストしない限り繰り返し使う事ができます。

ただ、本来の文脈での Exit() だと **program** を抜けるハズなので、`LOOP.INC` の中身を理解していない人に誤解を与えかねません。


 - **長所**: スッキリとした処理を記述できる。
 - **短所**: 何をやっているのか解りにくい。
 - **標準 Pascal**: インクルード指令や無名メソッドはない。

**See also:**

 - [インクルード ファイル（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%AB%E3%83%BC%E3%83%89_%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%EF%BC%88Delphi%EF%BC%89)
 - [条件付きコンパイル（Delphi）(DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E6%9D%A1%E4%BB%B6%E4%BB%98%E3%81%8D%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%AB%EF%BC%88Delphi%EF%BC%89)

## ■ 例外処理を使う

Abort() を使い、サイレント例外を発生させて抜けます。

```pascal: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:**

 - [System.SysUtils.Abort (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Abort)
 - [System.SysUtils.EAbort (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.EAbort)
 - [例外の生成と処理 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E4%BE%8B%E5%A4%96%EF%BC%88Delphi%EF%BC%89#.E4.BE.8B.E5.A4.96.E3.81.AE.E7.94.9F.E6.88.90.E3.81.A8.E5.87.A6.E7.90.86)


## ■ アプリケーションを終了する

**program** 内のループで Exit() を呼ぶとアプリケーションを終了させられます。

```pascal: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） (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E5%88%B6%E5%BE%A1)
 - [System.Exit (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Exit)
 - [System.Halt (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.Halt)
 - [Vcl.Forms.TApplication.Terminate (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Forms.TApplication.Terminated)
 - [FMX.Forms.TApplication.Terminate (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/FMX.Forms.TApplication.Terminated)

# おわりに
**Delphi** だと一般的には **goto** を使うのがセオリーかと思いますが、**標準 Pascal** とは異なり、多重ループを抜ける方法がイロイロありますね。

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

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

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

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

**See also:**

 - [割と簡単に ’標準 Modula-2 / Oberon-2' を試してみたい (Qiita)](https://qiita.com/ht_deko/items/8e8be01f749915167a91)
 - [割と簡単に ’標準 Pascal' を試してみたい (Qiita)](https://qiita.com/ht_deko/items/41e95154e8da2f901698)