同一行日志里有服务名 api,解析函数里也可能临时使用 service。如果读取、解析、筛选都读写同一个包变量,后一次处理就可能改变前一次的状态。my 给每个词法作用域一个局部名字,use strict; use warnings; 则让未声明名字早暴露,并显示可疑转换;它们并不自动保证输入符合日志格式。

examples/perl-language/experiments/01-scope.pl 在外层声明 $service = 'api',内层再声明一个 $service = 'worker'。运行 perl examples/perl-language/experiments/01-scope.pl,STDOUT 为 inside=worker、outside=api 两行,STDERR 空,退出码 0。输出证明了这两个声明的遮蔽范围;它没有证明任何输入已经通过字段校验。正式工具的 LogTool::read_records 让文件句柄、行号、@fields 和输出记录局限在一次调用中,返回数组引用,不把当前行留在包级变量里。

反例运行 perl -we 'use strict; my $service = "api"; print $missing;':编译失败,STDOUT 空,STDERR 以 Global symbol "$missing" requires explicit package name 开始,退出码 255。删去 strict 虽可让拼错的变量名进入程序,却没有修复拼写错误;这也是不靠“先跑起来”评估正确性的原因。

warnings 不是拒绝策略。perl -we 'use strict; use warnings; my $value = undef; print $value + 1;' 仍在 STDOUT 打印 1,同时 STDERR 出现 Use of uninitialized value,退出码 0。在 CLI 的字段边界必须自己决定拒绝条件:invalid.tsv 使用未知等级 DEBUG,import STDERR 是 line 1: invalid record、STDOUT 空、退出码 2。警告让问题可见,die/退出契约才决定调用者是否把结果当成功。两个反例和正例由 t/01-04-semantics.t 与 t/00-contract.t 覆盖。

补充实验 perl examples/perl-language/experiments/course-semantics.pl scope 把 my、our、local 放在同一段输出里:my=LEXICAL our=INFO、local_inside=WARN、local_after=INFO。my 只给当前词法块一个名字;our 暴露的是包变量的词法别名;local 临时保存并恢复包变量的值,恢复边界随动态调用栈走。t/course-semantics.t 断言这三行,防止把 local 写成“另一种 my”,也防止把 our 当作全局变量的无限制裸访问。

这个差别会进入解析器设计:行号、字段数组和临时错误变量适合 my;模块版本号和显式导出列表可以是包变量;只在一段输出格式化中临时改变分隔符、警告策略或全局句柄状态时才考虑 local。三者都不是数据校验器,校验仍要在字段数、等级枚举和日期语义处显式完成。官方依据是 Perl 5.38.2 perlsub 的 Private Variables via my()、Temporary Values via local(),以及 perlfunc 的 our 条目。

练习:若 my $line_number 放到整个脚本顶层而不是 read_records 内,连续调用同一解析函数时行号应该从几开始?每个文件都应从 1 起算;因此行号属于一次读入调用,不属于整个进程的全局生命期。实验脚本和计数、环境、失败诊断见 examples/perl-language/evidence/01-04.md。

参考:经校验的 Perl 5.38.2 发行包 pod/perlintro.pod(Safety net、Variable scoping)、pod/perlsub.pod(Private Variables via my())、pod/perllexwarn.pod;上游发行目录。