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
関数を使うことで、モジュールを超えた例外処理が行えます。