在这段Perl代码中,$_被修改在哪里?

3
下面的Perl代码在PerlCritic(由Activestate提供)中会生成一个警告:
sub natural_sort {
    my @sorted;
    @sorted = grep {s/(^|\D)0+(\d)/$1$2/g,1} sort grep {s/(\d+)/sprintf"%06.6d",$1/ge,1} @_;
}

生成的警告是:

不要在列表函数中修改$ _

此处有关于该警告的更多信息 我不理解这个警告,因为我不认为我正在修改$ _,虽然我想我必须这样做。请问有人能解释一下吗?
5个回答

10

你的两个 grep 都在使用 s//,因此都会修改 $_。比如下面这个例子:

grep {s/(^|\D)0+(\d)/$1$2/g,1}

这与以下内容相同:

grep { $_ =~ s/(^|\D)0+(\d)/$1$2/g; 1 }

我认为你最好使用map,因为你没有通过grep进行任何过滤,而只是将grep用作迭代器:

sub natural_sort {
    my $t;
    return map { ($t = $_) =~ s/(^|\D)0+(\d)/$1$2/g; $t }
           sort
           map { ($t = $_) =~ s/(\d+)/sprintf"%06.6d",$1/ge; $t }
           @_;
}

这样做可以起到相同的作用并且保持批评者沉默。如果您想要更好的列表操作而不是简单的map,您可能需要查看List::MoreUtils


3
在 grep 中使用了替换操作 (即 s/// ),该操作会修改被 grepped 的列表,也就是 $_

3
这些情况在 perldoc perlvar 中有解释:
以下情况下,即使您没有使用它,Perl 也会假定 $_:
- 下列函数:abs、alarm、chomp、chop、chr、chroot、cos、defined、eval、exp、glob、hex、int、lc、lcfirst、length、log、lstat、mkdir、oct、ord、pos、print、quotemeta、readlink、readpipe、ref、require、reverse(仅在标量上下文中)、rmdir、sin、split(第二个参数)、sqrt、stat、study、uc、ucfirst、unlink、unpack。 - 所有文件测试(-f,-d),除了 -t,默认为 STDIN。请参阅 -X。 - 模式匹配操作 m//、s/// 和 tr///(又名 y///),当没有使用 =~ 运算符时。 - 如果没有提供其他变量,则在 foreach 循环中的默认迭代器变量。 - 在 grep() 和 map() 函数中的隐式迭代器变量。 - 在 given() 的隐式变量。 - 当一个操作的结果被单独用作 while 测试的唯一条件时,在输入记录的默认位置。在 while 测试之外,这种情况不会发生。

2
许多人已经正确回答了 s 操作符修改的是 $_,然而在即将发布的 Perl 5.14.0 中,s 操作符将会有一个新的 r 标志(即 s///r),它不会就地修改,而是返回修改后的元素。在 The Effective Perler 上阅读更多信息。您可以使用 perlbrew 安装这个新版本。

编辑:Perl 5.14 现已发布! 公告 公告 变更

这里是 mu 建议的函数(使用 map),但使用此功能:

use 5.14.0;

sub natural_sort {
    return map { s/(^|\D)0+(\d)/$1$2/gr }
           sort
           map { s/(\d+)/sprintf"%06.6d",$1/gre }
           @_;
}

1
其他答案忽略了非常重要的一点,那就是这行代码:

与前面的代码是有联系的。
grep {s/(\d+)/sprintf"%06.6d",$1/ge,1} @_;

实际上是修改传递给函数的参数,而不是它们的副本。

grep 是一个过滤命令,在代码块内部 $_ 的值是指向 @_ 中的一个值的别名。而 @_ 则包含了传递给函数的参数的别名,因此当 s/// 操作符执行其替换时,更改是针对原始参数进行的。以下示例说明了这一点:

sub test {grep {s/a/b/g; 1} @_}

my @array = qw(cat bat sat);

my @new = test @array;

say "@new";   # prints "cbt bbt sbt" as it should
say "@array"; # prints "cbt bbt sbt" as well, which is probably an error

你正在寻找的行为(应用修改 $_ 的函数到列表的副本中)已经被封装成许多模块中的 apply 函数。我的模块List :: Gen包含这样的实现。 apply 也很容易自己编写:
sub apply (&@) {
    my ($sub, @ret) = @_;
    $sub->() for @ret;
    wantarray ? @ret : pop @ret
}

因此,您的代码可以重写为:

sub natural_sort {
    apply {s/(^|\D)0+(\d)/$1$2/g} sort apply {s/(\d+)/sprintf"%06.6d",$1/ge} @_
}

如果您的目标是通过重复替换来对原始数据进行短暂修改并排序,那么您应该了解一种 Perl 习惯用语,即Schwartzian transform,这是实现该目标的更有效的方法。

我会研究一下这个习语,但鉴于我的数据,修改参数是可以接受的。 - Craig
@Craig => 如果修改参数是无意的,那么这是从不可接受的。如果几个月后需要在常量数组或将再次使用的数组上使用排序,该怎么办? - Eric Strom
我明白你的观点,但这不会发生。而且,我确实说过我会查看这个习语。 - Craig

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