例外 (Object Pascal)

提供: Appmethod Topics
移動先: 案内検索

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


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

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

例外について

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

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

例外の使用時期

例外は、プログラムを停止せずに、煩雑な条件文を使用しないで実行時エラーをトラップできる優れた方法を提供します。例外処理のセマンティクスが課す要件により、コードやデータのサイズおよび実行時パフォーマンスには制約があります。例外はどのような問題に対しても生成可能であり、try...except または try...finally 文に含めることによってほとんどすべてのコードのブロックを保護することができますが、実際には特殊な場面で使用されます。

例外処理は、エラーの発生率が低いか、または発生率の査定が困難であるにもかかわらず、そのエラーがアプリケーションのクラッシュなど致命的な障害を引き起こすような場合や、エラー条件を if...then 文でテストすることが複雑または困難であるような場合に最適です。また、オペレーティングシステムによる例外や、制御できないソースコードで書かれたルーチンによって生成された例外に対して応答しなければならない場合にも適しています。一般に、例外はハードウェア、メモリ、I/O、およびオペレーティングシステムのエラーに使用されます。

多くの場合、条件文を使用するのがエラーをテストする最良の方法です。たとえば、ファイルを開く前に、そのファイルが存在することを確認するとします。次のように確認できます。

try
    AssignFile(F, FileName);
    Reset(F);     // ファイルが見つからない場合は EInOutError 例外を生成する
except
    on Exception do ...
end;

しかし、次を使用することにより、例外処理のオーバーヘッドを回避することも可能です:

if FileExists(FileName) then    // ファイルが見つからない場合は False を返し、例外は生成しない

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

Assertions は、ソース コードの任意の場所で、ブール論理条件をテストする別の手段を提供します。Assert 文が失敗すると、プログラムは実行時エラーで停止するか、(SysUtils ユニットを使用している場合には)、SysUtils.EAssertionFailed 例外を発生させます。Assertions は、発生を予期しない条件をテストする場合にのみ、使用されるべきです。

例外型の宣言

例外型は、他のクラスとまったく同様に選択されます。実際には、どのようなクラスのインスタンスでも例外オブジェクトとして使用できますが、例外は SysUtils で定義されている SysUtils.Exception から派生させるようにすることをお勧めします。

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

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

これら宣言があることにより、SysUtils.EMathError 例外ハンドラ 1 つを定義し、その中で、SysUtils.EInvalidOp、SysUtils.EZeroDivide、SysUtils.Overflow、SysUtils.EUnderflow も処理させることができます。

例外クラスの中で、エラーに関する付加情報を提供するフィールド、メソッド、プロパティを定義することもあります。以下に例を示します。

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

例外の生成と処理

例外オブジェクトを生成するには、raise 文で例外クラスのインスタンスを使用します。以下に例を示します。

raise EMathError.Create;

通常、raise 文の形式は次のようになります。

raise object at address

ここでの object および at address は、共に任意です。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 は、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 を作成する別の方法を提供します。

処理の後、発生した例外は自動的に破壊されます。このため、発生した例外を手動では破壊しないでください。

メモ: ユニットの初期化セクション内で例外を生じさせると、意図した結果を得られない場合があります。通常の例外サポートは、SysUtils ユニットで定義されていますが、このユニットが初期化された後でないと、例外サポートが有効になりません。例外が初期化中に起こった場合、初期化されたすべてのユニット(SysUtils も含む)は、ファイナライズされ、その例外は改めて生成されます。その後、その例外は、通常プログラムを中断することにより、捕捉および処理されます。同様に、ユニットのファイナライズ セクションで例外が発生した場合には、SysUtils がすでにファイナライズされていなければ、意図した結果が得られないことがあります。

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: は任意で(付加する場合は有効な識別子を指定する)、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 節では、System.SysUtils.EMathError 以外の例外が処理されます。

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

try
   ...
except
   HandleException;
end;

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

例外の再生成

オブジェクト参照を指定せずに予約語 raise だけを単独で例外ブロック内に記述すると、その例外ブロックで処理される例外を生成することができます。これにより、例外ハンドラでエラーへの対処を限定的に実行した後、例外を再生成することができます。例外の発生後、手続きまたは関数でクリーンアップ処理を行わなければならないが、その手続きまたは関数では例外を完全には処理できないといった場合に、例外を再生成すると便利です。

たとえば、次に示す 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;

GetFileListTStringList オブジェクトを作成した後、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
   ... // ファイル 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、および他のユニットには、数多くの例外クラスが含まれており、それらはすべて SysUtils.Exception から派生しています(OutlineError を除く)。

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

関連項目