在Perl中,将@_用于参数传递有什么好处?

6
sub foo {
 $arg1 = shift @_;
 $arg2 = shift @_;
 # ...
}

这个习语有什么好处吗?与明确使用$_[0]$_[1]等相比,我只看到了缺点。需要对数组进行移动,这是耗时的。数组还被销毁了,因此在以后的某个时间点,这些参数就消失了(如果您需要它们,并且已经用不同的值覆盖了$arg1,则很遗憾)。

下面提到的一个重要问题是,@_ 中的更改会更改您的参数,这是您不想要的副作用。此外,移位文档记录了您期望的参数。您可以使用 my ($param1, $param2) = @_;,但我更喜欢使用 shift,通常我会使用 my $var = shift; 并隐式地使用 @_ 而不是在 my $var = shift @_; 中显式使用它。 - David W.
@David W.,“shift”和“my (...) = @_”不具有相同的“仅此而已”的暗示。 - ikegami
@ikegami - 我也会破坏 @_,这意味着如果我搞砸了,就无法恢复原始参数。我主要是出于 美观 的考虑这样做的。这使得很容易看到参数是什么,并且您可以在末尾添加注释以解释每个参数。 - David W.
是的,对于注释和默认值,移位操作符确实更胜一筹。 - ikegami
4个回答

9
转移 @_ 是在 OO Perl 中常见的,这样可以将参数与类的实例分离开来,而该实例会自动添加为 @_ 的第一个元素。
它也可用于在分配输入参数的默认值时写入较少的内容,尽管个人认为这并不吸引人。
sub foo {
  my $arg1 = shift // 2;
  my $arg2 = shift // 7;
  # ...
}

我认为与显式地使用 $[0], $1 等相比,只有劣势。

不要使用 $_[0], $_[1] 进行操作,一次性把整个 @_ 数组赋值是更好 / 更少出错 / 更易读的实践方法。

my ($arg1, $arg2) = @_;

请注意,@_元素是传递变量的别名,因此可能会发生意外更改。

sub foo {
  $_[0] = 33;
  $_[1] = 44;
}

my ($x, $y) = (11,22);

print "($x, $y)\n";
foo($x, $y);
print "($x, $y)\n";

输出

(11, 22)
(33, 44)

5

直接访问$_[0]等,比使用命名参数[1]更快,但是有很多理由倾向于使用命名参数。

  1. By far the most important reason, using named parameters serves as a form of documentation.

  2. Using @_ directly is hard to read because it's hard to keep track of which argument has what value.

    For example, "What does $_[4] contain? Is it the rows? Or is that $_[5]?"

  3. It's easy to use the wrong index by accident.

    • It's easy to type the wrong number.
    • It's easy to think you want one number when you want another.
    • It's easy to overlook an index when changing a sub's parameters.

  4. Using @_ directly is hard to read because of the amount of symbols.

    Compare

    grep { $_[0]{$_} =~ /.../ } keys %{$_[0]}
    

    with

    grep { $foos->{$_} =~ /.../ } keys %$foos
    
  5. It's useful for providing defaults.

    sub f {
       my $x = shift // "abc";
       my $y = shift // "def";
       ...
    }
    
  6. Copying into a scalar effectively introduces pass-by-copy semantics, which can be useful.

    $ perl -le'my $x=123; sub f {                  $x=456; print $_[0]; } f($x);'
    456
    
    $ perl -le'my $x=123; sub f { my $arg = shift; $x=456; print $arg;  } f($x);'
    123
    

笔记:

  1. Whether it's my preferred

    sub f {
       my (...) = @_;
       ...
    }
    

    or

    sub f {
        my ... = shift; 
        my ... = shift;
        ...
    }
    

我同意所有的观点。但是基本问题仍然存在:为什么我应该转移而不是进行列表分配或一系列标量分配? - ubuplex
2
@ubuplex,你的问题是关于这个习惯用法相对于显式使用 $_[0]$_[1] 的好处。那是完全不同的问题,评论区不是提问的地方。 - ikegami
你说得对。我的问题表述得很差。我应该问的是,“在参数传递中进行移位是否不必要地增加了时间开销,而直接使用@_则不会?”(无论是直接使用它,通过列表赋值还是通过一系列赋值)。现在有这么多人回答了,我不想再问这个问题了。 - ubuplex
我不确定这有什么不同。我只是会用以下方式表达我的答案:“是的,它会增加时间开销,但这里有6个理由说明它并非不必要。” - ikegami

