子程序(sub)是 Perl 复用逻辑的唯一单元:参数通过
@_传入,返回值取最后一个表达式的值。掌握"引用传参 + 显式 return",就能把一次性脚本长成工具箱。
sub avg { my ($sum, $n) = @_; # 第一步永远是解包参数 return $n ? $sum / $n : 0; } my $a = avg(430, 5); # 86
参数全部落在数组 @_ 里,my (...) = @_ 按位置解包。传多个参数时不写括号也行:avg 430, 5——但带上括号的可读性更好,我建议团队代码统一带括号。
直接传数组会被拍平进 @_,多个列表会混在一起分不出彼此:
sub top_n { my ($data, $n) = @_; # $data 是数组引用 my @sorted = sort { $b <=> $a } @$data; return @sorted[0 .. $n - 1]; } my @top = top_n(\@durations, 3); # 调用侧取引用传入
哈希同理,传 \%count。这是 Perl 与多数语言差异最大的一点:列表传参不保形,凡是要传数组或哈希,一律取引用。
子程序返回值也受上下文影响,惯用法是用 wantarray 提供双模式:
sub slow_apis { my ($stat, $threshold) = @_; my @hits = grep { $stat->{$_} > $threshold } keys %$stat; return wantarray ? @hits : scalar @hits; } my @list = slow_apis(\%avg_ms, 500); # 列表:慢接口名单 my $count = slow_apis(\%avg_ms, 500); # 标量:慢接口个数
和内建函数行为对齐(localtime 就是这么干的),但要克制——多数子程序一种返回形态就够。
⚠️ 常见坑:省略 return 时返回值是"最后一句表达式的值",
位置参数到第三个就开始记不住顺序,第四个一旦有可选参数就是灾难。惯用法是传哈希引用,调用处自带"参数名":
sub report { my (%opt) = @_; # 解包成哈希 my $top = $opt{top} // 10; # 默认值,defined-or 判断 my $title = $opt{title} // '未命名报表'; my $rows = $opt{rows} or return; # 必填项缺失直接返回 printf "%s(取前 %d 行)\n", $title, $top; print "$_\n" for @$rows[0 .. $top - 1]; } report( title => '慢接口日报', top => 5, rows => \@slow );
调用方读起来像配置文件,参数顺序无关,新增可选参数不破坏旧调用。工坊里凡是超过两个参数的子程序,一律走这个形态。
一次性脚本往往不校验,但工具箱里的子程序要默认"调用方会传错"。最轻量的守卫三件套:
sub top_n { my ($data, $n) = @_; die "top_n: 第一参须为数组引用" unless ref $data eq 'ARRAY'; die "top_n: n 须为正整数" unless defined $n && $n =~ /^\d+$/ && $n > 0; my @sorted = sort { $b <=> $a } @$data; return @sorted[0 .. ($n > @sorted ? $#sorted : $n - 1)]; # n 超长自动截断 }
die 的消息带上子程序名,报错时一眼知道该去哪修。ref ... eq 'ARRAY' 的检查防住了"忘了取引用传了列表"这类高频手滑——错误在入口被拦下,好过在深层循环里炸出一行看不懂的 "Can't use string as ARRAY ref"。
子程序可以取引用(\&name 或匿名 sub { ... }),让调用方注入行为。日志过滤器的通用化就是典型应用:
sub filter_lines { my ($fh, $pred) = @_; # $pred 是判断子引用 my @out; while (my $l = <$fh>) { push @out, $l if $pred->($l); # 调用判断子 } return @out; } my @errors = filter_lines($fh, sub { $_[0] =~ /^ERROR/ }); my @slow = filter_lines($fh, sub { $_[0] =~ /elapsed=(\d+)/ && $1 > 500 });
同一个骨架,两套过滤规则,循环逻辑只写一遍。内置的 sort、grep、map 接收块参数,本质就是同一机制——你在第 2 章已经用回调了,这里只是把使用权拿回自己手里。
子程序调用自己即递归,文本工坊里最常见的场景是遍历目录树:
use File::Find; # 标准库,别手写递归 find(sub { return unless -f $_; # 只关心普通文件 return unless /\.log$/; # 只关心日志 $total += -s $_; # 累计大小 }, $dir);
File::Find 内部就是递归,但把"访问顺序、符号链接、权限错误"这些边角都处理好了。手写递归处理目录树,十个里有九个会栽在符号链接成环上——这是"知道机制,用现成轮子"的典型案例:3.3 节会手写一次递归看清原理,工程代码里仍然回到 File::Find。
@_,第一行 my (...) = @_ 解包@$ref、%$ref、$ref->{k} 在子程序内解引用取值