Perl子程序是按引用调用还是按值调用?

时间:2011-04-21 07:41:29

标签: perl

我正在试图弄清楚Perl子程序及其工作原理。 从perlsub我知道子例程是按引用调用的,并且需要一个赋值(如my(@copy) = @_;)来将它们转换为按值调用。

在下文中,我看到change被引用调用,因为“a”和“b”被更改为“x”和“y”。但我很困惑为什么数组没有用额外的元素“z”扩展?

use strict;
use Data::Dumper;

my @a = ( "a" ,"b" );

change(@a);

print Dumper(\@a);

sub change
{
    @_[0] = "x";
    @_[1] = "y";
    @_[2] = "z";
}

输出:

$VAR1 = [
          'x',
          'y'
        ];

在下面,我传递哈希而不是数组。为什么键不能从“a”变为“x”?

use strict;
use Data::Dumper;

my %a = ( "a" => "b" );

change(%a);

print Dumper(\%a);

sub change
{
    @_[0] = "x";
    @_[1] = "y";
}

输出:

$VAR1 = {
    'a' => 'y'
};

我知道真正的解决方案是使用\@通过引用传递数组或哈希,但我想完全理解这些程序的行为。

5 个答案:

答案 0 :(得分:38)

Perl总是通过引用传递。只是有时呼叫者会通过临时标量。

你必须要意识到的第一件事是subs的参数可以是唯一的一件事:标量列表。*一个人不能传递数组或哈希值。评估数组和散列,返回其内容列表。这意味着

f(@a)

相同
f($a[0], $a[1], $a[2])

Perl通过引用传递。具体来说,Perl将每个参数别名化为@_的元素。修改元素@_将更改$a[0]等返回的标量,从而修改@a的元素。

第二个重要的事情是数组或哈希元素的键确定元素在结构中的存储位置。否则,$a[4]$h{k}将需要查看数组或散列的每个元素以找到所需的值。这意味着密钥不可修改。移动值需要使用新键创建新元素并删除旧键上的元素。

因此,无论何时获得数组或散列的键,都会获得键的副本。可以这么说,新鲜的标量。

回到问题,

f(%h)

相同
f(
   my $k1 = "a", $h{a},
   my $k2 = "b", $h{b}, 
   my $k2 = "c", $h{c}, 
)

@_仍然是%h返回的值的别名,但其中一些只是用于保存密钥的临时标量。改变这些将不会产生持久影响。

* - 一些内置插件(例如grep)更像是流控制语句(例如while)。它们有自己的解析规则,因此不限于传统的子模型。

** - 原型可以影响参数列表的评估方式,但它仍然会产生一个标量列表。

答案 1 :(得分:9)

Perl的子例程接受参数作为标量的平面列表。作为参数传递的数组实际上也是一个平面列表。即使哈希被视为一个键的平面列表,后跟一个值,后跟一个键等等。

除非您明确这样做,否则不会将平面列表作为参考传递。修改$_[0]修改$a[0]的事实是因为@_的元素成为作为参数传递的元素的别名。修改$_[0]与修改示例中的$a[0]相同。但是,虽然这与适用于任何编程语言的“通过引用传递”的常见概念大致相似,但这并不是专门传递Perl引用; Perl的引用是不同的(实际上“引用”是一个重载的术语)。别名(在Perl中)是某事物的同义词,其中作为引用类似于指向某事物的指针。

正如perlsyn所述,如果你作为一个整体分配给@_,你就会破坏其别名状态。另请注意,如果您尝试修改$_[0],而$_[0]恰好是文字而非变量,则会出错。另一方面,如果 可修改,则修改$_[0]会修改调用者的值。因此,在示例一中,将$_[0]$_[1]更改回@a,因为@_的每个元素都是@a中每个元素的别名。

你的第二个例子有点棘手。散列键是不可变的。除了删除散列键之外,Perl不提供修改散列键的方法。这意味着$_[0]不可修改。当您尝试修改$_[0]时,Perl无法遵守该请求。它可能应该发出警告,但事实并非如此。你看,传递给它的平面列表包括不可修改的密钥,后跟可修改的值等。这主要是一个非问题。我想不出有任何理由以你演示的方式修改哈希的各个元素;由于哈希没有特定的顺序,因此您无法简单地控制@_中哪些元素传播回%a中的哪些值。

正如您所指出的,正确的协议是通过\@a\%a,以便将它们称为$_[0]->{element}$_[0]->[0]。尽管符号稍微复杂一点,但它在一段时间后变成了第二天性,而且(在我看来)对于发生的事情更为清晰。

请务必查看the perlsub documentation。特别是:

  

传入的任何参数都显示在数组@_中。因此,如果您使用两个参数调用函数,那么这些参数将存储在$_[0]$_[1]中。数组@_是一个本地数组,但其元素是实际标量参数的别名。特别是,如果更新元素$_[0],则更新相应的参数(如果不可更新,则会发生错误)。如果参数是调用函数时不存在的数组或散列元素,则仅在修改它时(或者如果)修改该元素或者对其进行引用。 (Perl的某些早期版本创建了元素,无论元素是否已分配给。)分配给整个数组@_会删除该别名,并且不会更新任何参数。

答案 2 :(得分:4)

(请注意,use warningsuse strict更重要。)

@_本身不是对任何东西的引用,它是一个数组(实际上,只是一个堆栈的视图,但如果你做一些像引用它,它会变成一个真正的数组)其元素每个都是传递参数的别名。那些传递的参数是通过的单个标量;没有传递数组或散列的概念(尽管你可以将引用传递给一个)。

因此,@_的移位,拼接,附加元素等不会影响传递的任何内容,但它们可能会更改原始别名之一的索引或从中删除。

所以在你调用change(@a)的地方,这会在堆栈上放置两个别名,一个到$a[0],一个到$a[1]change(%a)更复杂; %a变平为一个交替的键和值列表,其中值是实际的散列值,修改它们会修改散列中存储的内容,但是键只是副本,不再与散列相关联。 / p>

答案 3 :(得分:2)

Perl不通过引用传递数组或散列本身,它将条目(数组元素,或散列键和值)展开到列表中并将此列表传递给函数。 @_然后允许您访问标量作为参考。

这与写作大致相同:

@a = (1, 2, 3);

$b = \$a[2];

${$b} = 4;

@a now [1, 2, 4];

你会注意到,在第一种情况下,你无法向@a添加额外的项目,所发生的一切都是你修改了已存在的@a成员。在第二种情况下,散列键实际上并不作为标量存在于散列中,因此当创建散列的扩展列表以将其传递到函数中时,需要将这些散列键创建为临时标量中的副本。修改此临时标量不会修改散列键,因为它不是散列键。

如果要修改函数中的数组或散列,则需要传递对容器的引用:

change(\%foo);

sub change {
   $_[0]->{a} = 1;
}

答案 4 :(得分:0)

首先,您将@ sigil混淆为指示数组。这实际上是一个列表。当您调用Change(@a)时,您将列表传递给函数,而不是数组对象。

哈希的情况略有不同。 Perl将您的调用评估为列表,并将值作为列表传递。