プログラミング > Perl > Perl/例外処理

例外処理 編集

この章では、Perlの例外処理について説明します。

コンピュータプログラミングにおいて、例外処理とは、プログラムの実行中に例外(特別な処理を必要とする異常な状態や例外的な状態)が発生した場合に対応する処理のことです。 例外が発生すると通常の実行フローが中断され、あらかじめ登録された例外ハンドラが実行されます。 この処理の詳細は、ハードウェア例外かソフトウェア例外か、またソフトウェア例外の実装方法によって異なります。 例外処理が提供される場合は、特殊なプログラミング言語構造、割り込みなどのハードウェア機構、またはシグナルなどのオペレーティングシステム(OS)プロセス間通信(IPC)機能によって支援されます。

Perlでは、例外処理は「特殊なプログラミング言語構造」を使用して行われます。

v5.34.0では、実験的なtry/catch 構文が追加されましたが、従来のeval {...}を使った方法を先に紹介します。

eval{} と $@ を使った例外処理 編集

Perlの組み込み関数evalは、eval STRINGの形式に加えて、eval { ... }というコードブロックを取る形式があります。 eval { ... } のコードブロック内で、ゼロ除算のような組み込み例外やdie命令によって発生するユーザー由来の例外のいずれかが発生すると、evalは処理を中止し、$@に値を設定して返します。例外が発生しなかった場合、$@はundefとなります。

eval{} と $@ を使った例外処理
use v5.30.0;
use warnings;

sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}

eval { div( 0, 0 ) };
warn $@ if $@;

eval { div( 1, 0 ) };
warn "@{[ join ':', (__PACKAGE__,__FILE__,__LINE__)]}: $@" if $@;
実行結果
main:Main.pl:10: Domain error: div(0, 0) at Main.pl line 6. 
main:Main.pl:14: Illegal division by zero at Main.pl line 7.
6 行目で、分子分母ともゼロの除算をユーザープログラムがドメインエラーとして例外を die 関数を使って投げ、11 行目でハンドリングしています。
7 行目で、処理系がゼロ除算を上げ、14 行目でハンドリングしています。
このように例外を発火したり捕捉したコードは、組込み関数 caller を使って呼出し元のパッケージ名:ファイル名:行番号を表示すると、例外の原因と経路を調べる役にたちます。

try{}catch{}を使った例外処理 編集

v5.34.0 で、実験的な try/catch(変数){} 構文が追加されました。 実験的なので use feature qw(try);が必要で、警告を抑止するにはno warnings "experimental::try";が必要です。

「eval{} と $@ を使った例外処理」と等価なコードを示します。

try{}catch{}を使った例外処理
use v5.34.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";

sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}

try {
    div( 1, 0 )
}
catch ($e) {
    warn "At @{[__FILE__]} line @{[__LINE__]}: $e"
}

try {
    div( 0, 0 )
}
catch ($e) {
    warn "At @{[__FILE__]} line @{[__LINE__]}: $e"
}
実行結果
At main.plx line 16: Illegal division by zero at main.plx line 9.
At main.plx line 23: main:main.plx:20: Domain error: div(0, 0) at main.plx line 8.
8 行目で、分子分母ともゼロの除算をユーザープログラムがドメインエラーとして例外を die 関数を使って投げ、15-17 行目でハンドリングしています。
9 行目で、処理系がゼロ除算を上げ、22-24 行目でハンドリングしています。

モダンな書き方ではありますが、「実験的」という性質上、公開するモジュールや実務での使用は、実験的な性質がなくなるまで控えた方が良いでしょう。

finally{}とdefer{} 編集

v5.36.0 で、try{}catch(変数){}構文にfinally{}節が追加されましたが、やはり実験的な機能です。

[try{}catch(変数){}finallyを使った例外処理]
use v5.36.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";

sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}

