3.3 作用域与递归


3.3 作用域与递归

作用域决定"这个名字管到哪":my 是词法作用域,从声明处到当前块结束;全局变量整个文件可见。递归则是子程序调用自己,遍历嵌套结构时它是自然选择。

my 与全局变量的分界

my $total = 0; # 文件级词法变量,本文件内可见 sub process { my $line = shift; # 块级,只在本子程序内有效 my $len = length $line; $total += $len; # 读写外层词法变量,合法 }

不开 strict 时,未声明的变量自动成为全局变量,任何子程序都能悄悄改它——这是老 Perl 脚本难维护的头号原因。use strict 把这条路堵死:所有变量必须 my 声明。真正需要跨文件共享的全局量用 our 显式声明,并把数量压到最少。

local 不是"局部变量"

Perl 的 local 是历史遗留命名,它的实际作用是"临时替换全局变量的值,块结束后还原",典型用途是给内建变量临时改值:

{ local $/ = undef; # 临时取消行分隔符 my $all = <$fh>; # 一次读入整个文件 } # 块结束,$/ 自动还原

local 理解成"暂存并还原"而不是"局部声明",就不会误用。

递归:遍历嵌套结构

第 2.4 节的多维结构层数不确定时(比如解析出来的配置树),递归最自然:

sub dump_stat { my ($node, $indent) = @_; $indent //= 0; if (ref $node eq 'HASH') { for my $k (sort keys %$node) { print ' ' x $indent, "$k:\n"; dump_stat($node->{$k}, $indent + 1); } } elsif (ref $node eq 'ARRAY') { print ' ' x $indent, join(', ', @$node), "\n"; } else { print ' ' x $indent, "$node\n"; } } dump_stat(\%stat);

ref 返回引用指向的类型名,递归按类型分流下钻。每层递归的 $node$indent 都是独立的 my 变量,互不干扰——这正是词法作用域给递归的保障。

递归下钻的调用轨迹

递归下钻的调用轨迹

💡 关键直觉:递归深度没有硬限制,但 Perl 默认递归很深时会警告。解析未知层数的配置文件时先加最大深度参数,比事后处理栈溢出体面。

闭包:子程序记住出生地的作用域

词法作用域还有一个高级产物:闭包。子程序定义时能把"定义处可见的 my 变量"打包带走,哪怕定义它的块早已退出:

sub make_counter { my $n = 0; # 每次调用 make_counter 都有新的 $n return sub { ++$n }; # 匿名子程序捕获了这个 $n } my $hits = make_counter(); my $miss = make_counter(); $hits->() for 1 .. 7; # 命中 7 次 $miss->(); $miss->(); # 未命中 2 次 print $hits->(), ' ', $miss->(), "\n"; # 输出 8 3

两个计数器各数各的,互不干扰——它们捕获的是两个独立的 $n。日志工坊里用它做"每行一个计数器"的工厂非常顺手,比在全局哈希里开键位更内聚。判断自己是否真的理解闭包,就看这段代码能不能不运行说出输出。

递归改迭代:深目录的保底方案

递归优雅但吃调用栈,遇到几十万层的病态嵌套(比如被符号链接循环构造出来的"假目录树")会崩。保底方案是用显式栈改写成迭代:

sub walk_iter { my ($root, $cb) = @_; my @stack = ($root); while (my $dir = pop @stack) { opendir my $dh, $dir or next; for my $e (readdir $dh) { next if $e eq '.' or $e eq '..'; my $path = "$dir/$e"; if (-d $path) { push @stack, $path } # 目录压栈,等待后续展开 else { $cb->($path) } # 文件立刻回调 } } } walk_iter($logdir, sub { $total += -s $_[0] if $_[0] =~ /\.log$/ });

递归版里"调用自己"的那一行,变成了"压栈";函数调用栈换成了数组栈,深度不再受限。理解这层转换之后再看 3.2 节推荐的 File::Find,就能明白它为什么既不怕深目录也不怕环——它内部同样做了显式栈与已访问检测。

作用域纪律清单

把本章的作用域知识压成四条可执行的检查项,写代码时对照:

  • 任何变量先 my 再使用(strict 兜底,编译期报错)
  • 子程序内部不修改外层变量;确需累计时,把结果 return 出去由调用方累加
  • 需要跨文件共享的量:our 声明 + 顶部集中放置 + 命名加前缀(如 $g_
  • local 只出现在"临时改内建变量"的场合,且永远包在裸块里

第四条尤其值得抄进团队规范——散落的 local $/ 忘了配对块,全文件后续的读文件行为都会被悄悄改变,这类 bug 的现象("读到的是整个文件而不是一行")与成因(几行之外的 local)相隔遥远,排查极耗时间。

顺带一提闭包与循环变量的经典组合坑:在 foreach 循环里生成多个闭包时,Perl 的循环变量是每个迭代独立的,所以每个闭包捕获的值各不相同;但若用 C 风格 for 加外层 my 变量,多个闭包会共享同一个变量,最终都拿到最后一次的值。两种写法只差一个字符,语义却相反——记住结论:生成闭包一律用 foreach 风格,省去事后排查"为什么所有回调都拿到最后一个值"的下午。

本节要点回顾

  • my 是词法作用域,块内声明块内生效,递归各层互不干扰
  • strict 强制声明,全局变量的静默共享是被它消灭的老 bug 源
  • local 是"暂存还原",改内建变量专用,不是局部声明
  • ref 判断节点类型再递归下钻,是遍历嵌套结构的标准姿势
  • 递归要设深度上限,防止病态数据把进程拖死

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