9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Delphi の例外処理

Last updated at Posted at 2021-06-15

はじめに

Delphi の例外処理に関する記事です。

See also:

例外処理

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

例外を処理するには次の 2 つの try 文を使います。

  • try…except…end
  • try…finally…end

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

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

例外型の派生

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

type
  Exception = class(TObject);

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

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

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

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

See also:

例外の生成

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

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

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

raise Exception.Create('Error');

のように書きます。

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

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

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

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

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

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

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

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

See also:

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

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

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

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

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

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

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

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

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

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 だと例外が発生しないため、例外ブロック内が実行されません。

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

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

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

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

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

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

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

try…finally 文

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

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

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

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

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

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

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 ブロックは実行されます。

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

Flg1 が False で Flg2 が True だと、Exit により TEST2() を抜けるため、それ以降が実行されませんが、finally ブロックは実行されます

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

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

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 文

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

procedure TEST;
begin
  //  任意の処理

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

  // 任意の処理
end;

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

procedure TEST;
begin
  //  任意の処理

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

  // 任意の処理

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

  // 任意の処理
end;

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

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

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

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

try…except 文 (例外ハンドラあり)

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

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

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

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.

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

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

例外ハンドラ

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

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

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

on E: Exception do

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

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

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

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

See also:

グローバル例外処理

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

  ...

  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
VCL アプリケーションの場合、ApplicationEvents コンポーネントをフォームに貼って、OnException のイベントハンドラにグローバル例外処理を記述する事もできます。

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

See also:

例外処理ルーチン

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

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

See also:

おわりに

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

See also:

  1. 0xDEADBEEF みたいな使い方をすればいいと思うよ。

  2. サンプルコードからのコピペでこれをやりがちです。サンプルコード単独では完全に正しくても、別のコードと組み合わせると問題が発生する事があります。

9
5
3

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
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?