Perl 19A:闭包保存查询条件,而不是共享一份临时全局状态
按等级、耗时和消息片段筛日志时,最容易犯的错误是把“当前筛选条件”放进包变量。第一次查询设置 $level = 'ERROR',第二次查询设置 $level = 'WARN',若两个过滤器读的是同一个变量,先创建的过滤器也会跟着变。闭包适合这个场景:构造阶段把已经校验过的条件复制到词法变量,返回的代码引用以后只读取这些词法变量。
LogTool::Course::Filter::make_filter(%conditions) 支持三个条件:level 必须是 INFO、WARN 或 ERROR;min_duration_ms 是非负整数,和记录里的 duration_ms 比较;contains 是消息里的字面字符串,不当作正则。实现先复制 %conditions 里的值,再返回 sub { ... }。返回的闭包接收一条记录哈希引用,满足所有条件时返回真,否则返回假。这里保存的是条件值,不保存调用者传入的哈希容器。
运行 perl examples/perl-language/experiments/course-semantics.pl closure,STDOUT 为:
图:闭包复制条件并保存词法状态
1 | |
第一行来自 make_filter(level => 'ERROR', min_duration_ms => 100, contains => 'disk.full'):错误记录耗时 120 且消息包含 disk.full,返回 1;WARN 记录传给同一个闭包返回 0。第二行来自另一个独立闭包 make_filter(level => 'WARN', contains => 'a.b'):消息 aXb then a.b 返回 1,而 diskXfull 返回 0,说明点号按字面值处理。t/course-semantics.t 还直接构造两个闭包,断言它们互不污染,并断言非法等级 DEBUG 在构造阶段报错。
反例是 make_shared_filter:它把条件写进同一个包变量 %SHARED_CONDITIONS,再返回读取该包变量的匿名子例程。先构造 ERROR 过滤器,再构造 WARN 过滤器后,前一个过滤器读取到的也变成 WARN 条件;实验输出 shared_error_after_warn=0 shared_warn=1。这不是闭包失效,而是闭包没有保存独立词法值,仍在读共享全局状态。
1 | |
[PATTERN] 工厂函数返回代码引用时,要把“构造时校验”和“调用时判断”分清。构造阶段拒绝非法条件,调用阶段只处理单条记录;这样错误能尽早暴露,记录筛选也不会因为外部哈希后续被改而改变。若条件里保存的是数组或哈希引用,仍要决定是否深拷贝;本例的三个条件都是标量值,所以复制到词法变量即可。
练习:若把 contains 传成 a.b,为什么不能用裸正则 /$contains/ 实现?裸正则会把点号解释成任意字符,aXb 也会通过;本课要求的是字面包含,因此使用 index。若将来要支持用户提供正则,应另开参数名,写清允许的语法和回溯边界,不能复用 contains 的字面契约。
参考:Perl 5.38.2 perlsub 的 Persistent Private Variables 说明闭包如何保存词法变量;perlref 的 Code References 说明代码引用调用;perlre 与 perlfunc 的 quotemeta 说明动态模式与字面字符串的区别。源码与断言见 examples/perl-language/lib/LogTool/Course/Filter.pm、examples/perl-language/experiments/course-semantics.pl 和 examples/perl-language/t/course-semantics.t。









