为什么Perl 5的功能原型不好?


116

另一个堆栈溢出问题中, Leon Timmermans断言:

我建议您不要使用原型。它们有其用途,但不适用于大多数情况,并且绝对不在此情况下使用。

为什么这可能是正确的(或相反)?我几乎总是为我的Perl函数提供原型,而且我从未见过有人对使用它们发表过不好的看法。


我也很好奇 我唯一不使用它们的时间是在使用可变数量的参数进行调用时。
Paul Tomblin

7
我是否可以建议您阅读文章“被认为有害的Perl原型”
tchrist 2012年

Answers:


121

如果使用正确,原型也不错。困难在于Perl的原型无法像人们通常期望的那样工作。具有其他编程语言背景的人们倾向于期望原型提供一种机制来检查函数调用是否正确:也就是说,他们具有正确的数量和类型的参数。Perl的原型并不适合此任务。这是滥用这是很糟糕。Perl的原型有一个独特且截然不同的目的:

原型允许您定义行为类似于内置函数的函数。

  • 括号是可选的。
  • 将上下文强加于参数。

例如,您可以定义如下函数:

sub mypush(\@@) { ... }

并称其为

mypush @array, 1, 2, 3;

无需编写\即可获取对数组的引用。

简而言之,原型使您可以创建自己的语法糖。例如,Moose框架使用它们来模拟更典型的OO语法。

这非常有用,但是原型非常有限:

  • 它们必须在编译时可见。
  • 它们可以被绕过。
  • 将上下文传播到参数可能会导致意外行为。
  • 除了严格规定的形式外,它们可能使调用函数变得困难。

有关所有血腥细节,请参见perlsub中的原型


2
我接受了这个答案,因为我觉得它可以最好地回答这个问题-原型从本质上来讲并不是很糟糕,而是您使用它们的方式。
Alnitak



那么,他们是一个误称吗?
彼得·莫滕森

69

问题在于Perl的函数原型无法实现人们认为的功能。它们的目的是允许您编写将像Perl的内置函数一样被解析的函数。

首先,方法调用完全忽略了原型。如果您正在执行OO编程,则方法具有的原型无关紧要。(因此,他们不应该有任何原型。)

其次,没有严格执行原型。如果您使用调用子例程&function(...),则原型将被忽略。因此,它们实际上并没有提供任何类型安全性。

第三,它们是遥不可及的诡异动作。(特别是$原型,它导致在标量上下文而不是默认列表上下文中评估相应的参数。)

特别是,它们使从数组传递参数变得困难。例如:

my @array = qw(a b c);

foo(@array);
foo(@array[0..1]);
foo($array[0], $array[1], $array[2]);

sub foo ($;$$) { print "@_\n" }

foo(@array);
foo(@array[0..1]);
foo($array[0], $array[1], $array[2]);

印刷品:

a b c
a b
a b c
3
b
a b c

以及3条有关的警告main::foo() called too early to check prototype(如果启用了警告)。问题在于,在标量上下文中求值的数组(或数组切片)将返回数组的长度。

如果您需要编写一个类似于内置函数的函数,请使用原型。否则,请勿使用原型。

注意:Perl 6将完全改写并且非常有用的原型。此答案仅适用于Perl 5。


但是它们仍然可以提供有用的检查,以确保您的调用方和子方使用相同数量的参数,那么这有什么问题呢?
Paul Tomblin,

2
没有; 普遍的共识是Perl函数原型基本上没有任何好处。至少在Perl 5中,您可能也不会对它们感到困扰。Perl6可能是一个不同的(更好的)故事。
乔纳森·勒夫勒

5
有更好的方法来验证参数,例如Params :: Validate模块:search.cpan.org/~drolsky/Params-Validate-0.91/lib/Params/…–
friedo

10
纠正:数组切片返回一个列表,因此标量上下文中的数组切片返回列表的最后一个元素。您倒数第二次调用foo()打印件2,因为那是您的两个元素切片中的最后一个元素。更改为my @array = qw(foo bar baz),您将看到不同之处。(顺便说一句,这就是为什么我没有在一次性的说明性代码中将数组/列表初始化为基于0或1的数字序列的原因。上下文中索引,计数和元素之间的混淆不止一次使我痛苦。愚蠢但真实。)
pilcrow,2012年

2
@pilcrow:我编辑了答案,a b c以使您的观点更加清楚。
Flimm 2014年

30

我同意以上两个海报。通常,$应避免使用。原型中使用块参数(当只有有用&),水珠(*),或参考原型(\@\$\%\*


总的来说,也许可以,但是我想提两个例外:首先,($)原型创建了一个命名的一元运算符,该运算符很有用(当然Perl认为它们很有用;有时我也有)。其次,在覆盖内置程序时(无论是通过导入还是使用CORE :: GLOBAL::),通常应该使用内置程序所具有的任何原型,即使其中包含$,也可能会让程序员感到惊讶(您自己,甚至带有列表上下文的列表,否则内置列表将提供标量上下文。
Sidhekin

4

有些人看着Perl子例程原型,认为这意味着它不具有以下含义:

sub some_sub ($$) { ... }

对于Perl,这意味着解析器需要两个参数。这是Perl的一种方法,它使您可以创建行为类似于内置程序的子例程,所有这些子例程都知道对后续代码的期望。您可以在perlsub中阅读有关原型的信息

人们在不阅读文档的情况下猜测,原型是指运行时参数检查或其他语言中看到的类似内容。与大多数人对Perl的猜测一样,事实证明它们是错误的。

但是,从Perl v5.20开始,Perl具有一项功能,在我撰写本文时是实验性的,它提供了更多类似于用户期望和期望的功能。Perl的子例程签名可以进行运行时参数计数,变量分配和默认设置:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Buster', 'Nikki', 'Godzilla' );

sub animals ($cat, $dog, $lizard = 'Default reptile') { 
    say "The cat is $cat";
    say "The dog is $dog";
    say "The lizard is $lizard";
    }

如果您正在考虑使用原型,那么这可能就是您想要的功能。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.