引用是构造多维结构的唯一手段:Perl 的数组只能装标量,想让"每个接口对应一组耗时记录",就必须把数组的引用当作标量放进去。
想当然地写二维结构会撞墙:
my %api; # 想存 api -> [耗时列表] push %api{login}, 120; # 语法错:哈希值不能直接 push
正确做法是给每个键放一个匿名数组的引用:
my %api; push @{ $api{login} }, 120; # 解引用后 push push @{ $api{login} }, 98; $api{search}[0] = 45; # 箭头语法糖,等价解引用
[ ... ] 创建匿名数组并返回引用,{ ... } 创建匿名哈希引用:
my $record = { api => '/api/order', count => 12, times => [ 210, 88, 340 ], client => { ip => '10.0.0.5', ua => 'curl' }, }; print $record->{times}[1]; # 88 print $record->{client}{ip}; # 10.0.0.5
连续下标之间的箭头可以省略:$record->{times}[1] 与 $record->{times}->[1] 等价。
my %stat; # 结构:小时 -> 接口 -> [总耗时, 次数] while (my $line = <$fh>) { my ($h, $api, $t) = $line =~ /^(\d{2}):\d{2}.*GET (\S+).*?(\d+)ms/; next unless $h; $stat{$h}{$api}[0] += $t; $stat{$h}{$api}[1]++; } for my $h (sort keys %stat) { for my $api (sort keys %{ $stat{$h} }) { my ($sum, $cnt) = @{ $stat{$h}{$api} }; printf "%s时 %s 平均 %d ms\n", $h, $api, $sum / $cnt; } }
%{ $stat{$h} } 是哈希解引用,@{ ... } 是数组解引用——两处大括号里都是"取引用再展开"。

⚠️ 常见坑:拷贝嵌套结构时
$b = $a只拷贝了最外层引用,两份"拷贝"仍然共享内层数据。要真拷贝请用Clone模块或Storable的dclone。
结构一深,肉眼跟踪下标就开始出错,标准做法是让 Data::Dumper 代替眼睛:
use Data::Dumper; $Data::Dumper::Indent = 1; # 紧凑缩进 $Data::Dumper::Sortkeys = 1; # 键排序,输出可比较 my %stat = ( '10' => { '/api/search' => [3300, 3] } ); print Dumper(\%stat); # 输出:$VAR1 = { '10' => { '/api/search' => [ 3300, 3 ] } };
Sortkeys 打开后两次运行的输出完全一致,可以直接 diff 对比"改造前后结构是否变形"。给统计脚本再配一个自检函数,聚合完先验证结构合法性:
sub check_stat { my ($stat) = @_; for my $h (keys %$stat) { for my $api (keys %{ $stat->{$h} }) { my $pair = $stat->{$h}{$api}; die "叶子不是二元数组: $h $api" unless ref $pair eq 'ARRAY' && @$pair == 2; die "次数为零: $h $api" unless $pair->[1] > 0; } } }
ref 返回引用的类型名,运行期就能确认结构形状,比靠约定文档可靠。这类断言函数在第 7 章会升级成正式测试。
第一个是引用计数与循环引用。$a->{b} = $b; $b->{a} = $a; 互指成环后,两个变量都没人用了内存却不释放,长驻进程里日积月累就是泄漏。脚本跑完就退出的场景无所谓,写成守护进程就要用 Scalar::Util 的 weaken 打断环。判断是否中招:use Devel::Cycle 或对比运行前后进程内存。
第二个是自动存活(autovivification):查询一个不存在的深层键,Perl 会沿途把不存在的层自动建成引用——
my %h; if ($h{a}{b}{c}) { ... } # 仅做判断,%h 却已被建成 {a=>{b=>{}}}
这个特性写聚合时是福利(前面 $stat{$h}{$api}[0] += $t 全靠它省去初始化),做存在性检查时是陷阱:本想"看看有没有",结果"查着查着建出来了"。规避办法有两种,用 exists $h{a} && exists $h{a}{b} 链式判断,或者 no autovivification; 关闭该行为。分清"我在累加"还是"我在探测"两种意图,是引用章结业的标准。
顺带一个易被忽略的输出习惯:打印多层结构时给循环起个有业务含义的名字。for my $hour (sort keys %stat) 读起来是"按小时遍历",而 for my $k (sort keys %h) 三个月后连作者自己都要点回去看。嵌套结构一深,变量名就是唯一的文档,命名成本一分钟,解读收益一年。
结构打印还有一个省事入口:print Dumper($stat{10}); 只传某一层的引用,就能单独检查某个小时的数据形状,排查时不至于被整棵树淹没。工坊习惯是"整树看一次定形状,逐层看细节"。
[] 匿名数组、{} 匿名哈希,返回引用,是多维结构的砖块@{ ref } / %{ ref } 解引用,->[...] 和省略箭头是糖use Data::Dumper; print Dumper(\%stat); 直接看清整个结构