为什么Perl中的foreach变量赋值会修改数组中的值?

20

好的,我有以下代码:

use strict;
my @ar = (1, 2, 3);
foreach my $a (@ar)
{
  $a = $a + 1;
}

print join ", ", @ar;

那输出是什么?

2, 3, 4

这是怎么回事?为什么会那样?这种情况会一直发生吗?$a不是局部变量吗?他们当时在想什么呢?


12
此行为已在 perlsyn 手册中有所记录:http://perldoc.perl.org/perlsyn.html#Foreach-Loops。FWIW 是“顺便提一下”的意思。 - friedo
7
把它想象成声明迭代器。在几乎所有实现迭代器的语言中,您可以使用迭代器来修改现有值。如果您想要复制语义,只需进行复制即可 :) - Robert P
7个回答

27

Perl有很多看起来有点奇怪的语法,这些语法可以极大地简化常见任务(比如迭代列表并以某种方式修改其内容),但如果您不了解它们可能会让您感到困惑。

$a被赋予数组中的值的引用 - 这允许您在循环内部修改数组。如果您不想这样做,请不要修改$a


感谢你解释为什么他们决定这样做。 - tster

23

请参阅perldoc perlsyn:

如果LIST的任何元素是一个左值,则您可以通过在循环内修改VAR来修改它。相反,如果LIST的任何元素不是一个左值,则任何尝试修改该元素的尝试都将失败。换句话说,foreach循环索引变量是您正在循环遍历的列表中每个项目的隐式别名。

这并不是什么奇怪或者异常的语言特性,虽然我发现有多少人在遇到他们不理解的行为时拒绝查看文档而感到奇怪。


4
+1 表示“我确实觉得很奇怪有多少人完全拒绝阅读任何文档”。 :-P - C. K. Young
18
即使有文献证明一件事情,也不一定能被视为奇怪。比如,如果语言规范规定在 return 语句之后只会执行一条语句,那这就是一种既奇怪又有文献支持的情况。奇怪是一个主观的评价。 - tster

9

$a 在这里是数组元素的别名。只要不在代码中使用 $a =,就不会修改数组。 :-)

如果我没记错的话,mapgrep等函数都有相同的别名行为。


5

正如其他人所说,这是有文档记录的。

我的理解是,@_formapgrep的别名行为不仅提供了速度和内存优化,还为创意提供了有趣的可能性。其本质上是通过引用传递调用构造块,从而避免了不必要的数据复制,从而节省时间和内存。

use strict;
use warnings;

use List::MoreUtils qw(apply);

my @array = qw( cat dog horse kanagaroo );

foo(@array);


print join "\n", '', 'foo()', @array;

my @mapped = map { s/oo/ee/g } @array;

print join "\n", '', 'map-array', @array;
print join "\n", '', 'map-mapped', @mapped;

my @applied = apply { s/fee//g } @array;

print join "\n", '', 'apply-array', @array;
print join "\n", '', 'apply-applied', @applied;


sub foo {
   $_ .= 'foo' for @_;
}

注意使用List::MoreUtils apply函数。它类似于map,但是它复制了主题变量而不是使用引用。如果你讨厌编写以下代码:
 my @foo = map { my $f = $_; $f =~ s/foo/bar/ } @bar;

你会喜欢 应用,它可以将其转化为:

 my @foo = apply { s/foo/bar/ } @bar;

需要注意的是:如果您将只读值传递到修改其输入值的构造之一中,您将收到“尝试修改只读值”的错误。

perl -e '$_++ for "o"'

3

这里的重要区别在于,当您在for循环的初始化部分声明一个my变量时,它似乎具有本地变量和词法变量的某些属性(对内部知识更多的人能否进行澄清?)

my @src = 1 .. 10;

for my $x (@src) {
    # $x is an alias to elements of @src
}

for (@src) {
    my $x = $_;
    # $_ is an alias but $x is not an alias
}

这样做的有趣副作用是,在第一种情况下,在for循环内定义的sub{}将成为对列表$x中任何元素的闭包别名。 了解这一点,即使是奇怪的,也可以关闭对别名值的引用,甚至可能是全局的,我认为这在任何其他结构中都不可能。
our @global = 1 .. 10;
my @subs;
for my $x (@global) { 
    push @subs, sub {++$x}
}

$subs[5](); # modifies the @global array

回复:“我认为没有其他结构可以实现这个”。你肯定听说过Perl的座右铭TIMTOWTDI(http://en.wikipedia.org/wiki/TIMTOWTDI)。获取数组元素的引用:`my $g5r = $global[5]; ${$g5r}++;。使用全局变量创建别名:our $g5; *g5 = $global[5]; $g5++;。将其本地化并使用子程序:sub doSubOnRef(&$) { local $_; *_ = $_[1]; $_[0]->() } doSubOnRef { $_++ } $global[5];。检查它们是否都指向同一个东西:my @equality = map { $_ == $global[5] } $global[5], $g5, $g5r, doSubOnRef { $_ } $global[5];`。 - Chris Johnsen
@Chris => 不同之处在于,在我的示例中,$subs [5]是围绕别名的闭包,这个别名也恰好是(但不必是)全局的。我的观点是,我不知道还有其他方法可以使用核心创建围绕别名的闭包。在您的示例中,无法从doSubOnRef返回闭包,因为类型全局别名/本地变量仅适用于包变量,除非上述情况,否则无法关闭它们。 - Eric Strom
它并不是轻量级的,但可以使用绑定词法完成:@subs_ 等同于你的 @subs: package TiedScalarRef; require Tie::Scalar; our @ISA = qw(Tie::StdScalar); sub FETCH { ${$_[0]->SUPER::FETCH(@_)} } sub STORE { ${$_[0]->SUPER::FETCH($_[0])} = $_[1] }package main; my @subs_; for my $i (0..9) { my $r; tie $r, 'TiedScalarRef', \$global[$i]; push @subs_, sub {++$r} } $subs_[5](); - Chris Johnsen

0

你的 $a 只是在循环遍历列表时作为每个元素的别名使用。它被用来代替 $_。你可以看出 $a 不是局部变量,因为它在块外声明。

如果你把 $a 看作是 $_ 的替身(这就是它的作用),那么给 $a 赋值改变列表内容的原因就更加明显了。事实上,如果你像这样定义自己的迭代器,$_ 就不存在了。

foreach my $a (1..10)
    print $_; # error
}

如果你想知道这个意义何在,考虑以下情况:

my @row = (1..10);
my @col = (1..10);

foreach (@row){
    print $_;
    foreach(@col){
        print $_;
    }
}

在这种情况下,为 $_ 提供一个更友好的名称会使其更易读。
foreach my $x (@row){
    print $x;
    foreach my $y (@col){
        print $y;
    }
}

0

尝试

foreach my $a (@_ = @ar)

现在修改$a不会改变@ar。在我的v5.20.2版本上运行良好。

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