Perl变量作用域问题

7

我有一个Perl类。它有一个sort()方法,我希望它与内置的sort()函数基本相同:

$object->sort(sub ($$) { $_[0] <=> $_[1] });

但我不能做到:

$object->sort(sub { $a <=> $b });

因为作用域原因。但是List::Util模块使用reduce()函数实现了这个功能。我查看了List::Util模块,他们使用no strict 'vars'进行了一些相当讨厌的操作来完成此操作。我尝试过这样做,但没有成功。
据我所知,reduce()函数之所以能够工作是因为它被导出到适当的命名空间中,因此我的类无法这样做,因为这个函数已经牢固地在另一个命名空间中。这是正确的吗,还是在我的情况下有一些(毫无疑问更加丑陋和不明智的)方法可以实现这一点?
3个回答

9

好的,另外两个答案都是半对的。这里是一个实际排序的工作解决方案:

package Foo;

use strict;
use warnings;

sub sort {
    my ($self, $sub) = @_;

    my ($pkg) = caller;
    my @x = qw(1 6 39 2 5);
    print "@x\n";
    {
        no strict 'refs';
        @x = sort {
            local (${$pkg.'::a'}, ${$pkg.'::b'}) = ($a, $b);
            $sub->();
        } @x;
    }
    print "@x\n";

    return;
}


package main;

use strict;
use warnings;

my $foo = {};
bless $foo, 'Foo';

$foo->sort(sub { $a <=> $b });
# 1 6 39 2 5
# 1 2 5 6 39

据推测,您需要对实际是对象一部分的某些数据进行排序。
您需要使用caller函数,这样就可以将$a$b局部化在调用者的包中,这是Perl查找它们的位置。它正在创建只在调用该子程序时存在的全局变量。
请注意,开启warnings会收到“名称仅使用一次”的警告;我相信您可以通过某种方式避免这种情况。

2
这可能对你的目的足够好,但它很脆弱。不能保证比较函数属于与sort方法调用者相同的包。这就是Sub::Identify发挥作用的地方。 - cjm
@cjm - 这是真的,我一定会研究Sub::Identify,但我的更大问题是让它总体运行,而不是在一般情况下工作。具体方案比一般性失败更好。然而,将这个答案与你的答案结合起来,可以给我一个通用解决方案,这是一个好事。 - Chris Lutz
1
尽管sort内置函数也存在同样的问题。它假设比较函数来自调用者相同的包。因此,如果您可以接受这一点,就可以避免对Sub::Identify的依赖。(或者您可以有条件地要求Sub::Identify,并在未安装时回退到caller。但这需要更多的工作。) - cjm
实际上,这正是我一直在考虑的,我想我会这样做。 - Chris Lutz
啊哈,我从没想过那个。虽然我不确定是否应该鼓励这样的事情。 - Eevee

4
您可以使用Sub::Identify来查找与coderef相关联的包(它称之为stash_name)。然后根据需要在该包中设置 $a 和 $b。您可能需要在方法中使用no strict 'refs'来使其正常工作。
这是Evee的答案,经过修改以适用于一般情况:
use strict;
use warnings;

package Foo;

use Sub::Identify 'stash_name';

sub sort {
    my ($self, $sub) = @_;

    my $pkg = stash_name($sub);
    my @x = qw(1 6 39 2 5);
    print "@x\n";
    {
        no strict 'refs';
        @x = sort {
            local (${$pkg.'::a'}, ${$pkg.'::b'}) = ($a, $b);
            $sub->();
        } @x;
    }
    print "@x\n";

    return;
}


package Sorter;

sub compare { $a <=> $b }

package main;

use strict;
use warnings;

my $foo = {};
bless $foo, 'Foo';

$foo->sort(\&Sorter::compare );

$foo->sort(sub { $b <=> $a });

1
List::Util 只使用 caller。我想在一般情况下这是不够的,对吗?如果调用者传递了来自其他包的函数,则 List::Util 会设置调用者的 $a 而不是函数的。 - Rob Kennedy
在5.10.1中List::Util的版本使用XS代码,基本上与Sub::Identify执行相同的操作来确定coderef所属的包。 - cjm
或者,我可以用XS重写我的模块,这样就不需要依赖了,并且还有机会学习XS。然而,在此期间,我会研究一下这个问题。 - Chris Lutz

1

你可以使用local运算符在子程序调用期间为$a$b设置值:

sub sample
{
    my $callback = shift;
    for (my $i = 0; $i < @_; $i += 2) {
        local ($a, $b) = @_[$i, $i + 1];
        $callback->();
    }
}       

sample sub { print "$a $b\n" }, qw(a b c d e f g h i j);

如果你有一个普通的子程序,而不是一个方法,那么你可以让它更像sort,这样你就不需要在回调函数之前使用sub。在函数上使用原型:
sub sample (&@)

然后你可以这样调用它:

sample { print "$a $b\n" } qw(a b c d e f g h i j);

然而,方法并不受原型的影响。


他特别询问的是一个方法,而不是一个简单的sub。 - Sinan Ünür
3
如果您从类外调用该方法,这种方法是行不通的。您只是将$a和$b局限于而非调用者。 - cjm
啊,我以为这个做法已经有了,但是看来不是。我从perlvar的印象是$a$b在这种情况下足够神奇,以至于它们可以“正常工作”。 - Rob Kennedy
1
不,$a和$b只是普通的包变量。 - cjm
@Rob:是的,我也是这么想的——但当我查看时,perlvar实际上说它们是包变量。 - j_random_hacker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接