例外(Delphi)

提供: RAD Studio
移動先: 案内検索

クラスとオブジェクト:インデックス への移動

このトピックで扱う内容は次のとおりです。

  • 例外および例外処理の概念の概要
  • 例外型の宣言
  • 例外の発生と処理

例外について

例外は、エラーやその他のイベントによってプログラムの正常な実行が中断されたときに発生します。 例外が発生すると制御が例外ハンドラに移り、そこで通常のプログラム ロジックとエラー処理を分離できます。 例外はオブジェクトなので、継承を使って例外を階層構造に分類することができ、また、既存のコードに影響を与えずに新しい例外を導入することができます。 例外では、それが発生した位置から処理される位置まで、エラー メッセージなどの情報を伝えることができます。

アプリケーションで SysUtils ユニットを使用すると、ほとんどの実行時エラーは自動的に例外に変換されます。 メモリ不足、ゼロ除算、一般保護例外など、SysUtils ユニットを使用しなければアプリケーションが終了してしまうような多くのエラーも捕捉し処理できるようになります。

例外を使用する状況

例外は、プログラムを停止させずに、また煩雑な条件文を使用せずに実行時エラーをトラップする明快な方法となります。 例外処理のセマンティクス上の要件により、コード/データのサイズや実行時パフォーマンスの面では不利になります。 ほとんどどのような理由であっても例外を発生させることができ、try...except 文や try...finally 文でラップすることによりほとんどどのようなコード ブロックでも保護することができますが、これらのツールは実際には、特殊な状況に使用するためのものです。

例外処理が適しているのは、エラーの発生する可能性が低いかその評価が困難ではあるものの、エラーの結果、アプリケーションのクラッシュなど最悪の事態になりそうな場合や、エラー条件を if...then 文で検査することが面倒または困難であるような場合、また、オペレーティング システムやソース コードを管理していないルーチンで発生した例外に応答する必要がある場合です。 例外は、ハードウェア、メモリ、I/O、オペレーティング システムのエラーによく使用されます。

多くの場合、エラーを検査するには条件文が最適です。 たとえば、ファイルを開く前にそのファイルが存在するかどうかを確認するとしましょう。 それには、次のような処理をすることになるでしょう。

try
    AssignFile(F, FileName);
    Reset(F);     // raises an EInOutError exception if file is not found
except
    on Exception do ...
end;

しかし、次のようにすることで、例外処理のオーバーヘッドを避けることも可能でしょう。

if FileExists(FileName) then    // returns False if file is not found; raises no exception

begin
    AssignFile(F, FileName);
    Reset(F);
end;

アサーション は、ソース コードの任意の箇所で論理条件を検査するもう 1 つの手段となります。 Assert 文が失敗すると、プログラムは実行時エラーで停止するか、(SysUtils ユニットを使用している場合は)SysUtils.EAssertionFailed 例外を発生させます。 アサーションを使用するのは、発生を予期しない条件を検査する場合に限るべきです。

例外型の宣言

例外型の宣言は他のクラスの場合と同様です。 実は、どのようなクラスのインスタンスでも例外として使用できますが、SysUtils に定義されている SysUtils.Exception クラスから例外を派生させることをお勧めします。

継承を使って例外を同種のグループに分類することができます。 たとえば、SysUtils に含まれている次の宣言では、数値演算エラーを扱う一連の例外型が定義されています。

type
   EMathError = class(Exception);
   EInvalidOp = class(EMathError);
   EZeroDivide = class(EMathError);
   EOverflow = class(EMathError);
   EUnderflow = class(EMathError);

これらの宣言があれば、SysUtils.EInvalidOp、SysUtils.EZeroDivide、SysUtils.Overflow、SysUtils.EUnderflow も処理する単一の例外ハンドラ SysUtils.EMathError を定義できます。

例外クラスでは、エラーに関する追加情報を伝えるフィールド、メソッド、プロパティを定義することもあります。 例:

type EInOutError = class(Exception)
       ErrorCode: Integer;
     end;

例外の発生と処理

