检查Perl函数参数是否值得?

19

关于MooseX::Method::Signatures和早期的模块Params::Validate,有很多关注,它们都设计用于对方法或函数的每个参数进行类型检查。我正在考虑在我的所有未来Perl代码中使用前者,包括个人和工作场所。但我不确定这是否值得努力。

我想到了之前看到(和编写的)没有进行此类检查的所有Perl代码。我很少看到一个模块这样做:

my ($a, $b) = @_;
defined $a or croak '$a must be defined!';
!ref $a or croak '$a must be a scalar!";
...
@_ == 2 or croak "Too many arguments!";

也许是因为没有某种辅助模块就太麻烦了,但也可能是因为实际上我们不会向函数发送过多的参数,也不会向期望标量的方法发送数组引用 - 或者如果我们这样做了,我们会使用use warnings;并很快就得到警告 - 采取的是鸭子式类型检查的方法。

那么,Perl类型检查是否值得性能损失,或者它的优点主要表现在编译、强类型语言(如C或Java)中?

我对任何有使用这些模块的Perl编写经验并从中获得了好处(或没有获得好处)的人的回答感兴趣;如果您的公司/项目有任何与类型检查相关的政策;以及类型检查和性能方面的任何问题。

更新:我最近读了一篇有趣的文章,叫做Strong Testing vs. Strong Typing。忽略了轻微的Python偏见,它基本上指出,在某些情况下,类型检查可能会令人窒息,即使您的程序通过了类型检查,也不能保证正确性 - 正确的测试是确保正确的唯一方法。

9个回答

14
如果检查参数是否正好是您所需的很重要,那么值得这样做。性能只有在功能正确时才有意义。无论能多快地得到一个错误答案或核心转储都不重要:)
现在,这听起来像是一件愚蠢的事情,但考虑一些情况,它并非如此。我真的关心这里的 @_ 中的内容吗?
sub looks_like_a_number { $_[0] !~ /\D/ }
sub is_a_dog            { eval { $_[0]->DOES( 'Dog' ) } }
在这两个例子中,如果参数不是你所期望的,你仍然会得到正确的答案,因为无效的参数不会通过测试。有些人认为这很丑陋,我可以理解他们的观点,但我也认为另一种方式同样丑陋。谁会胜出呢?
然而,有时候你需要保护条件,因为你的情况并不那么简单。接下来你要传递数据给下一个程序,它可能希望它们在某些范围内或者属于某些类型,并且不能优雅地失败。
当我思考保护条件时,我会思考如果输入是错误的会发生什么以及我对失败的关注度是多少。我必须根据每种情况的要求来判断。我知道这个回答很糟糕,但我更喜欢它胜过一个束缚和约束力的方法,在这个方法中,即使它并不重要,你也必须经历所有的混乱。
我非常不喜欢Params::Validate,因为它的代码通常比我的子例程还要长。Moose的东西非常吸引人,但你必须意识到它是一种让你声明你想要的东西的方式,你仍然可以构建手动创建的东西(你只是不必看到它或做它)。我最讨厌Perl的一件事是缺少可选方法签名,这也是Perl 6以及Moose最吸引人的特性之一。

7

我基本上同意Brian的观点。您需要担心自己的方法输入多少,这在很大程度上取决于您有多担心a)某人将输入错误数据,以及b)错误数据将破坏方法的目的。我还要补充一点,外部方法和内部方法之间存在区别。您需要更加勤奋地处理公共方法,因为您向类的消费者承诺了一个承诺;相反,您可以对内部方法不那么勤奋,因为您对访问它的代码具有更大的(理论)控制力,并且如果出现问题,只能责怪自己。

MooseX::Method::Signatures是添加一种简单的声明方式来解释方法参数的优雅解决方案。Method::Signatures::Simple和Params::Validate很好,但缺少我发现Moose最吸引人的一个功能:类型系统。我已经使用MooseX::Declare,并通过扩展MooseX::Method::Signatures进行了几个项目,我发现编写额外检查的门槛如此之低,几乎令人沉迷。


6

是的,值得这样做——防御性编程是那些总是值得做的事情之一。


4
这个问题的反驳是,在每次函数调用时检查参数是多余的,浪费 CPU 时间。支持这个观点的人认为,当所有传入的数据在首次进入系统时都经过严格检查时,内部方法就不需要参数检查,因为只有代码调用它们时才会传递通过系统边界的检查的数据,所以假设它仍然有效。
从理论上讲,我非常喜欢这种模式的声音,但我也能看到它很容易像一个纸牌屋一样倒塌,如果有人使用系统(或系统需要增长以允许使用)以一种在建立初始验证边界时无法预见的方式。只需一个外部调用内部函数,所有赌注都将失效。
在实践中,我目前正在使用 Moose,并且 Moose 实际上没有给您绕过属性级别验证的选项,此外 MooseX::Declare 处理和验证方法参数比手动展开 @_ 更轻松,因此这几乎是一个无用的观点。

