木曜日, 11月 30, 2006

[Perl]例外をオブジェクトとして扱う

例外をオブジェクトとして扱うと以下のような利点があります。

  1. エラーの種類を正規表現ではなくオブジェクトの型で分けられる
  2. オブジェクトに複雑な情報を付加できる
例外オブジェクトを扱うモジュールとしてErrorモジュールやException::Classが有りますが、そういったモジュールを使わなくてもPerl5.005以降ではdieにblessされたリファレンスが渡せるため、普通に例外オブジェクトが使えます。

たとえばこんな感じでエラークラスを定義しておくと
use strict;
package MyException;
use base 'Class::Accessor';
use overload qw{""} => \&as_string;
use Carp;
__PACKAGE__->mk_accessors(qw(description stacktrace associated));
sub new {
my $class = shift;
my $desc = shift;
my @args = @_;
unless ($desc =~ /\n\z/) {
my $line = (caller($Carp::CarpLevel))[2];
$desc .= " at line $line.";
}
my $self = $class->SUPER::new({ description => $desc, @args });

$self->stacktrace("$class". Carp::longmess() );
return $self;
}
sub throw {
my $proto = shift;
my $desc = shift;
die $proto if ref $proto;
local $Carp::CarpLevel = $Carp::CarpLevel + 1;

my $self = $proto->new($desc, @_);
die $self;
}

sub as_string {
my $self = shift;
return
qq{$self->{description}
------------------- stacktrace ---------------------
$self->{stacktrace}
};
}

package HogeException;
our @ISA = qw(MyException);

package SystemException;
our @ISA = qw(MyException);
以下のようにisaで判断して関連するオブジェクトを取り出したりできます。


#!/usr/local/bin/perl
use strict;
package App;
use MyException;
use Data::Dumper;

sub hoge_func {
my $obj = { hoge => "@_" };
HogeException->throw('normal throw', associated => $obj);
}

sub run {
my $class = shift;
eval {
$class->hoge_func('hoge_func', @_);
};
if (my $err = $@) {
print "#####\n$err#####\n"; # MyException->as_stringが呼ばれる
if (UNIVERSAL::isa($err,'HogeException')) {
# オブジェクトの型で処理を分岐。関連付けられている情報を取り出す
print Dumper($err->associated);
} else {
die $err;
}
}
};

package main;
App->run('hoge');

実行結果

#####
normal throw at line 9.
------------------- stacktrace ---------------------
HogeException at die2.pl line 9
App::hoge_func('App', 'hoge_func', 'hoge') called at die2.pl line 15
eval {...} called at die2.pl line 14
App::run('App', 'hoge') called at die2.pl line 29

#####
$VAR1 = {
'hoge' => 'App hoge_func hoge'
};

ただこのままだとモジュール内で普通にdieされているものはスタックトレースがでなかったりと不便なので、シグナルハンドラを使ってもうひと頑張りしてみます
local $SIG{__DIE__} = sub {
if (ref $_[0] && $_[0]->isa('MyException') ) {
$_[0]->throw;
}
else {
# スタックトレースにここの呼び出しを含めないようにする
local $Carp::CarpLevel = $Carp::CarpLevel + 1;
SystemException->throw( join " ", @_ )
};
};

これを設定しておくと普通にdieが使われている場所でも例外オブジェクトに変換されてスタックトレースが取れるようになります。
スタックトレースだけを考えたら
local $SIG{__DIE__} = \&Carp::confess

で充分だと思います。

0 件のコメント: