第 14 章:错误处理与异常
第 14 章:错误处理与异常
“未处理的错误是最危险的错误”
健壮的错误处理是生产代码的必要条件。本章介绍 Perl 的错误处理机制。
14.1 die 和 warn
use strict;
use warnings;
# die — 抛出致命错误(退出程序)
die "发生了错误!\n"; # 带换行:直接打印消息
die "发生了错误"; # 不带换行:自动附加文件名和行号
# warn — 发出警告(继续运行)
warn "这是一个警告!\n";
# $! — 系统错误信息
open my $fh, '<', 'nonexistent.txt'
or die "无法打开文件: $!\n";
错误信息格式
# 带换行:打印消息并退出
die "错误消息\n";
# 输出: 错误消息
# 不带换行:附加 at file.pl line N
die "错误消息";
# 输出: 错误消息 at script.pl line 10.
# 使用 __FILE__ 和 __LINE__
die sprintf "错误 at %s line %d\n", __FILE__, __LINE__;
14.2 eval — 捕获错误
eval 块(推荐)
# eval 块捕获 die 的错误
eval {
open my $fh, '<', 'data.txt' or die "文件不存在: $!";
my $content = do { local $/; <$fh> };
close $fh;
# ... 更多操作
};
if ($@) {
warn "捕获到错误: $@\n";
} else {
print "操作成功\n";
}
# 推荐写法(更清晰)
my $result = eval {
# ... 可能失败的操作
return 42;
};
if (defined $result) {
print "结果: $result\n";
} elsif ($@) {
warn "错误: $@\n";
}
eval 字符串(不推荐)
# eval 字符串 — 执行字符串中的代码(危险!)
my $result = eval "2 + 2"; # 4
# 不推荐用于错误处理
# 安全风险:如果字符串来自用户输入
eval 的返回值
# eval 块返回最后一个表达式的值
my $val = eval { 1 / 0 };
# $val = undef, $@ = "Illegal division by zero"
if ($@) {
warn "除零错误: $@";
}
14.3 Try::Tiny — 现代异常处理
Try::Tiny 是推荐的现代错误处理方式,解决了 eval 的各种陷阱:
use Try::Tiny;
try {
open my $fh, '<', 'data.txt' or die "无法打开: $!";
my $data = do { local $/; <$fh> };
close $fh;
print "成功读取\n";
} catch {
warn "捕获错误: $_\n"; # $_ 包含错误消息
} finally {
print "无论如何都会执行\n"; # 清理代码
};
Try::Tiny vs eval
| 特性 | eval 块 | Try::Tiny |
|---|---|---|
| 语法 | eval { } / $@ | try { } / catch { } |
| $@ 污染 | 可能 | 不会 |
| $@ 竞态 | 有 | 无 |
| finally | 不支持 | 支持 |
| 性能 | 略快 | 略慢 |
| 依赖 | 内置 | 需安装 |
# eval 的陷阱:
eval { die "error" };
# 如果在 die 和检查 $@ 之间有 DESTROY 方法修改了 $@...
# $@ 可能变成 undef!
# Try::Tiny 没有这个问题
try { die "error" } catch { print "捕获: $_" };
14.4 autodie — 自动错误处理
autodie 让系统调用在失败时自动 die:
use autodie;
# 不需要 "or die" 了
open my $fh, '<', 'data.txt'; # 失败时自动 die
my @lines = <$fh>;
close $fh;
# 自动处理的函数包括:
# open, close, read, write, print
# mkdir, rmdir, unlink, rename
# chdir, flock, binmode
# system, exec
# ...
autodie 的作用域
# 文件级 autodie
use autodie;
# 块级 autodie
{
no autodie; # 此块内禁用 autodie
open my $fh, '<', 'file.txt'; # 需要手动检查
}
# 只对特定函数启用
use autodie qw(open close);
autodie vs 手动 or die
# 手动检查(传统写法)
open my $fh, '<', 'file.txt' or die "打开失败: $!\n";
print $fh "data" or die "写入失败: $!\n";
close $fh or die "关闭失败: $!\n";
# autodie(推荐写法)
use autodie;
open my $fh, '<', 'file.txt';
print $fh "data";
close $fh;
14.5 自定义异常类
# 基础异常类
package MyApp::Error;
use overload
'""' => sub { $_[0]->message },
bool => sub { 1 },
fallback => 1;
sub new {
my ($class, %args) = @_;
return bless {
message => $args{message} // "Unknown error",
code => $args{code} // 500,
file => $args{file} // caller(0),
line => $args{line} // caller(0),
}, $class;
}
sub message { $_[0]->{message} }
sub code { $_[0]->{code} }
# 具体异常类
package MyApp::Error::NotFound;
use parent 'MyApp::Error';
sub new {
my ($class, $resource) = @_;
return $class->SUPER::new(
message => "资源未找到: $resource",
code => 404,
);
}
package MyApp::Error::Permission;
use parent 'MyApp::Error';
sub new {
my ($class, $action) = @_;
return $class->SUPER::new(
message => "权限不足: $action",
code => 403,
);
}
使用自定义异常
use Try::Tiny;
use MyApp::Error;
use MyApp::Error::NotFound;
sub find_user {
my ($id) = @_;
# 模拟查询
die MyApp::Error::NotFound->new("user:$id") unless $id > 0;
return { id => $id, name => "User $id" };
}
try {
my $user = find_user(0);
} catch {
if ($_->isa('MyApp::Error::NotFound')) {
warn "404: " . $_->message . "\n";
} elsif ($_->isa('MyApp::Error')) {
warn "Error " . $_->code . ": " . $_->message . "\n";
} else {
warn "未知错误: $_\n";
}
};
14.6 异常处理模式
哨兵模式
sub process_file {
my ($file) = @_;
open my $fh, '<', $file or return undef;
while (<$fh>) {
chomp;
# 处理...
}
close $fh;
return 1; # 成功
}
my $ok = process_file("data.txt");
warn "处理失败" unless defined $ok;
异常模式(推荐)
use Try::Tiny;
sub process_file {
my ($file) = @_;
open my $fh, '<', $file or die "打开失败: $!";
while (<$fh>) {
chomp;
die "格式错误" unless /^valid/;
# 处理...
}
close $fh;
}
try {
process_file("data.txt");
print "处理成功\n";
} catch {
warn "错误: $_\n";
};
14.7 业务场景:API 错误处理
#!/usr/bin/env perl
use strict;
use warnings;
use Try::Tiny;
use JSON::XS;
use HTTP::Tiny;
my $client = HTTP::Tiny->new(timeout => 10);
sub api_request {
my ($method, $url, $data) = @_;
my $response;
try {
$response = $client->request($method, $url, {
headers => { 'Content-Type' => 'application/json' },
($data ? (content => encode_json($data)) : ()),
});
} catch {
die "网络错误: $_\n";
};
# 检查 HTTP 状态码
die "HTTP $response->{status}: $response->{reason}\n"
unless $response->{success};
# 解析 JSON
my $result;
try {
$result = decode_json($response->{content});
} catch {
die "JSON 解析失败: $_\n";
};
return $result;
}
# 使用
my $users = try {
api_request('GET', 'https://jsonplaceholder.typicode.com/users');
} catch {
warn "请求失败: $_";
return [];
};
for my $user (@$users) {
printf "%-30s %s\n", $user->{name}, $user->{email};
}
本章小结
| 要点 | 内容 |
|---|---|
die | 抛出致命错误 |
warn | 发出警告(不退出) |
eval { } | 捕获错误的旧方式 |
Try::Tiny | 推荐的现代错误处理 |
autodie | 自动让系统调用失败时报错 |
$@ | eval 错误信息 |
$! | 系统错误信息 |
| 自定义异常 | 使用类层次结构 |
练习
- 使用 eval 和 Try::Tiny 分别实现除零错误处理
- 创建一个自定义异常类层次(Base → IOError → NotFound)
- 使用 autodie 重写一个文件操作脚本
- 实现一个带重试机制的函数(最多重试 3 次)
- 编写一个 API 客户端,使用异常类处理各种错误