为啥 Perl 5 的函数原型不好?

Posted

技术标签:

【中文标题】为啥 Perl 5 的函数原型不好?【英文标题】:Why are Perl 5's function prototypes bad?为什么 Perl 5 的函数原型不好? 【发布时间】:2010-09-22 18:14:41 【问题描述】:

在another Stack Overflow question Leon Timmermans 断言:

我建议你不要使用原型。它们有它们的用途,但在大多数情况下不是这样,而且绝对不是在这种情况下。

为什么这可能是真的(或其他)?我几乎总是为我的 Perl 函数提供原型,而且我以前从未见过其他人说使用它们不好。

【问题讨论】:

我也很好奇。我唯一不使用它们的时候是当我使用可变数量的参数调用时。 能否推荐您阅读这篇文章,“Perl Prototypes Considered Harmful”? 【参考方案1】:

有些人在查看 Perl 子例程原型时,认为它并不意味着什么:

sub some_sub ($$)  ... 

对于 Perl,这意味着解析器需要两个参数。这是 Perl 让您创建行为类似于内置程序的子程序的方式,所有这些程序都知道从后续代码中可以期待什么。您可以在 perlsub

中阅读有关原型的信息

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

但是,从 Perl v5.20 开始,Perl 有一个特性,在我写这篇文章时是实验性的,它提供的东西更像用户的期望和什么。 Perl 的 subroutine signatures 确实运行时参数计数、变量分配和默认设置:

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";
    

如果您正在考虑原型,这可能是您想要的功能。

【讨论】:

【参考方案2】:

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

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

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

例如,您可以这样定义一个函数:

sub mypush(\@@)  ... 

并将其称为

mypush @array, 1, 2, 3;

无需编写\ 即可引用数组。

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

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

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

请参阅 perlsub 中的 Prototypes 了解所有血腥细节。

【讨论】:

我接受了这个答案,因为我觉得它最好地回答了这个问题 - 原型本质上并不坏,它只是你如何使用它们。 Moose 原型是 /awesome/ p3rl.org/MooseX::Declare p3rl.org/MooseX::Method::Signatures Far More Than Everything You Ever Wanted to Know about Perl Prototypes. 那么它们是用词不当?【参考方案3】:

问题在于 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。

【讨论】:

但是它们仍然提供了一个有用的检查,即你的调用者和 sub 使用相同数量的参数,那有什么问题呢? 否;普遍的共识是 Perl 函数原型基本上没有提供任何好处。你不妨不理会它们,至少在 Perl 5 中是这样。Perl 6 可能是一个不同的(更好的)故事。 还有更好的方法来验证参数,例如 Params::Validate 模块:search.cpan.org/~drolsky/Params-Validate-0.91/lib/Params/… 更正:数组切片返回一个列表,因此标量上下文中的数组切片返回列表的最后一个元素。您对 foo() 的倒数第二次调用会打印 2,因为这是双元素切片中的最后一个元素。更改为my @array = qw(foo bar baz),您会看到不同之处。 (顺便说一句,这就是为什么我不在一次性的演示代码中将数组/列表初始化为基于 0 或 1 的数字序列。上下文中的索引、计数和元素之间的混淆不止一次困扰着我。愚蠢但真实。) @pilcrow:我编辑了答案以使用a b c 使您的观点更清楚。【参考方案4】:

我同意以上两张海报。一般来说,应该避免使用$。原型仅在使用块参数 (&)、glob (*) 或引用原型(\@\$\%\*)时才有用

【讨论】:

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

以上是关于为啥 Perl 5 的函数原型不好?的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript:动态扩展原型是一种不好的做法吗?

为啥 C++11 会从 std::vector 填充构造函数的原型中移除默认值?

我要查一些API函数的原型,在哪里查呢?

JavaScript理解原型与原型链

Perl 5.10 是不是把原型弄乱了?

03. JavaScript高级(3/5)(原型链讲解)