Perl作用域 - 子程序中访问变量

3

我正在进行一些代码高尔夫比赛,并决定尝试聪明地声明一个子程序,让它已经在范围内拥有所需的变量,以避免传递参数时额外的代码:

#! 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,第一次进入foo时,它的值为(1),正如预期的那样,在将2推送到数组之前。现在@aTest看起来像(1,2)

到目前为止还不错。

然后我们退出foo,并开始进行for循环的第二轮。 @aTest再次被重新赋值为(1)

我们第二次进入foo,但是@aTest保留了它在foo中先前的值(即(1,2)),然后又推送了另一个2,变成了(1,2,2)

这里发生了什么?

我以为由于@aTest在同一范围内,它将同时引用foo内部和外部的同一变量。那么它在foo内部如何保留其旧值呢?


使用B::Deparse并没有显示任何内容。你看过B::Concise的输出吗? - undefined
我可以在Perl 5.20.1上确认这一点。有趣的是,在调用foo()之前直接输出@aTest时会发生什么。它总是( 1 )。就好像在子程序中有一个新的@aTest词法变量,但从未声明过。为什么这不会失败呢? - undefined
如果你在for循环之外声明my @aTest,它就能正常工作。 - undefined
我在irc.perl.org的#p5p频道上发布了这个问题。显然,它在编译时创建了一个闭包,在第一次迭代时它们是相同的,但之后它们似乎分离了。 - undefined
相关链接:http://stackoverflow.com/questions/8839005/perl-what-scopes-closures-environments-are-producing-this-behaviour/8839552#8839552 - undefined
请将以下与编程相关的内容从英文翻译成中文。只返回翻译后的文本:https://dev59.com/f2855IYBdhLWcg3w_5cQ#4048277, https://dev59.com/hnjZa4cB1Zd3GeqPaSJw - undefined
3个回答

4
在解释之前,需要学习的一课是不要在循环或其他子程序中放置命名子程序(匿名子程序可以)。
简单例子:
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;

然而,它像预期的那样打印出了123。记住指令my放置在堆栈上的位置?它比我之前提到的更加智能。如果变量包含一个对象,或者如果变量仍然被除文件/sub之外的其他东西引用(例如,当它被捕获时),那么它将被替换为一个新的变量,而不是被清除。

这意味着什么,

                         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

2

我在irc.perl.org的#p5p频道发了这个问题,得到了有趣的交流,解释了正在发生的事情。

[15:13:35] <simbabque> 有人能解释一下在Perl作用域中访问子程序中的变量发生了什么吗?我试图阅读该程序的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> 如果你有my $foo = sub { };$foo->();它将按预期工作。
[15:18:34] <alh> 或者在更新的Perl版本中,使用功能use feature qw(lexical_subs); my sub foo {}foo()也可以工作。
[15:18:35] <simbabque> 好吧,那个家伙说他在打高尔夫球时遇到了这个问题。
[15:19:13] <simbabque> alh:对于这两个,我也希望它是一个闭包,但由于子程序foo {}在编译时完成,我感到困惑。
[15:19:38] <rjbs> 词法子程序“做正确的事情”与绑定有关。
[15:19:45] <alh> 仍然是闭包,只是每次重新评估
[15:20:56] <haarg> 它们共享op树,但绑定到不同的变量
[15:23:29] <simbabque> 我在函数内部和外部添加了say "foo: ".\@aTest;say "out ".\@aTest;。那很奇怪。第一轮都是相同的,然后foo保持相同,而循环中的地址得到新地址并保留在随后的迭代中。
[15:26:48] <alh> 当然,在循环中第一次运行时,子程序封闭的变量与循环看到的变量是相同的。
[15:27:01] <alh> 然后我们循环,并获得全新的变量,但子程序不会(因为它没有再次编译)。
[15:27:46] <simbabque> alh:那很有道理,但为什么所有后续迭代都重用相同的变量,但在循环中重置它会让你感到惊讶?这只是Perl对其内存的智能运用吗?
[15:28:48] <alh> 不,您的子程序已封闭了一个变量并保持对其的引用-因此它永远不会消失,并且其值在子调用之间保持不变。
[15:29:07] <alh> 子程序中没有“my @aTest”来“重置”变量
[15:29:19] <alh> 因此,它只保留其值-这就是闭包的目的

我所指的输出来自于这个修改:
for my $i(0..3) {
  my @aTest = (1);

  sub foo {
    push @aTest, 2;
    my $unused = 0;
    print " foo: ".\@aTest;
  }
    print " out: ".\@aTest;

  foo();
}

因此,本质上是在编译时在 @aTest 上建立闭包。在第一次迭代中,循环中的变量与子程序中的变量相同。在所有后续迭代中,它会创建一个新的循环变量,因此我们每次看到一个新的 (1)。但子程序不会再次编译,因此其中的 @aTest 变量保持不变并增长。


1
你嵌套了子程序,所以它的行为与你预期的不同。
引用块: 子程序在编译时存储在全局命名空间中。在你的例子中,b(); 是 main::b(); 的缩写。要限制函数对作用域的可见性,需要将匿名子程序分配给变量。 命名和匿名子程序都可以形成闭包,但由于命名子程序只被编译一次,如果你嵌套它们,它们的行为就不像许多人所期望的那样。 请阅读其余内容here

这并没有真正解释变量为什么保持不变。确实,在编译时剖析子程序,并且该子程序不是词法作用域,但这不是重点。提问者已经知道这一点。问题实际上是为什么@aTestsub foo的作用域内有一个新的变量,尽管从未在那里声明过。提问者正在使用严格模式。 - undefined
1
你难道不好奇吗?如果你不理解它,你将永远无法决定自己是否真的不应该使用它。相反,你依赖他人告诉你。 :) - undefined
这是一个悲观的看法。无论如何,你提供的链接中显示的警告至少在我的Perl 5.20.1中没有发出。它属于“闭包”类别,并且在使用use warnings 'all'时也不会显示出来。回到2002年,当5.6版本刚发布时(正如现在所说的那样有警告),它是会被发出的,但现在已经不会了。此外,没有人提到想要使用它。我们只是好奇而已。 - undefined
如果你将整个代码放在一个子程序中,你会得到警告 - undefined
1
@simbabque,“不会保持共享”在这里并没有问题,因为在循环中放置命名的子程序是相当常见的。例如:{ my $x; sub foo { return $x ||= ...; } }。请记住,{ }是一个只执行一次的循环(就像for (1) { })。它可以使用next(结束循环)、redo(重新开始循环)和last(结束循环)。 - undefined
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接