2.4 引用与嵌套数据结构


2.4 引用与嵌套数据结构

引用是构造多维结构的唯一手段: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 模块或 Storabledclone

调试嵌套结构:Dumper 实战与自检函数

结构一深,肉眼跟踪下标就开始出错,标准做法是让 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::Utilweaken 打断环。判断是否中招: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); 直接看清整个结构

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U