例外オブジェクトを発生させるには、raise 文で例外クラスのインスタンスを使用します。 例:

raise EMathError.Create;

一般に、raise 文の形式は次のとおりです。

raise object at address

objectat address はどちらも省略可能です。アドレスを指定する場合は、評価した結果がポインタ型になる式であればどのようなものでもかまいませんが、通常は手続きまたは関数へのポインタです。 例:

raise Exception.Create('Missing parameter') at @MyFunction;

この方法を使用すると、スタック内でエラーの実際の発生時点よりも早く例外を発生させることができます。

例外が発生すると、つまり、raise 文で例外が参照されると、その例外は特別な例外処理ロジックで管理されます。 raise 文は通常どおりには制御を返しません。 代わりに、当該クラスの例外を処理できる最も内側の例外ハンドラに制御を移します。 なお、最も内側のハンドラとは、最後に入ってまだ抜け出ていない try...except ブロックのハンドラです。

たとえば、下記の関数では文字列を整数に変換しますが、結果の値が指定範囲外の場合は、SysUtils.ERangeError 例外を発生させます。

function StrToIntRange(const S: string; Min, Max: Longint): Longint;
begin
    Result := StrToInt(S);   // StrToInt is declared in SysUtils
    if (Result < Min) or (Result > Max) then
       raise ERangeError.CreateFmt('%d is not within the valid range of %d..%d', [Result, Min, Max]);
end;

CreateFmt メソッドが raise 文で呼び出されている点に注意してください。 SysUtils.Exception とその下位クラスには、例外メッセージとコンテキスト ID を作成する代替手段となる特殊なコンストラクタがあります。

発生した例外は、処理された後、自動的に破棄されます。 発生した例外を手動で破棄しようとしないでください。

メモ: ユニットの初期化セクション内で例外を発生させると、意図した結果にならない可能性があります。 通常の例外サポートは <spanSysUtils ユニットで提供されますが、このユニットが初期化された後でなければ、例外サポートを利用できません。 初期化中に例外が発生した場合、初期化されたユニット(<spanSysUtils も含む)はすべて最終化され、その例外が再度発生します。 次に、通常、プログラムに割り込むことで、その例外が捕捉され処理されます。 同様に、ユニットの最終化セクションで例外が発生した場合は、例外の発生時に <spanSysUtils が既に最終化されていれば、意図した結果にならない可能性があります。

try...except 文

例外は try...except 文の中で処理されます。 例:

try
   X := Y/Z;
   except
     on EZeroDivide do HandleZeroDivide;
end;

この文では、YZ で除算しようとしていますが、SysUtils.EZeroDivide 例外が発生した場合は、HandleZeroDivide というルーチンを呼び出します。

try...except 文の構文は次のとおりです:

try statements except exceptionBlock end

ここで、statements は(セミコロンで区切られた)一連の文で、exceptionBlock は次のいずれかです。

  • 別の一連の文
  • 一連の例外ハンドラ(任意で下記の文が続く)
else statements

例外ハンドラの形式は次のとおりです:

on identifier: type do statement

ここでの identifier: は省略可能(指定する場合、identifier は任意の有効な識別子)、type は例外を表すのに使用する型、statement は任意の文です。

try...except 文では、冒頭の statements リスト内の文を実行します。 例外が発生しない場合、例外ブロック(exceptionBlock)は無視され、プログラムの次の部分に制御が移ります。

冒頭の statements リストの実行中に例外が発生した場合は、statements リスト内の raise 文か、statements リストから呼び出された手続きまたは関数により、次のように例外の "処理" が試みられます。

  • 例外に一致するハンドラが例外ブロック内にあれば、その最初のハンドラに制御が移ります。 例外ハンドラが例外に "一致" するのは、例外ハンドラで指定された type がその例外のクラスまたはそのクラスの上位クラスである場合です。
  • 例外に一致するハンドラが見つからない場合は、else 節があれば、その中の文に制御が移ります。
  • 例外ブロックが例外ハンドラのない一連の文にすぎない場合は、そのうちの最初の文に制御が移ります。