5
我同意所有的观点。但基本问题仍然存在:为什么我应该转而执行列表赋值或一系列标量赋值,而不是进行移位操作?
既然我要进行移位操作,我会解释一下为什么这样做。
有三种方法可以处理子例程的参数:
方法1:列表赋值
 sub foo {
    my ( $user, $date, $system ) = @_;   # No shifting, and @_ preserved

方法二:从@_中进行个人分配
 sub foo {
    my $user   = $_[1];  
    my $date   = $_[2];   # Must be YYYY-MM-DD
    my $system = $_[3];   # Optional

方法三:使用“shift”
sub foo {
    my $user    = shift;
    my $date    = shift;    # Must be YYYY-MM-DD
    my $system  = shift;    # Optional
方法 1 很受欢迎。Ikegami 使用它,许多其他高级 Perl 开发人员也使用它。它为您提供参数列表,并提供了一种说“这些是我的参数,没有其他参数”的方式。
然而,方法 2方法 3 提供了一个漂亮、易于阅读的参数列表,您可以在行末添加注释。但是,方法 2 还具有维护 @_ 值的优点,那么为什么要使用 shift
再看看 方法 2。看到问题了吗?我从 @_[1] 开始,而不是从 @_[0] 开始——这是一个常见的错误。此外,在开发子例程时,您可能会决定重新排序参数。使用 方法 2 意味着需要重新编号。而 方法 3 不需要重新编号,因此您不会遇到这种情况:
 sub foo {
    my $date   = $_[1];   # Must be YYYY-MM-DD
    my $user   = $_[0];  
    my $system = $_[2];   # Optional

嗯,参数的顺序是什么来着?

那么,保留@_的值怎么办呢?如果你编写了一个更改参数值的程序,那么你可能不会从@_中还原它。而且,如果你这样做,你可能会产生一些容易出错的代码。如果需要更改参数,请将其放入另一个变量中并进行更改。这不是1975年,计算机只有4千字节的内存。

说到1970年代的计算机,计算机的速度已经足够快了,即使进行几十、几百或者几千次移位操作,对于您的总运行时间也不会有太大影响。如果您的程序效率低下,请在效率低下的地方进行优化,而不是通过可能消除移位来缩短几毫秒的时间。与实际运行程序相比,更多的时间将花费在维护程序上。


Damian Conway(Perl Best Practices作者)推荐使用方法1方法3。他表示方法1更为简洁,将参数保持在水平列表中有助于提高可读性,前提是参数数量较少。(强调为本人所加)。

Damian关于方法3表示:"当一个或多个参数需要进行合理性检查或需要在尾注释中记录时,使用基于shift的版本更好。"

我通常都使用方法3。这样,如果我的参数列表增长,我就不用担心重新格式化,而且我认为它看起来更好。


1
重新排列参数意味着重新排序方法3中的行。关于优化:我刚刚进行了一个实验。我计算了第42个斐波那契数(fib(0)=fib(1)=1,fib(n)=fib(n-1)+fib(n-2))。这会产生无尽的递归调用,所有调用都只执行参数传递、2次比较、3个算术操作,因此参数传递的开销得到了最大化。在$_[0]/$n=$_[0]/($n)=@_/$n=shift的情况下,运行时间为244/246/244/245秒。所以你是对的。我接受这个答案。(当然,迭代版本只需0.00秒。) - ubuplex

1
在编写对象类中的方法时,我总是首先将调用者从@ _中移除,然后仅展开其余参数,从而保留所有其他非调用者参数。 这使得调用站点在括号内看起来更像方法定义本身。
sub method
{
  my $self = shift;
  my ( $x, $y, $z ) = @_;
}

$obj->method( "x", "y", "z" );

这样,如果我需要将方法调用委派给其他地方,我只需传递@_本身即可:
sub do_thing_with_whatsit
{
  my $self = shift;
  $self->{whatsit}->do_thing( @_ );
}

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