2
我想在这里提到两点。 第一是测试,第二是性能问题。
1)测试
你提到测试可以做很多事情,而且测试是确保代码正确的唯一方法。总的来说,我认为这是绝对正确的。但是测试本身只解决了一个问题。
如果您编写一个模块,那么您有两个问题或者说有两个不同的人使用您的模块。
您作为开发人员和使用您的模块的用户。测试有助于第一个问题,即确保您的模块正确并执行正确的操作,但它并没有帮助只是使用您的模块的用户。
对于后者,我有一个例子。我使用Moose和其他一些东西编写了一个模块,我的代码总是出现段错误。然后我开始调试代码并寻找问题。我花费了大约4个小时的时间来查找错误。最后问题是我使用了具有Array Trait的Moose。我使用了“map”函数,但我没有提供子例程函数,只是一个字符串或其他内容。
当然,这是我的绝对愚蠢的错误,但我花费了很长时间来调试它。最后,仅仅检查输入参数是否为subref将使开发人员花费10秒钟的时间,并且会让我和可能其他人花费更多的时间。
我还知道其他例子。我已经完全使用Moose编写了REST客户端接口。最后,您总是会得到对象,您可以更改属性,但是它不会为每个更改调用REST API。相反,您更改值,最后调用update()方法传输数据并更改值。
现在我有一个用户写道: $obj->update({ foo => 'bar' })
当然,我得到了一个错误,即update()不起作用。但是它确实没有起作用,因为update()方法没有接受hashref。它只是将对象的实际状态与在线服务同步。正确的代码应该是。 $obj->foo('bar'); $obj->update();
第一件事有效,因为我从未检查过参数。如果有人给出比我期望的更多的参数,我不会抛出错误。该方法就像正常一样开始。
sub update { my ( $self ) = @_; ... }
当然,我的所有测试都绝对100%正常工作。但是处理这些不是错误的错误也花费了我的时间。它也会让用户花费更多的时间。
所以最后。是的,测试是确保您的代码正确运行的唯一正确方法。但这并不意味着类型检查是毫无意义的。类型检查是为了帮助您模块上的所有非开发人员正确使用您的模块。并节省您和其他人找到垃圾错误的时间。

2) 性能

简单来说:在你关心之前,性能并不重要。

这意味着在你的模块工作得太慢之前,性能总是足够快的,你不需要关心它。如果你的模块真的运行得太慢,你需要进行进一步的调查。但是为了这些调查,你应该使用像 Devel::NYTProf 这样的分析器来查看哪里出现了问题。

我会说,在99%的情况下,慢并不是因为你进行类型检查,而更多的是因为你的算法。你进行了很多计算,过于频繁地调用函数等等。通常,如果你使用完全不同的解决方案、使用更好的算法或者进行缓存等操作,性能就会得到提升,而不是因为你进行了类型检查。但是即使检查是导致性能问题的原因,那么就只在需要的地方删除它。

没有理由在不需要关心性能的地方进行类型检查。比如,你觉得在上面的例子中进行类型检查很有必要吗?在这里,我编写了一个 REST 客户端。99% 的性能问题都是请求发送到 Web 服务的次数或者请求所需的时间。不使用类型检查或者 MooseX::Declare 等东西可能根本不会加快速度。

即使你看到了性能上的劣势,有时候也是可以接受的。因为速度并不重要,或者因为某些东西给你带来了更大的价值。DBIx::Class 比使用 DBI 的纯 SQL 更慢,但是 DBIx::Class 为你提供了很多好处。


1

Params::Validate非常好用,但是检查参数会减慢程序速度。测试是必须的(至少在我编写的代码中)。


1

是的,这绝对值得,因为它将有助于开发、维护、调试等方面。

如果开发人员意外地向方法发送了错误的参数,将生成一个有用的错误消息,而不是将错误传播到其他地方。


0
有时候。我通常在通过哈希或哈希引用传递选项时这样做。在这些情况下,很容易记错或拼错选项名称,使用Params::Check进行检查可以节省大量烦人的故障排除时间。

例如:

sub revise {
    my ($file, $options) = @_;

    my $tmpl = {
        test_mode => { allow => [0,1], 'default' => 0 },
        verbosity => { allow => qw/^\d+$/, 'default' => 1 },
        force_update => { allow => [0,1], 'default' => 0 },
        required_fields => { 'default' => [] },
        create_backup => { allow => [0,1], 'default' => 1 },
    };

    my $args = check($tmpl, $options, 1)
      or croak "Could not parse arguments: " . Params::Check::last_error();
    ...
}

在添加这些检查之前,我会忘记使用下划线或连字符的名称,传递require_backup而不是create_backup等。而且这是我自己编写的代码 - 如果其他人要使用它,您应该绝对进行某种白痴证明。 Params :: Check 使进行类型检查,允许值检查,默认值,必需选项,将选项值存储到其他变量等变得相当容易。

0

我正在一个相当大的面向对象项目中广泛使用Moose。 Moose的严格类型检查在一些情况下帮了我大忙。最重要的是,它有助于避免将“undef”值错误地传递给方法的情况。仅在这些情况下,它就为我节省了数小时的调试时间。

性能确实会受到影响,但可以管理。使用NYTProf 2个小时帮助我找到了一些我过度使用的Moose属性,我只需重构代码,就获得了4倍的性能提升。

使用类型检查。防御性编程是值得的。

Patrick。


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