2012年2月9日

例外チェーン

Delphi 2009の新機能の一つに例外チェーン、あるいはネストした例外オブジェクトというものがあります。従来の(Delphi 2007およびそれ以前の)例外処理では、発生した例外をtry...except文で捕捉した場合に、そのままendに到達することで例外処理を終了させるか、別の例外オブジェクトを生成して送出することで例外処理をさらに上位に向かって継続するか、例外を再生成("raise;")することで例外処理を継続するか、のいずれかになります。しかし、別の例外を送出する場合は元の例外オブジェクトの持つ情報は消滅してしまいますし、例外を再生成する場合は新たな情報を付け加えることができません。そこでこの例外チェーンという機能を使うことで、元の例外オブジェクトの持つ情報に新たな情報を加えて上位の処理に送出することができます。

例として、あるテキストファイルの1行目に書かれている文字列を10進数とみなして取り込む、という関数を考えてみます。
type
  EFileIsEmpty = class(Exception);

function ReadIntegerValueFromFile(const Path: String): Integer;
var
  Filename: String;
  SL: TStringList;
begin

  Filename := IncludeTrailingPathDelimiter(Path) + 'FOO.TXT';
  SL := TStringList.Create;
  try
    SL.LoadFromFile(Filename);
    if SL.Count = 0 then
    begin
      raise EFileIsEmpty.Create('File is empty.');
    end;

  Result := StrToInt(SL.Strings[0]);

  finally
    SL.Free;
  end;

end;

ここで例外が送出される状況には、1.何らかの理由でファイルを開けない(EFOpenError)、2.ファイルが空(0行)だった(EFileIsEmpty)、3.1行目に10進数に変換できない文字があった(EConvertError)の3つがあります(正確にはリソース不足でTStringList.Createが失敗する状況を含め4つですが、今回は考えないことにします)。それぞれの原因に対応した例外クラスがあるため、呼び出し元ではエラーの理由を例外オブジェクトのクラスの違いで知ることができます。
procedure TForm1.Button1Click(Sender: TObject);
begin

  try
    ReadIntegerValueFromFile('C:\BAR');

  except
    on E: EFOpenError do
    begin
      MessageDlg('ファイルを開けませんでした。' + sLineBreak + E.Message,
                 mtInformation,[mbOk],0);
      Exit;
    end;

    on E: EFileIsEmpty do
    begin
      MessageDlg('ファイルが空でした。' + sLineBreak + E.Message,
                 mtInformation,[mbOk],0);
      Exit;
    end;

    on E: EConvertError do
    begin
      MessageDlg('不正な文字列が入っていました。' + sLineBreak + E.Message,
                 mtInformation,[mbOk],0);
      Exit;
    end;
  end;

end;

ここでエラーメッセージにファイル名を表示したい、ということになったとします。ところが呼出元のレベルではファイルの存在するパスはわかっていますがフルパス名はReadIntegerValueFromFileの内部に隠蔽されてしまっています。そこで
type
  EFileIsEmpty = class(Exception);
  EFileReadError = class(Exception)
  private
    FFilename: String;
  public
    constructor Create(const Msg: string; const AFilename: String);
    property Filename: String read FFilename;
  end;

function ReadIntegerValueFromFile(const Path: String): Integer;
var
  Filename: String;
  SL: TStringList;
begin

  Filename := IncludeTrailingPathDelimiter(Path) + 'FOO.TXT';
  SL := TStringList.Create;
  try
    try
      SL.LoadFromFile(Filename);
      if SL.Count = 0 then
      begin
        raise EFileIsEmpty.Create('File is empty.');
      end;
      Result := StrToInt(SL.Strings[0]);

    except
      raise EFileReadError.Create('Error!',Filename);
    end;

  finally
    SL.Free;
  end;

end;

constructor EFileReadError.Create(const Msg, AFilename: String);
begin

  inherited Create(Msg);

  FFilename := AFilename;

end;

とすることで
procedure TForm1.Button1Click(Sender: TObject);
begin

  try
    ReadIntegerValueFromFile('C:\BAR');

  except
    on E: EFileReadError do
    begin
      MessageDlg(Format('ファイル ''%s'' の読み込みでエラーが発生しました。' + 
                        sLineBreak  + '%s',
                        [E.Filename,E.Message]),
                 mtInformation,[mbOk],0);
      Exit;
    end;
  end;