以上の条件がどれも満たされない場合は、最後の 1 つ前に入ってまだ抜け出ていない try...except 文の例外ブロックで検索が続行されます。 該当するハンドラ、else 句、文リストのいずれもそこで見つからない場合は、さらにその 1 つ前に入った try...except 文に移って検索が続行され、以下同様の処理が続きます。 最も外側の try...except 文に達し、そこでも例外が処理されない場合は、プログラムが終了します。

例外が処理される場合は、処理が行われる try...except 文を含んだ手続きまたは関数までさかのぼってスタックが追跡され、実行される例外ハンドラ、else 節、文リストのいずれかに制御が移ります。 このプロセスでは、例外が処理される try...except 文に入った後に行われた手続き呼び出しおよび関数呼び出しはすべて破棄されます。 例外オブジェクトは、そのあと、Destroy デストラクタの呼び出しによって自動的に破棄され、try...except 文の次の文に制御が移ります。 なお、ExitBreakContinue のいずれかの標準手続きを呼び出した結果、制御が例外ハンドラから出た場合も、やはり、例外オブジェクトは自動的に破棄されます。

以下の例では、最初の例外ハンドラでゼロ除算例外を処理し、2 番目の例外ハンドラでオーバーフロー例外を処理し、最後の例外ハンドラでその他のすべての数値演算例外を処理しています。 SysUtils.EMathError は、他の 2 つの例外クラスの上位クラスであるため、例外ブロックの最後に記述します。最初に記述すると、他の 2 つのハンドラは絶対に呼び出されません。

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
end;

例外ハンドラでは、例外クラス名の前に識別子を指定できます。 指定した場合は、on...do の次の文を実行している間、この識別子が例外オブジェクトを表すことになります。 この識別子のスコープは、その文に限定されます。 例:

try
  ...
except
  on E: Exception do ErrorDialog(E.Message, E.HelpContext);
end;

例外ブロックに else 節を指定した場合は、例外ブロックの例外ハンドラで処理されない例外はすべて、その else 節で処理されます。 例:

try
  ...
except
  on EZeroDivide do HandleZeroDivide;
  on EOverflow do HandleOverflow;
  on EMathError do HandleMathError;
else
  HandleAllOthers;
end;

この else 節では、SysUtils.EMathError 以外の例外が処理されます。

例外ハンドラをまったく含まず、文のリストだけで構成される例外ブロックでは、すべての例外が処理されます。 例:

try
   ...
except
   HandleException;
end;

この例では、tryexcept の間にある文を実行した結果発生した例外はすべて、HandleException ルーチンで処理されます。

例外の再発生

予約語 raise が例外ブロックで発生した際:

  • プレーンの raise を、現在の例外オブジェクトを再度発生させるために使用します。これにより、例外ハンドラでエラーへの対処を部分的に行ったあと、例外を再度発生させることができます。 手続きや関数で例外の発生後、クリーンアップ処理を行う必要があるものの、その例外を完全には処理できない場合、例外を再発生させると役に立ちます。 例:
 try
   raise Exception.Create('Error Message');
 except
   on E: Exception do
   begin
     E.Message := E.Message +sLineBreak+ 'FATAL';
     raise;
   end;
 end;
  • 一方、raise E を使用して現在の例外オブジェクトを再発生させないでください。代わりに raise を使用してください。そうしないと、アクセス違反が発生する可能性があります。これは誤りです:
 try
   raise Exception.Create('Error Message');
 except
   on E: Exception do
   begin
     E.Message := E.Message +sLineBreak+ 'FATAL';
     raise E;
   end;
 end;

たとえば、次の GetFileList 関数について考えてみましょう。この関数は、TStringList オブジェクトを割り当て、指定された検索パスと一致するファイル名をそのオブジェクトに入力します。

function GetFileList(const Path: string): TStringList;
var
  I: Integer;
  SearchRec: TSearchRec;
begin
  Result := TStringList.Create;
  try
    I := FindFirst(Path, 0, SearchRec);
    while I = 0 do
      begin
          Result.Add(SearchRec.Name);
          I := FindNext(SearchRec);
      end;
  except
      Result.Free;
      raise;
  end;
