Perl - 什么范围/闭包/环境产生这种行为?

时间:2012-01-12 16:58:09

标签: perl closures environment subroutine

鉴于根目录,我希望识别任何.svn目录和pom.xml中最浅的父目录。

为实现这一目标,我定义了以下功能

use File::Find;
sub firstDirWithFileUnder {
    $needle=@_[0];
    my $result = 0;
    sub wanted {
        print "\twanted->result is '$result'\n";
        my $dir = "${File::Find::dir}";

        if ($_ eq $needle and ((not $result) or length($dir) < length($result))) {
            $result=$dir;
            print "Setting result: '$result'\n";
        }
    }
    find(\&wanted, @_[1]);
    print "Result: '$result'\n";
    return $result;
}

..并称之为:

    $svnDir = firstDirWithFileUnder(".svn",$projPath);
    print "\tIdentified svn dir:\n\t'$svnDir'\n";
    $pomDir = firstDirWithFileUnder("pom.xml",$projPath);
    print "\tIdentified pom.xml dir:\n\t'$pomDir'\n";

有两种情况我无法解释:

  1. 当搜索.svn成功时,嵌套子例程$result内感知的wanted值将持续到firstDirWithFileUnder的下一个调用中。因此,当pom搜索开始时,虽然行my $result = 0;仍然存在,但wanted子例程将其值视为上一次firstDirWithFileUnder调用的返回值。
  2. 如果my $result = 0;行被注释掉,那么该函数仍然可以正常执行。这意味着a)外部范围(firstDirWithFileUnder)仍然可以看到$result变量能够返回它,并且b)打印显示wanted仍然看到$result值最后一次,也就是说它似乎形成了一个在firstDirWithFileUnder的第一次调用之后持续存在的闭包。
  3. 有人可以解释发生了什么,并建议我在进入外部范围时如何正确地将$result的值重置为零?

1 个答案:

答案 0 :(得分:5)

使用warnings然后diagnostics会产生这些有用的信息,包括解决方案:

  

变量“$ needle”不会在-----第12行(#1)保持共享

     

(W闭包)内部(嵌套)命名子例程引用a       在外部命名子例程中定义的词法变量。

     

当调用内部子程序时,它将看到的值       外部子例程的变量与第一个之前和期间的变量一样       调用外部子程序;在这种情况下,第一次调用之后       外子程序完成后,内子程序和外子程序都没有       更长时间共享变量的公共值。换句话说,       变量将不再被共享。

     

这个问题通常可以通过制作内部子程序来解决       匿名,使用sub {}语法。当内部匿名subs表示       它们是外部子程序中的引用变量       会自动反弹到这些变量的当前值。


$result是词法范围的,这意味着每次调用&firstDirWithFileUnder时都会分配一个全新的变量。 sub wanted { ... }编译时子例程声明,这意味着它由Perl解释器一次编译并存储在包的符号表中。由于它包含对词法范围$result变量的引用,因此Perl保存的子例程定义仅引用$result的第一个实例。第二次拨打&firstDirWithFileUnder并声明新的$result变量时,这将是一个与$result内的&wanted完全不同的变量。

您需要将sub wanted { ... }声明更改为词法范围的匿名子:

my $wanted = sub {
    print "\twanted->result is '$result'\n";
    ...
};

并调用File::Find::find作为

find($wanted, $_[1])

此处,$wanted是子例程的运行时声明,并在每次单独调用{{1}时使用当前对$result的引用重新定义}。


更新:此代码段可能具有指导意义:

&firstDirWithFileUnder

典型输出:

sub foo {
    my $foo = 0;  # lexical variable
    $bar = 0;     # global variable
    sub compiletime {
        print "compile foo is ", ++$foo, " ", \$foo, "\n";
        print "compile bar is ", ++$bar, " ", \$bar, "\n";
    }
    my $runtime = sub {
        print "runtime foo is ", ++$foo, " ", \$foo, "\n";
        print "runtime bar is ", ++$bar, " ", \$bar, "\n";
    };
    &compiletime;
    &$runtime;
    print "----------------\n";
    push @baz, \$foo;  # explained below
}
&foo for 1..3;

请注意,编译时间compile foo is 1 SCALAR(0xac18c0) compile bar is 1 SCALAR(0xac1938) runtime foo is 2 SCALAR(0xac18c0) runtime bar is 2 SCALAR(0xac1938) ---------------- compile foo is 3 SCALAR(0xac18c0) compile bar is 1 SCALAR(0xac1938) runtime foo is 1 SCALAR(0xa63d18) runtime bar is 2 SCALAR(0xac1938) ---------------- compile foo is 4 SCALAR(0xac18c0) compile bar is 1 SCALAR(0xac1938) runtime foo is 1 SCALAR(0xac1db8) runtime bar is 2 SCALAR(0xac1938) ---------------- 始终引用相同的变量$foo,这也是运行时SCALAR(0xac18c0)第一次运行函数的时间。

此示例中包含$foo的最后一行,&foo,以便push @baz,\$foo$foo结束时不会收集垃圾。否则,第二个和第三个运行时&foo可能指向相同的地址,即使它们引用了不同的变量(每次声明变量时都会重新分配内存)。