我正在做一些代码高尔夫,并决定尝试'聪明'并声明一个子程序,它将拥有它已经在范围内所需的变量,以避免额外的代码必须传入参数:
#! perl
use strict;
use warnings;
for my $i(0..1) {
my @aTest = (1);
sub foo {
# first time round - @aTest is (1)
# second time round - @aTest is (1,2)
push @aTest, 2;
# first time round - @aTest is (1,2)
# second time round - @aTest is (1,2,2)
my $unused = 0;
}
foo();
}
foo
看到变量@aTest
,并且在我第一次输入(1)
之前,它的预期值为foo
,然后才将2
推送到@aTest
数组也是如此。 (1,2)
现在看起来像foo
。
到目前为止一切顺利。
然后我们退出@aTest
,并开始第二轮for循环。 (1)
再次重新分配给foo
。
我们第二次输入@aTest
,但foo
保留了先前在(1,2)
中所拥有的值(即2
),然后推送另一个(1,2,2)
}成为@aTest
。
这里发生了什么?
我认为由于foo
属于同一范围,因此它指的是{{1}}内部和外部的相同变量。那么``foo内部如何保留它的旧价值呢?
答案 0 :(得分:3)
在解释之前,这里要学习的教训是不要将命名的 subs放在循环或其他子中。 (匿名潜艇很好。)
简化示例:
for (1..3) {
my $var = $_;
sub foo { say $var; }
foo();
}
输出:
1
1
1
for (1..3) {
my $var = $_;
sub foo { say $var; }
foo();
}
相当于
for (1..3) {
my $var = $_;
BEGIN { *foo = sub { say $var; }; }
foo();
}
由于这更清楚,$var
在编译时被捕获。
但是$var
如何在编译时存在?它不是每次循环都被创建的吗?不可以。你需要意识到的是my
在编译时创建变量 。在运行时,它只是在堆栈上放置一个指令,当从堆栈中弹出指令时,该指令会导致变量被清除。这允许在循环的每次传递中使用相同的$var
,这很好,因为分配和解除分配标量相当昂贵。
现在,如果我说的是什么,那么以下内容将打印3
三次,因为@a
将包含对同一变量的三个引用:
my @a;
for (1..3) {
my $x = $_;
push @a, \$x;
}
say $$_ for @a;
但是,它按预期打印1
,2
和3
。还记得指令my
放在堆栈上吗?它比我提到的更聪明。如果变量包含一个对象,或者该变量仍被其中的文件/子被引用(例如,当它被捕获时),则将其替换为新变量而不是被清除。
这意味着什么,
First pass Second pass Third pass
for (1..3) { -------------------- -------------------- --------------------
my $var = $_; Orig $var assigned 1 New $var assigned 2 Same $var assigned 3
say \$var; SCALAR(0x996da8) != SCALAR(0x959b78) == SCALAR(0x959b78)
sub foo { say $var; } Prints captured $var Prints captured $var Prints captured $var
foo();
} $var is replaced $var is cleared $var is cleared
because REFCNT=2 because REFCNT=1 because REFCNT=1
相反,请尝试
for (1..3) {
my $var = $_;
my $foo = sub { say $var; }; # Captures $var at runtime.
$foo->();
}
输出:
1
2
3
答案 1 :(得分:2)
我在irc.perl.org上将这个问题发布到了#p5p,并得到了一个有趣的交流,解释了发生了什么。
[15:13:35]< simbabque>有谁可以解释Perl scoping - accessing variables in subroutine中发生了什么?我试图读取该程序的B :: Concise的输出,但我的理解还不够强大。我们看到的行为会有错误吗? [15:15:35]< rjbs> & foo是对@aTest的封闭 [15:16:33]< haarg>但只有第一个@aTest,因为& foo在编译时创建一次 [15:17:01]< rjbs>右。
[15:18:09]< rjbs>根据我的经验,在包装或裸块之外的任何其他内部声明一个命名子,请求将来的心痛 [15:18:23]< alh>如果你有我的$ foo = sub {}; $ foo->();它会像预期的那样工作 [15:18:34]< alh>或者在较新的perls中,使用qw(lexical_subs);我的子foo {} foo()也会工作 [15:18:35]< simbabque>好吧,那个家伙说他在打高尔夫球的时候遇到过 [15:19:13]< simbabque> alh:有两个我也期望它是一个闭包,但因为sub foo {}在编译时完成了我很困惑 [15:19:38]< rjbs>关于绑定的词汇子句“做正确的事” [15:19:45]< alh>仍然关闭,每次都重新评估 [15:20:56]< haarg>他们共享操作树,但是绑定到不同的变量
[15:23:29]< simbabque>我在函数内部和外部添加了say "foo: ".\@aTest;
和say "out ".\@aTest;
。这也很奇怪。第一轮都是相同的,然后foo保持相同的一个,循环中的一个得到一个新的地址,并在后续的迭代中保持它 [15:26:48]< alh>当然,第一次通过循环,在sub中关闭的变量与循环看到的变量相同 [15:27:01]< alh>然后我们循环,得到一个全新的,但子没有(因为它没有再次编译)
[15:27:46]< simbabque> alh:这是有道理的,但为什么所有后续迭代都重用相同的变量但在循环中重置它?这只是Perl对其记忆的聪明才智吗? [15:28:48]< alh>不,你的子已关闭一个变量并保持对它的引用 - 所以它永远不会消失,它的值在子调用中保持不变 [15:29:07]< alh>子中没有“我的@aTest”来“重置”变量
[15:29:19]< alh>所以它只是保持其价值 - 这就是关闭点 [15:29:21]< simbabque> alh:我的意思是循环中的那个,而不是副子 [15:29:41]< alh>循环中的那个让你感到惊讶的是什么? [15:29:52]< simbabque> out:ARRAY(0x2356300)foo:ARRAY(0x2356300)out:ARRAY(0x20978b0)foo:ARRAY(0x2356300)out:ARRAY(0x20978b0)foo:ARRAY(0x2356300)out:ARRAY(0x20978b0)foo:ARRAY(0x2356300)
[15:30:13]< simbabque>第一轮他们是一样的 [15:30:33]< simbabque>然后out(这是我在循环中的@aTest)获得一个新地址,但是之后的那一轮保持不变 [15:30:49]< haarg>地址保持不变
[15:30:54]< haarg>这并不意味着它是相同的变量 [15:31:02]< haarg>更多“再次获得相同的地址”
[15:31:12]< simbabque>所以Perl只是重用那个地址?
我所指的输出来自于此修改:
for my $i(0..3) {
my @aTest = (1);
sub foo {
push @aTest, 2;
my $unused = 0;
print " foo: ".\@aTest;
}
print " out: ".\@aTest;
foo();
}
所以它实际上是在编译时在@aTest
上构建一个闭包。在第一次迭代中,循环中的变量与sub中的变量相同。在所有后续迭代中,它在循环中创建一个新变量,因此我们每次都会看到一个新的(1)
。但是sub不再被编译,因此@aTest
变量保持不变并且增长。
答案 2 :(得分:1)
您嵌套子程序,使其不会按预期方式运行。
子程序在编译时存储在全局命名空间中。在你的例子中b();是main :: b();的简写。要限制函数对范围的可见性,需要将匿名子例程分配给变量。
命名子程序和匿名子程序都可以形成闭包,但由于命名子程序只有嵌套它们才会被编译一次,因此它们的行为并不像人们期望的那样。
请阅读其余的here。