Perl范围 - 访问子例程中的变量

时间:2017-02-03 13:46:45

标签: perl

我正在做一些代码高尔夫,并决定尝试'聪明'并声明一个子程序,它将拥有它已经在范围内所需的变量,以避免额外的代码必须传入参数:

#! 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内部如何保留它的旧价值呢?

3 个答案:

答案 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;

但是,它按预期打印123。还记得指令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