Perl - Какие области видимости / замыкания / среды вызывают такое поведение? - PullRequest
1 голос
/ 12 января 2012

Для корневого каталога я хочу определить самый мелкий родительский каталог из любого каталога .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;затем функция по-прежнему выполняется правильно.Это означает, что а) внешняя область видимости (firstDirWithFileUnder) все еще может видеть переменную $result, чтобы иметь возможность ее вернуть, и б) печать показывает, что wanted по-прежнему видит значение $result с прошлого раза, т.е.сформировал замыкание, которое сохранилось после первого вызова firstDirWithFileUnder.

Может кто-нибудь объяснить, что происходит, и предложить, как я могу правильно сбросить значение $result в ноль при входе во внешнюю область?

1 Ответ

5 голосов
/ 12 января 2012

Использование warnings, а затем diagnostics дает эту полезную информацию, включая решение:

Переменная «$ needle» не будет использоваться совместно в ----- строке 12 (# 1)

(W замыкание) Внутренняя (вложенная) подпрограмма с именем ссылается на Лексическая переменная, определенная во внешней подпрограмме с именем.

Когда вызывается внутренняя подпрограмма, она увидит значение переменная внешней подпрограммы, как это было до и во время first вызов внешней подпрограммы; в этом случае после первого звонка внешняя подпрограмма завершена, внутренняя и внешняя подпрограммы не будут больше разделяют общее значение для переменной. Другими словами, переменная больше не будет общедоступна.

Эту проблему обычно можно решить, сделав внутреннюю подпрограмму анонимный, используя синтаксис sub {}. Когда внутренние анонимные подпрограммы ссылочные переменные во внешних подпрограммах созданы, они автоматически возвращаются к текущим значениям таких переменных.


$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 - это объявление времени выполнения для подпрограммы, и оно переопределяется с текущей ссылкой на $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. В противном случае 2-я и 3-я среды выполнения $foo могут указывать на один и тот же адрес, даже если они ссылаются на разные переменные (память перераспределяется при каждом объявлении переменной).

...