end;

のようにエラーがあったときにそのファイルのフルパス名を知ることができます。が、エラーの原因はわからなくなってしまいました。そこで例外チェーンの登場です。クラスプロシージャException.RaiseOuterExceptionを使用することで、その例外ハンドラで受け取った例外オブジェクトを消滅させることなく新たな例外を送出することができます。
type
  EFileIsEmpty = class(Exception);
  EFileReadError = class(Exception)
  private
    FFilename: String;
  public
    constructor Create(const Msg: string; const AFilename: String);
    property Filename: String read FFilename;
  end;

function ReadIntegerValueFromFile(const Path: String): Integer;
var
  Filename: String;
  SL: TStringList;
begin

  Filename := IncludeTrailingPathDelimiter(Path) + 'FOO.TXT';
  SL := TStringList.Create;
  try
    try
      SL.LoadFromFile(Filename);
      if SL.Count = 0 then
      begin
        raise EFileIsEmpty.Create('File is empty.');
      end;
      Result := StrToInt(SL.Strings[0]);

    except
      Exception.RaiseOuterException(EFileReadError.Create('Error!',Filename));
    end;

  finally
    SL.Free;
  end;

end;

constructor EFileReadError.Create(const Msg, AFilename: String);
begin

  inherited Create(Msg);

  FFilename := AFilename;

end;

ここで送出される例外オブジェクトはEFileReadErrorのままです。しかし
procedure TForm1.Button1Click(Sender: TObject);
begin

  try
    ReadIntegerValueFromFile('C:\BAR');

  except
    on E: EFileReadError do
    begin
      if E.InnerException is EFOpenError then
      begin
        MessageDlg(Format('ファイル ''%s'' を開けませんでした。' + 
                          sLineBreak + '%s',
                          [E.Filename,E.InnerException.Message]),
                   mtInformation,[mbOk],0);
      end
      else if E.InnerException is EFileIsEmpty then
      begin
        MessageDlg(Format('ファイル ''%s'' が空でした。' + 
                          sLineBreak + '%s',
                          [E.Filename,E.InnerException.Message]),
                   mtInformation,[mbOk],0);
      end
      else if E.InnerException is EConvertError then
      begin
        MessageDlg(Format('ファイル ''%s'' の1行目に不正な文字列が入っていました。' + 
                          sLineBreak + '%s',
                          [E.Filename,E.InnerException.Message]),
                   mtInformation,[mbOk],0);
        Exit;
      end;

      Exit;
    end;
  end;

end;

と例外オブジェクトのInnerExceptionプロパティでException.RaiseOuterExceptionを呼び出した時点での例外オブジェクトを参照することができます。

この例外チェーンに関係するメソッド、プロパティには
  • RaiseOuterException: 例外ブロック内で使用し、新しく生成した例外オブジェクトをパラメータとして呼び出すことでその時点での例外オブジェクトをチェーンした例外を送出するExceptionクラスのクラスプロシージャ。
  • ThrowOuterException: RaiseOuterExceptionと同じ。C++では例外はraiseするものではなくthrowするものなのでこの名前のクラスプロシージャも用意されている。
  • InnerException: 最も近いRaiseOuterExceptionを送出した時点での例外オブジェクトを格納しているプロパティ。通常の例外の送出(raise EXXXX.Create)ではnilとなる。またRaiseOuterExceptionがネストして呼び出されている場合はE.InnerException.InnerException...のようにさかのぼって例外オブジェクトを参照することができる。
  • BaseException: ネストしてRaiseOuterExceptionが呼び出された場合に最初に送出された例外オブジェクトを格納しているプロパティ。通常の例外の送出(raise EXXXX.Create)ではnilとなる。また1段階しかRaiseOuterExceptionが呼び出されていない場合はInnerException=BaseExceptionとなる。
  • ToString例外チェーン上の全ての例外オブジェクトのMessageプロパティをCR+LFで連結したプロパティ。
があります。

元ねたはDELPHI 2009 HANDBOOK

0 件のコメント: