高级子程序技术

在“理解子程序”一文中,我讲述了子程序是什么以及为什么你想使用它们。本文在此基础上进一步讨论了一些更常见的子程序技术,使它们更加有用。

其中一些技术是高级的,但你不需要理解其他技术就可以单独使用它们。此外,并非每种技术在每种情况下都有用。就像所有技术一样,将这些视为你的工具箱中的工具,而不是每次打开编辑器都必须做的事情。

命名参数

位置参数

默认情况下,子程序使用“位置参数”。这意味着子程序的参数必须按特定顺序出现。对于参数列表较少的子程序(三个或更少的项目),这不是问题。

sub pretty_print {
    my ($filename, $text, $text_width) = @_;

    # Format $text to $text_width somehow.

    open my $fh, '>', $filename
        or die "Cannot open '$filename' for writing: $!\n";

    print $fh $text;

    close $fh;

    return;
}

pretty_print( 'filename', $long_text, 80 );

问题

然而,一旦每个人都开始使用你的子程序,它开始扩展其功能。参数列表往往会扩展,使得记住参数顺序变得越来越困难。

sub pretty_print {
    my (
        $filename, $text, $text_width, $justification, $indent,
        $sentence_lead
    ) = @_;

    # Format $text to $text_width somehow. If $justification is set, justify
    # appropriately. If $indent is set, indent the first line by one tab. If
    # $sentence_lead is set, make sure all sentences start with two spaces.

    open my $fh, '>', $filename
        or die "Cannot open '$filename' for writing: $!\n";

    print $fh $text;

    close $fh;

    return;
}

pretty_print( 'filename', $long_text, 80, 'full', undef, 1 );

快速回答——子程序末尾的1代表什么?如果你用了超过五秒钟才想出来,那么子程序调用就是不可维护的。现在,想象一下子程序不在这里,没有文档或注释,而且是由下周就要离职的人编写的。

解决方案

最可维护的解决方案是使用“命名参数”。在Perl 5中,实现这一点的最好方法是通过使用散列引用。散列也可以工作,但需要子程序作者额外的工作来验证参数列表是否正确。散列引用使任何不匹配的键立即明显地作为编译错误出现。

sub pretty_print {
    my ($args) = @_;

    # Format $args->{text} to $args->{text_width} somehow.
    # If $args->{justification} is set, justify appropriately.
    # If $args->{indent} is set, indent the first line by one tab.
    # If $args->{sentence_lead} is set, make sure all sentences start with
    # two spaces.

    open my $fh, '>', $args->{filename}
        or die "Cannot open '$args->{filename}' for writing: $!\n";

    print $fh $args->{text};

    close $fh;

    return;
}

pretty_print({
    filename      => 'filename',
    text          => $long_text,
    text_width    => 80,
    justification => 'full',
    sentence_lead => 1,
});

现在,读者可以立即清楚地看到pretty_print()调用正在做什么。

以及可选参数

通过使用命名参数,你可以获得一些或所有参数可以可选的优点,而不必强迫用户在他们不希望指定的所有位置中放置undef

验证

在Perl中进行参数验证比在其他语言中更困难。例如,在C或Java中,每个变量都有一个与之关联的类型。这包括子程序声明,这意味着尝试将错误类型的变量传递给子程序会导致编译时错误。相比之下,由于perl将所有内容都展平为单个列表,所以根本没有任何编译时检查。(好吧,几乎有,因为有原型。)

这个问题如此严重,以至于CPAN上有几十个模块来解决它。最常推荐的一个是Params::Validate

原型

Perl中的原型是一种让Perl在编译时知道对特定子程序期望什么的方法。如果你曾尝试将数组传递给内置的vec()并看到vec()参数不足,你就遇到了原型。

在很大程度上,原型带来的麻烦比它们的价值要大。一方面,Perl不检查方法的原型,因为这需要能够在编译时确定哪个类将处理该方法。因为你可以在运行时改变@ISA——你看到了问题。然而,主要原因是因为原型不够智能。如果你指定了sub foo ($$$),你不能传递一个包含三个标量的数组给它(这是vec()的问题)。相反,你必须说foo( $x[0], $x[1], $x[2] ),这很痛苦。

原型可以非常有用,原因之一是能够将子程序作为第一个参数传递。Test::Exception正是利用了这一点。

sub do_this_to (&;$) {
    my ($action, $name) = @_;

    $action->( $name );
}

do_this_to { print "Hello, $_[0]\n" } 'World';
do_this_to { print "Goodbye, $_[0]\n" } 'cruel world!';

上下文感知

使用内置函数 wantarray,子例程可以确定其调用上下文。在 Perl 中,子例程的上下文有以下三种之一——列表上下文、标量上下文或空上下文。列表上下文表示返回值将用作列表,标量上下文表示返回值将用作标量,空上下文表示返回值将不被使用。

sub check_context {
    # True
    if ( wantarray ) {
        print "List context\n";
    }
    # False, but defined
    elsif ( defined wantarray ) {
        print "Scalar context\n";
    }
    # False and undefined
    else {
        print "Void context\n";
    }
}

my @x       = check_context();  # prints 'List context'
my %x       = check_context();  # prints 'List context'
my ($x, $y) = check_context();  # prints 'List context'

my $x       = check_context();  # prints 'Scalar context'

check_context();                # prints 'Void context'

对于实现或增强上下文感知的 CPAN 模块,请查看 Contextual::ReturnSub::ContextReturn::Value

注意:您可以通过在标量上下文和列表上下文中调用子例程时执行完全不同的操作来严重误用上下文感知。不要这样做。子例程应该是一个单一、易于识别的工作单元。并非所有人都理解上下文的所有不同排列组合,包括您的标准 Perl 专家。

相反,我建议有一个标准的返回值,除非是空上下文。如果您的返回值计算成本高昂且仅用于返回目的,那么知道您是否处于空上下文可能非常有帮助。然而,这可能是一种过早优化,因此始终在优化前后进行测量(基准测试和性能分析),以确保您正在优化需要优化的内容。

模仿 Perl 内部函数

许多 Perl 内部函数修改它们的参数,并在没有提供参数时使用 $_@_ 作为默认值。一个完美的例子是 chomp()。以下是一个 chomp() 的版本,它演示了一些这些技术

sub my_chomp {
    # This is a special case in the chomp documentation
    return if ref($/);

    # If a return value is expected ...
    if ( defined wantarray ) {
        my $count = 0;
        $count += (@_ ? (s!$/!!g for @_) : s!$/!!g);
        return $count;
    }
    # Otherwise, don't bother counting
    else {
        @_ ? do{ s!$/!!g for @_ } : s!$/!!g;
        return;
    }
}
  • 如果想要返回空值,请使用 return; 而不是 return undef;。如果有人将返回值赋给数组,则后者将创建一个包含一个值(undef)的数组,该值求值为真。前者将正确处理所有上下文。
  • 如果您在没有参数的情况下想要修改 $_,则必须显式检查 @_。您不能做类似 @_ = ($_) unless @_ 的事情,因为 $_ 将会失去其魔法。
  • 除非 $count 有用(使用空上下文的检查),否则不会计算 $count
  • 关键是 @_ 的别名。如果您直接修改 @_(而不是将 @_ 中的值赋给变量),那么您将修改传递的实际参数。

结论

我希望我已经向您介绍了工具箱中的一些新工具。编写良好子例程的艺术非常复杂。我所展示的每一项技术都是程序员工具箱中的一个工具。就像一位熟练的木匠不会在每个项目中都使用钻头一样,一位熟练的程序员也不会使每个子例程都使用命名参数或模仿内置函数。您必须每次评估这些技术,看它们是否会使代码更易于维护。过度使用这些技术会使您的代码 更难 维护。恰当地使用它们会使您的生活更加轻松。

标签

反馈

这篇文章有什么问题吗?通过在 GitHub 上打开问题或拉取请求来帮助我们。