end;

GetFileList では TStringList オブジェクトを作成したあと、FindFirst 関数と FindNext 関数(SysUtils に定義)を使用して、このオブジェクトを初期化します。 検索パスが無効である、文字列リストに入力するだけの十分なメモリがない、などの理由で文字列リストの初期化に失敗した場合、GetFileList では、新しく割り当てた文字列リストを破棄する必要があります。呼び出し側では、その文字列リストの存在をまだ知らないからです。 このため、この文字列リストの初期化は try...except 文の中で行われます。 例外が発生した場合は、この文の例外ブロックで文字列リストが破棄されたあと、同じ例外が再度発生します。

ネストした例外

例外ハンドラで実行されるコードでは、自分で例外を発生させて処理することができます。 このような例外も例外ハンドラ内で処理される限り、元の例外に影響を及ぼすことはありません。 ただし、例外ハンドラで発生した例外がひとたびそのハンドラを越えて伝播されれば、元の例外は失われます。 このことを次の Tan 関数で説明します。

type
   ETrigError = class(EMathError);
   function Tan(X: Extended): Extended;
   begin
      try
        Result := Sin(X) / Cos(X);
      except
        on EMathError do
        raise ETrigError.Create('Invalid argument to Tan');
      end;
   end;

Tan の実行中に SysUtils.EMathError 例外が発生した場合、例外ハンドラにより ETrigError が発生します。Tan には ETrigError のハンドラが用意されていないため、例外は元の例外ハンドラを越えて伝播され、その結果、SysUtils.EMathError 例外が破棄されます。 呼び出し側には、Tan 関数が ETrigError 例外を発生させたように見えます。

try...finally 文

実行したオペレーションが例外によって中断されたかどうかにかかわらず、そのオペレーションの特定の部分が必ず完了するようにしたい場合があります。 たとえば、あるルーチンでリソースを制御できる場合、そのルーチンが正常に終了したかどうかにかかわらず、そのリソースが解放されることがしばしば重要になります。 このような状況では、try...finally 文を使用できます。

次の例では、ファイルを開いて処理するコードで、たとえ実行中にエラーが発生してもファイルが最後には必ず閉じられるようにする方法を示しています。

Reset(F);
try
   ... // process file F
finally
   CloseFile(F);
end;

try...finally 文の構文は次のとおりです。

try statementList1 finally statementList2 end

ここでの各 statementList は、セミコロンで区切られた一連の文です。 try...finally 文では、statementList1try 句)内の文を実行します。statementList1 が例外を発生させずに終了した場合は、statementList2finally 句)が実行されます。statementList1 の実行中に例外が発生した場合は、statementList2 に制御が移ります。statementList2 の実行がいったん終了すると、例外が再度発生します。ExitBreakContinue のいずれかの手続きへの呼び出しにより、制御が statementList1 から出ると、statementList2 が自動的に実行されます。このように finally 節は、try 節がどのように終了しようと、必ず実行されます。

例外が発生しても、finally 句で処理されなかった場合、その例外は try...finally 文の中から外へ伝播され、try 句ですでに発生した例外はすべて失われます。したがって、finally 節では、他の例外の伝播を妨げないように、ローカルに発生した例外をすべて処理しなければなりません。

標準の例外クラスおよびルーチン

SysUtils ユニットと System ユニットでは、ExceptObject、ExceptAddr、ShowException など、例外を処理するための標準ルーチンがいくつか宣言されています。 SysUtilsSystem などのユニットには、多数の例外クラスも含まれており、それらは(OutlineError を除いて)すべて SysUtils.Exception から派生しています。

SysUtils.Exception クラスには Message および HelpContext というプロパティがあり、これらを使用して、状況依存型オンライン ドキュメント用のエラー説明やコンテキスト ID を渡すことができます。 また、このクラスには、説明やコンテキスト ID を異なる方法で指定できるさまざまなコンストラクタ メソッドも定義されています。

関連項目