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

Kom*_*ave 1 environment perl closures 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;
}
Run Code Online (Sandbox Code Playgroud)

..因此称呼它:

    $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";
Run Code Online (Sandbox Code Playgroud)

有两种情况我无法解释:

  1. 当搜索.svn成功时,$result嵌套子例程中感知的值将wanted持续到下一次调用firstDirWithFileUnder.因此,当pom搜索开始时,虽然该行my $result = 0;仍然存在,但wanted子例程将其值视为上次firstDirWithFileUnder调用的返回值.
  2. 如果该my $result = 0;行被注释掉,那么该函数仍然可以正常执行.这意味着a)外部scope(firstDirWithFileUnder)仍然可以看到$result变量能够返回它,并且b)打印显示wanted仍然看到$result上次的值,即它似乎形成了一个持续超出第一次调用的闭包firstDirWithFileUnder.

有人可以解释发生了什么,并建议我如何$result在进入外部范围时正确地将值重置为零?

mob*_*mob 5

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

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

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

当调用内部子程序时,它将看到外部子程序变量的值,就像它在第一次 调用外部子程序之前和期间一样; 在这种情况下,在第一次调用外部子程序完成后,内部子程序和外部子程序将不再共享该变量的公共值.换句话说,该变量将不再共享.

通常可以通过使用sub {}语法使内部子例程匿名来解决此问题. 当创建引用外部子例程中的变量的内部匿名子时,它们会自动回弹到这些变量的当前值.


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

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

my $wanted = sub {
    print "\twanted->result is '$result'\n";
    ...
};
Run Code Online (Sandbox Code Playgroud)

并调用File::Find::find

find($wanted, $_[1])
Run Code Online (Sandbox Code Playgroud)

这里$wanted是子例程的运行时声明,并且$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;
Run Code Online (Sandbox Code Playgroud)

典型输出:

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)
----------------
Run Code Online (Sandbox Code Playgroud)

请注意,编译时$foo总是引用相同的变量SCALAR(0xac18c0),这也是运行$foo函数的第一次运行时间.

的最后一行&foo,push @baz,\$foo包括在这个例子中,这样$foo就不会被回收,在年底&foo.否则,第二个和第三个运行时$foo可能指向相同的地址,即使它们引用不同的变量(每次声明变量时都会重新分配内存).