foreach my $i ( 0, 1 ) {
    foreach my $j ( 0, 1 ) {
        try {
            say "div($i, $j) --> @{[ div( $i, $j ) ]}"
        }
        catch ($e) {
            warn "At @{[__FILE__]} line @{[__LINE__]}: $e";
            next
        }
        finally {
            say "finally! \$i = $i, \$j = $j"
        }
        say "plain. \$i = $i, \$j = $j"
    }
}
実行結果
At finally.pl line 18: main:finally.pl:15: Domain error: div(0, 0) at finally.pl line 8.
finally! $i = 0, $j = 0
div(0, 1) --> 0
finally! $i = 0, $j = 1
plain. $i = 0, $j = 1
At finally.pl line 18: Illegal division by zero at finally.pl line 9.
finally! $i = 1, $j = 0
div(1, 1) --> 1
finally! $i = 1, $j = 1
plain. $i = 1, $j = 1
前節のプログラムと基本的に同じですが、分子分母をループで回しました。
例外をcatchしたときは next でループの内側の先頭に戻っています。
next の影響で say "plain. \$i = $i, \$j = $j"は例外が出ると実行されません。
しかし、finallyコードブロックのsay "finally! \$i = $i, \$j = $j"は、例外の発生有無にかかわらず実行されます。

finallyコードブロックは、例外の有無にかかわらず必ず実行する処理(例えばファイルハンドラのクローズなど)を想定していますが、同様のことはPerlのv5.36.0から導入されたdeferコードブロックでも実現できます。 deferコードブロックは、スコープが終了する時点で必ず実行され、LIFO順で実行されることが保証されています。また、deferブロック内で例外が発生した場合、それは通常の例外と同様に処理されます。

[defer{}try{}catch(変数){}を使った例外処理]
use v5.36.0;
use warnings;
use feature qw(try);
no warnings "experimental::try";

sub div {
    my ( $x, $y ) = @_;
    die "@{[join ':', caller]}: Domain error: div($x, $y)" if $x == 0 and $y == 0;
    $x / $y;
}

foreach my $i ( 0, 1 ) {
    foreach my $j ( 0, 1 ) {
        use feature 'defer';
        defer {
            say "defer! \$i = $i, \$j = $j"
        }
        try {
            say "div($i, $j) --> @{[ div( $i, $j ) ]}"
        }
        catch ($e) {
            warn "At @{[__FILE__]} line @{[__LINE__]}: $e";
            next
        }
        say "plain. \$i = $i, \$j = $j"
    }
}
実行結果
At defer.pl line 22: main:defer.pl:19: Domain error: div(0, 0) at defer.pl line 8.
defer! $i = 0, $j = 0
div(0, 1) --> 0
plain. $i = 0, $j = 1
defer! $i = 0, $j = 1
At defer.pl line 22: Illegal division by zero at defer.pl line 9.
defer! $i = 1, $j = 0
div(1, 1) --> 1
plain. $i = 1, $j = 1
defer! $i = 1, $j = 1

finally{}は、defer{}よりも例外処理の意識度が高いという意味で、コードの可読性を高めるための構文糖と言えます。 また、defer{}は、try/catchだけでなく、eval{};if($@){...}と組み合わせることもできます。

Carpモジュールのcroak関数を使ったモジュールを超えた例外 編集

Carpモジュールのcroak関数は、現在のサブルーチン(またはメソッド)を呼び出した場所を含めたスタックトレースと共に、エラーメッセージを表示してプログラムを終了します。 これにより、開発者は問題のある箇所を特定し、デバッグを行うことができます。

モジュールの場合、croak関数を使って例外をスローすることができます。 この場合、エラーメッセージにはモジュールの名前が含まれますが、例外が発生した場所はモジュール内部ではなく、モジュールを使用しているスクリプトファイルです。

以下は、例外がモジュールを超えてスローされる例です。

MyModule.pm
# MyModule.pm:

package MyModule;

use Carp qw(croak);

sub do_something {
    my ($x, $y) = @_;

    croak "Domain error" if $x == 0 and $y == 0;
    croak "Division by zero" if $y == 0;
    return $x / $y;
}

1;
main.pl
# main.pl

use MyModule;

my $result = eval { MyModule::do_something(10, 0) };
warn "An exception occurred: $@" if $@;

この例では、main.plスクリプトファイルでevalブロックを使って、MyModule::do_something関数を呼び出します。 この関数は、引数$x$yがともにゼロの場合にcroak関数を使って例外("Domain error")をスローします。 また、引数$yがゼロの場合にもcroak関数を使って例外("Division by zero")をスローします。 evalブロックは、この例外をキャッチして警告メッセージを表示します。

このように、croak関数を使うことで、モジュールを超えた例外処理が行えます。