Delphi の例外処理

# はじめに
Delphi の例外処理に関する記事です。

**See also:**

 - [例外 (Delphi) (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E4%BE%8B%E5%A4%96%EF%BC%88Delphi%EF%BC%89)


# 例外処理
例外は、エラーやその他のイベントによってプログラムの通常の実行が中断された場合に生成されます。

例外を処理するには次の 2 つの **try** 文を使います。
 
 - **try…except…end**
 - **try…finally…end**

例外は例外型 (例外クラス) のオブジェクトです。例外型はクラスなので、詳細な例外処理を行うためには `SysUtils` を **uses** に追加する必要があります。アプリケーションが `SysUtils` ユニットを使う場合、殆どの実行時エラーが自動的に例外に変換されます。

裏を返せば 「ざっくりとした例外処理なら `SysUtils` を使わずにやれる」という事でもあります。

## 例外型の派生
最も基本的な例外型は `Exception` であり、`System.SysUtils` で次のように定義されています。

```pascal
type
  Exception = class(TObject);
```

例外型を派生させる事によって、例外を細かく制御できるようになります。

```pascal
type
  EException = class(Exception);
  EExceptionA = class(EException);
  EExceptionB = class(EException);
  EExceptionC = class(EException);
```

クラス名はプレフィクスとして `T` を付けるのが慣例となっていますが、例外型名には `E` を付けるのが慣例となっています。

`System.SysUtils` には様々な例外が定義されていますが、最も有名 (?) なのは `EProgrammerNotFound` でしょう [^1]。日本語で読むと **「いいプログラマのっとふぁうんど」** となり、さらに深刻度が増します。

**See also:**

 - [System.SysUtils.Exception (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Exception)
 - [System.SysUtils.EProgrammerNotFound (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.EProgrammerNotFound)
 - [1. Basic Types Declarations - Delphi’s Object Pascal Style Guide (DocWiki)](http://docwiki.embarcadero.com/RADStudio/en/Type_Declarations#1._Basic_Types_Declarations)


## 例外の生成
例外を発生させるには (例外オブジェクトを生成するには) **raise** 文を使用します。

```
raise 文 =
  raise [例外インスタンス [at アドレス式]] .
```

通常、例外を発生させる場合には、

```pascal
raise Exception.Create('Error');
```

のように書きます。

`アドレス式` に指定するのはポインタ型として評価できるものであれば何でも構いませんが、普通は手続きや関数へのポインタとなります。アドレス式を指定しなかった場合には例外を上げた場所に位置が設定されます。

```pascal
  procedure proc;
  begin
    raise Exception.Create('Error'); // 例外位置
  end;
```

アドレス式 (例では手続きへのポインタ) を指定した場合には手続きの先頭に位置が設定されます。

```pascal
  procedure proc;
  begin // 例外位置
    raise Exception.Create('Error') at @proc;
  end;
```

例外型のメソッドの殆どはコンストラクタです。例えば **raise** 文で、例外型の `CreateFmt()` を 使うと、書式付きメッセージ文字列を生成できます。

```pascal
  raise Exception.CreateFmt('Error at (%d, %d)', [Col, Row]);
```

用途に応じて例外型のコンストラクタを選択するとよいでしょう。

例外オブジェクトを自前で破棄する必要はありません。例外は例外ハンドラ機構により自動的に破棄されます。

**See also:**

 - [(11.4.) 手続き型 (Procedural Types)](https://qiita.com/ht_deko/items/b93ac03bfee002f17137#114-%E6%89%8B%E7%B6%9A%E3%81%8D%E5%9E%8B-procedural-types)
 - [Exception.CreateFmt (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Exception.CreateFmt)


## try…except 文 (例外ハンドラなし)

**try** ブロックで例外が発生すると例外ブロック (**except** ブロック) が実行されます。

```
try…except 文 =
  try 文リスト except 例外ブロック end .

文リスト = 
  文 {";" 文} .

例外ブロック = 
  [例外ハンドラ {";" 例外ハンドラ} [else 文リスト]] | 文リスト .

例外ハンドラ =
  on [識別子 ":"] 例外型識別子 do 文 .
```

まずは例外ハンドラのない、シンプルな **try…except** 文を見てみましょう。

```pascal
try
  // 文リスト1 (例外が発生しそうな処理)
except
  // 文リスト2 (例外トラップ)
end;
```

次のようなコンソールアプリケーションがあったとします。リリースビルドで実行ファイルを生成し、コマンドラインから実行してテストしてみてください。

```pascal:Project1.dpr
program Project1;
{$APPTYPE CONSOLE}
uses
  System.SysUtils;

procedure TEST1(Flg1, Flg2: Boolean);
begin
  Writeln('try 文開始前');
  try
    Writeln('エラー発生源前');
    if Flg1 then
      raise Exception.Create('エラー');
    if Flg2 then
      Exit;
    Writeln('エラー発生源後');
  except
    Writeln('例外トラップ');
  end;
  Writeln('try 文終了後');
end;

begin
  Writeln('BEGIN');
  TEST1(False, False);
  Writeln('END');
end.
```

手続き `TEST1()` は、2 つのパラメータの組み合わせにより 4 つの実行結果があります。それぞれの結果をみてみましょう。

2 つのパラメータが共に **False** だと例外が発生しないため、例外ブロック内が実行されません。

```text:TEST1(False,False)
BEGIN
try 文開始前
エラー発生源前
エラー発生源後
try 文終了後
END
```

Flg1 が **False** で Flg2 が **True** だと、**Exit** により `TEST1()` を抜けるため、それ以降が実行されません。

```text:TEST1(False,True)
BEGIN
try 文開始前
エラー発生源前
END
```

Flg1 が **True** だと、**Exit** 前に例外が発生するため、Flg2 の状態に関係なく結果は同じになります。エラー発生源後の処理が実行されず、例外トラップ処理が実行されています。

```text:TEST1(True,False)
BEGIN
try 文開始前
エラー発生源前
例外トラップ
try 文終了後
END
```

これは簡単に理解できると思います。例外ブロックに何も処理を書かなければ例外を握りつぶす事になりますが、バグの温床となりかねないので、ご利用は計画的に。

 - **try** ブロックで例外が発生すると、**except** ブロックを実行し、制御は **end** の後の文に移る。
 - **try** ブロックで例外が発生しなかった場合、**except** ブロックは無視され、制御は **end** の後の文に移る。

## try…finally 文
**try** ブロックで例外が発生してもしなくても、**finally** ブロックが実行されます。

```
try…finally 文 =
  try 文リスト finally 文リスト end .

文リスト = 
  文 {";" 文} .
```

**try…finally** 文はシンプルで、このようになっています。

```pascal
try
  // 文リスト1 (例外が発生しそうな処理)
finally
  // 文リスト2 (必須処理)
end;
```

**try…except** 文の時と同じようなコンソールアプリケーションを作ってみます。リリースビルドで実行ファイルを生成し、コマンドラインから実行してテストしてみてください。

```pascal:Project1.dpr
program Project1;
{$APPTYPE CONSOLE}
uses
  System.SysUtils;

procedure TEST2(Flg1, Flg2: Boolean);
begin
  Writeln('try 文開始前');
  try
    Writeln('エラー発生源前');
    if Flg1 then
      raise Exception.Create('エラー');
    if Flg2 then
      Exit;
    Writeln('エラー発生源後');
  finally
    Writeln('必須処理');
  end;
  Writeln('try 文終了後');
end;

begin
  Writeln('BEGIN');
  TEST2(False, False);
  Writeln('END');
end.
```

手続き `TEST2()` も、2 つのパラメータの組み合わせにより 4 つの実行結果があります。それぞれの結果をみてみましょう。

2 つのパラメータが共に **False** だと例外が発生しませんが、**finally** ブロックは実行されます。

```text:TEST2(False,False)
BEGIN
try 文開始前
エラー発生源前
エラー発生源後
必須処理
try 文終了後
END
```

Flg1 が **False** で Flg2 が **True** だと、**Exit** により `TEST2()` を抜けるため、それ以降が実行されませんが、<font color="Crimson">**finally** ブロックは実行されます</font>。

```text:TEST2(False,True)
BEGIN
try 文開始前
エラー発生源前
必須処理
END
```

Flg1 が **True** だと、**Exit** 前に例外が発生するため、Flg2 の状態に関係なく結果は同じになります。エラー発生源後の処理が実行されず、例外トラップ処理は実行されますが、<font color="Crimson">**try** 文の後に制御は移らず、TEST2() 呼び出し後の処理も実行されません</font>。

```text:TEST2(True,False)
BEGIN
try 文開始前
エラー発生源前
必須処理
Exception がモジュール Project1.exe の 0001C9A8 で発生しました。
エラー.
```

思いもよらない挙動だったかもしれません。

 - **try** ブロックで例外が発生した場合、**finally** ブロックを実行し、ルーチン (またはプログラム) を抜ける。
 - **try** ブロックで例外が発生しなかった場合、**finally** ブロックを実行し、制御は **end** の後の文に移る。
 - **try** ブロックを **Exit** (または **Break**、または **Continue**) で抜けようとすると、**finally** ブロックが実行される。
 - **finally** ブロックはユニットにおける **finalization** セクションのようなもの。処理が **try** ブロック内に入ったならば、ルーチンを抜けるまでに必ず実行される。
 - 例外により **finally** ブロックが実行された場合、処理が終わると例外が再生成される。
 - その性格上、ルーチン内に複数の **try…finally** 文を記述するのは避けた方がいい。

## ネストされた try 文

`例外が発生したら、メモリリークが起きないようにオブジェクトを破棄してすぐに手続き/関数を抜ける` のであれば、次のようなコードは意図した通りに動作するでしょう。

```pascal
procedure TEST;
begin
  //  任意の処理

  // オブジェクトの生成
  try
    // オブジェクトの操作
  finally
    // 必須処理 (オブジェクトの破棄)
  end;

  // 任意の処理
end;
```

やりがちなのは、コードのコピペによる意味の変化です [^2]。これをやってしまうとコードが意図した通りに動作しない事があります。

```pascal
procedure TEST;
begin
  //  任意の処理

  // オブジェクトの生成
  try
    // オブジェクトの操作
  finally
    // 必須処理 (オブジェクトの破棄)
  end;

  // 任意の処理

  // オブジェクトの生成
  try
    // オブジェクトの操作
  finally
    // 必須処理 (オブジェクトの破棄)
  end;

  // 任意の処理
end;
```

`例外が発生したら、メモリリークが起きないようにオブジェクトを破棄してすぐに手続き/関数を抜ける` のであれば全く問題のないコードです。しかしながら、`途中で例外が発生しても後続の処理を続行しなくてはならない` のであれば正しく動作しないかもしれないコードとなります。後者の場合には、**try…finally** 文と **try…except** 文をネストして使う事をオススメします。

```pascal
try
  try
    // 文リスト1
  finally
    // 文リスト2 (必須処理)
  end;
except
end;
```

この書き方ならば、途中で例外が発生しても処理を続行する事ができます。ネストの順序 (外/内) はどちらが先でも大差はないと思います。

「**try…finally** 文を単独で使うのをやめて、ネストした **try** 文で置き換えろ」と言っているのではありません。

## try…except 文 (例外ハンドラあり)
**try…except** 文では**例外ハンドラ**を記述する事によって例外を細かく制御できます。

```pascal
try
  // 文リスト1 (例外が発生しそうな処理)
except
  on EExceptionA do
    // 文リスト2 (例外 EExceptionA に対する処理)
  on EExceptionB do
    // 文リスト2 (例外 EExceptionB に対する処理)
  on EExceptionC do
    // 文リスト3 (例外 EExceptionC に対する処理)
  else
    // 上記例外ハンドラで捕捉できなかった例外に対する処理
end;
```

次のようなコンソールアプリケーションがあったとします。

```pascal
program Project1;

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

type
  EException = class(Exception);
  EExceptionA = class(EException);
  EExceptionB = class(EException);
  EExceptionC = class(EException);

procedure TEST3(n: Integer);
begin
  try

    case n of
      1: raise EExceptionA.Create('エラーA');
      2: raise EExceptionB.Create('エラーB');
      3: raise EExceptionC.Create('エラーC');
    else
      raise EException.Create('エラー');
    end;

  except
    on A: EExceptionA do
      begin
        // 文リスト2 (例外 EExceptionA に対する処理)
        Writeln(A.Message);
      end;
    on B: EExceptionB do
      begin
        // 文リスト2 (例外 EExceptionB に対する処理)
        Writeln(B.Message);
      end;
    on C: EExceptionC do
      begin
        // 文リスト3 (例外 EExceptionC に対する処理)
        Writeln(C.Message);
      end;
    else
      begin
        Writeln(Exception(ExceptObject).ClassName);
        raise; // 例外の再生成
      end;
  end;
end;

begin
  Writeln('BEGIN');

  TEST3(1);
  TEST3(2);
  TEST3(3);
  TEST3(4);

  Writeln('END');
end.
```

上記コードを実行すると、次のような結果になります。

```text
BEGIN
エラーA
エラーB
エラーC
EException
EException がモジュール Project1.exe の 0001CE5D で発生しました。
エラー.
```

### 例外ハンドラ

例外ハンドラは次のような形式です。

```
on [識別子 ":"] 例外型識別子 do 文
```

`例外型識別子` には捕捉する例外の型 (例外型) を指定します。`ExceptObject()` 関数を使って現在の例外 (オブジェクト) を取得してもいいのですが、

```pascal
on E: Exception do
```

のように、捕捉した例外オブジェクトに `名前 (識別子)` を付けて扱う事も出来ます。コロンがあると **case** 文の `選択肢定数` のように見えるかもしれませんが、どちらかというと**インライン変数宣言**の方が近いかもしれません。例外ブロック内で例外ハンドラの識別子を重複させても問題ありません。すべて `E` という名前でも OK です。

`文` は単文 (単純文) でも複文 (複合文) でも構いません。

例外ハンドラ **else** の記述は任意です。普通は **else** の中味を記述せずに例外を握りつぶすか、**単独の raise** を記述して例外を再生成するかのどちらかになると思います。なお、単独の **raise** は例外ブロック内でのみ有効な記述方法です。

 - **try** ブロックで例外が発生すると、**except** ブロック内の一致する例外ハンドラへ処理が移る。例外が処理されたら制御は **end** の文に移る。
 - 例外ブロック内に一致する例外ハンドラが存在しなかった場合には、(端的に言えば) エラーになる。
 - **try** ブロックで例外が発生しなかった場合、制御は **end** の後の文に移る。

**See also:**

 - [例外の再生成 (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.E5.86.8D.E7.94.9F.E6.88.90)
 - [例外のネスト (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.E3.83.8D.E3.82.B9.E3.83.88)
 - [例外チェーン (Owl's perspective)](http://owlsperspective.blogspot.com/2012/02/exception-chain.html)

## グローバル例外処理
VCL アプリケーションや FireMonkey アプリケーションでは**グローバル例外処理**を行うことができます。

```pascal
  ...

  public
    { Public 宣言 }
    procedure AppException(Sender: TObject; E: Exception);

  ...

procedure TForm1.AppException(Sender: TObject; E: Exception);
begin

  // 何かの処理

  Application.ShowException(E); // 例外ダイアログを表示
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnException := AppException;
end;
```

`Application` オブジェクトの `OnException` にグローバル例外処理イベントハンドラを割り当てると、特定の例外を無視したり例外のログを取ったりする事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/fa1e2b40-b508-7546-972e-17a01a296095.png)
VCL アプリケーションの場合、`ApplicationEvents` コンポーネントをフォームに貼って、`OnException` のイベントハンドラにグローバル例外処理を記述する事もできます。

:::note warn
サードパーティのロギング/デバッグツールを使っている場合、グローバル例外処理のためのイベントハンドラが実行されない (置き換わっている) 可能性があります。
:::

**See also:**

 - [Vcl.Forms.TApplication.OnException (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/Vcl.Forms.TApplication.OnException)
 - [FMX.Forms.TApplication.OnException (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/FMX.Forms.TApplication.OnException)
 - [Vcl.Forms.TApplication.ShowException (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/Vcl.Forms.TApplication.ShowException)
 - [FMX.Forms.TApplication.ShowException (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/FMX.Forms.TApplication.ShowException)
 - [Vcl.AppEvnts.TApplicationEvents (DocWiki)](https://docwiki.embarcadero.com/Libraries/ja/Vcl.AppEvnts.TApplicationEvents)

## 例外処理ルーチン
詳細は割愛しますが、例外処理ルーチンには次のようなものがあります。

| ルーチン | 説明 |
|:---|:---|
| [Abort](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Abort) | サイレント例外を生成します。  |
| [AcquireExceptionObject](http://docwiki.embarcadero.com/Libraries/ja/System.AcquireExceptionObject)  | 例外オブジェクトへのアクセスを保持します。 |
| [Assert](http://docwiki.embarcadero.com/Libraries/Sydney/ja/System.Assert) | 論理式が真であるかどうかをテストします。|
| [Error](http://docwiki.embarcadero.com/Libraries/ja/System.Error)  | Error は実行時例外を発生させるのに使用します。 |
| [ExceptAddr](http://docwiki.embarcadero.com/Libraries/ja/System.ExceptAddr) | 現在の例外が発生したアドレスを返します。  |
| [ExceptionErrorMessage](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.ExceptionErrorMessage) | 標準エラーメッセージを形式化します。  |
| [ExceptObject](http://docwiki.embarcadero.com/Libraries/ja/System.ExceptObject) | 現在の例外オブジェクトを返します。  |
| [OutOfMemoryError](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.OutOfMemoryError) | EOutOfMemory の例外を生成します。 |
| [RaiseLastOSError](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.RaiseLastOSError) | 最後に発生した OS またはシステムライブラリエラーの例外を生成します。  |
| [RaiseLastWin32Error](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.RaiseLastWin32Error) | 最後に発生した Win32 エラーの例外を生成します。(非推奨)  |
| [ReleaseExceptionObject](http://docwiki.embarcadero.com/Libraries/ja/System.ReleaseExceptionObject) | AcquireExceptionObject によって取得される例外オブジェクトを解放します。  |
| [ShowException](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.ShowException) |  例外メッセージを表示して例外の物理アドレスを示します。 |
| [Win32Check](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Win32Check) | Windows API 呼び出しの戻り値を調べ，呼び出しが失敗した場合に適切な例外を生成します。 |

**See also:**

 - [System.GetLastError (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.GetLastError)
 - [System.IOResult (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.IOResult)
 - [System.RunError (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.RunError)
 - [System.SysUtils.SysErrorMessage (DocWiki)](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.SysErrorMessage)
 - [入出力（I／O）エラー (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E5%85%A5%E5%87%BA%E5%8A%9B%EF%BC%88I%EF%BC%8FO%EF%BC%89%E3%82%A8%E3%83%A9%E3%83%BC)


# おわりに
ちょっと癖のある Delphi の例外処理についてでした。
**try** 文の含まれるコードをコピペする時にはくれぐれも注意してくださいね。

**See also:**

 - [try - finally - end 構文 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/Others/try-finally.htm)
 - [Delphiの例外処理を理解する (Qiita: @wateryinhare62
)](https://qiita.com/wateryinhare62/items/2377743fa3817af516a1)

[^1]: 0xDEADBEEF みたいな使い方をすればいいと思うよ。
[^2]: サンプルコードからのコピペでこれをやりがちです。サンプルコード単独では完全に正しくても、別のコードと組み合わせると問題が発生する事があります。