闭包与局部"子"中的局部变量可见性



Perl 5.18.2似乎接受"本地子例程"。

例:

sub outer()
{
my $x = 'x';   # just to make a simple example
sub inner($)
{
print "${x}$_[0]n";
}
inner('foo');
}

如果没有"本地子例程",我会写:

#...
my $inner = sub ($) {
print "${x}$_[0]n";
}
$inner->('foo');
#...

最重要的是,我认为两者是等价的。

然而,第一个变体并不像Perl抱怨的那样工作:

可变$x在 ...

其中...描述了"本地子例程"中引用$x行。

谁能解释这一点?Perl 的本地子例程与 Pascal 的本地子例程有根本的不同吗?

问题中的术语">局部子例程"似乎指的是词汇子例程。 这些是私有子例程,仅在定义它们的作用域(块)内可见,在定义之后;就像私有变量一样。

但是它们被定义(或预先声明)为mystate,如my sub subname { ... }

仅仅在另一个子例程中编写一个sub subname { ... }并不能使它"本地"(在任何版本的Perl中),但它的编译就像它与其他子例程一起编写一样,并放置在其包的符号表中(例如main::)。


这个问题在标题中提到了关闭,这是对此的评论

Perl 中的闭包是程序中的一个结构,通常是一个标量变量,引用一个 sub,并在创建时从其作用域中携带环境(变量)。另请参阅其上的 perlfaq7 条目。 解释起来很乱。例如:

sub gen { 
my $args = "@_"; 
my $cr = sub { say "Closed over: $args, my args: @_" }
return $cr;
}
my $f = gen( qw(args for gen) );
$f->("hi closed");
# Prints:
# Closed over: args for gen, my args: hi closed

匿名 sub "关闭"定义它的作用域中的变量,从某种意义上说,当它的生成函数返回其引用并超出范围时,由于该引用的存在,这些变量仍然存在。 由于匿名 sub 是在运行时创建的,因此每次调用其生成函数并重新制作其中的词法时,anon sub 也是如此,因此它始终可以访问当前值。 因此,返回的对 anon-sub 的引用使用词法数据,否则这些数据将消失。一点魔法。

回到"本地"潜艇的问题。 如果我们想为问题引入实际的闭包,我们需要从outer子例程返回一个代码引用,例如

sub outer {
my $x = 'x' . "@_";
return sub { say "$x @_" }
}
my $f = outer("args");
$f->( qw(code ref) );   # prints:  xargs code ref

或者,根据主要问题,如 v5.18.0 中引入并从 v5.26.0 开始稳定,我们可以使用命名的词法(真正嵌套!

sub outer {
my $x = 'x' . "@_";

my sub inner { say "$x @_" };
return &inner;
}

在这两种情况下,my $f = outer(...);都有从outer返回的代码引用,该代码引用正确使用局部词法变量($x)及其最新值。

但是我们不能在outer内部使用名为普通的 sub 来关闭

sub outer {
...
sub inner { ... }  # misleading, likely misguided and buggy
return &inner;    # won't work correctly
}

inner是在编译时创建的,并且是全局的,因此它从outer使用的任何变量都将从第一次调用outer时烘焙其值。 因此,只有在下一次调用outer之前inner才会正确 - 当outer中的词汇环境被重新制作但inner没有。例如,我可以很容易地找到这篇文章,并在perldiag中看到条目(或将use diagnostics;添加到程序中)。


在我看来,穷人的对象在某种程度上,因为它具有功能和数据,在其他地方在另一个时间制造,并且可以与传递给它的数据一起使用(两者都可以更新)

如果需要"本地"订阅,则可以根据所需的向后兼容性级别使用以下方法之一:

  • 5.26+:

    my sub inner { ... }
    
  • 5.18+:

    use experimental qw( lexical_subs );  # Safe: Accepted in 5.26.
    my sub inner { ... }
    
  • "任何"版本:

    local *inner = sub { ... };
    

但是,您不应该使用sub inner { ... }.

<小时 />
sub f { ... }

基本相同

BEGIN { *f = sub { ... } }

所以

sub outer {
...
sub inner { ... }
...
}

基本上是

BEGIN {
*outer = sub {
...
BEGIN {
*inner = sub { ... };
}
...
};
}

如您所见,即使在outer之外也可以看到inner,因此它根本不是"本地的"。

正如你所看到的,*inner赋值是在编译时完成的,这引入了另一个主要问题。

use strict;
use warnings;
use feature qw( say );
sub outer {
my $arg = shift;
sub inner {
say $arg;
}
inner();
}
outer( 123 );
outer( 456 );
Variable "$arg" will not stay shared at a.pl line 9.
123
123

5.18 确实引入了词汇("本地")子例程。

use strict;
use warnings;
use feature qw( say );
use experimental qw( lexical_subs );  # Safe: Accepted in 5.26.
sub outer {
my $arg = shift;
my sub inner {
say $arg;
};
inner();
}
outer( 123 );
outer( 456 );
123
456

如果你需要支持旧版本的 Perl,你可以使用以下命令:

use strict;
use warnings;
use feature qw( say );
sub outer {
my $arg = shift;
local *inner = sub {
say $arg;
};
inner();
}
outer( 123 );
outer( 456 );
123
456

我从man perldiag中找到了一个相当好的解释:

Variable "%s" is not available
(W closure) During compilation, an inner named subroutine or eval
is attempting to capture an outer lexical that is not currently
available.  This can happen for one of two reasons.  First, the
outer lexical may be declared in an outer anonymous subroutine
that has not yet been created.  (Remember that named subs are
created at compile time, while anonymous subs are created at run-
time.)  For example,
sub { my $a; sub f { $a } }
At the time that f is created, it can't capture the current value
of $a, since the anonymous subroutine hasn't been created yet.

所以这将是一个可能的解决方法:

sub outer()
{
my $x = 'x';   # just to make a simple example
eval 'sub inner($)
{
print "${x}$_[0]n";
}';
inner('foo');;
}

。虽然这个不会:

sub outer()
{
my $x = 'x';   # just to make a simple example
eval {
sub inner($)
{
print "${x}$_[0]n";
}
};
inner('foo');;
}

最新更新