Perl 6 中子例程签名如何工作

在本系列比较 Perl 5 和 Perl 6 的第四篇文章中,了解 Perl 6 中的签名如何工作。
303 位读者喜欢这篇文章。
women programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

在比较 Perl 5 和 Perl 6 系列的第一篇文章中,我们研究了将代码迁移到 Perl 6 时可能遇到的一些问题。在第二篇文章中,我们考察了 Perl 6 中的垃圾回收机制,在第三篇文章中,我们研究了容器如何在 Perl 6 中取代引用。在第四篇文章中,我们将重点介绍 Perl 6 中的(子例程)签名,以及它们与 Perl 5 中的签名有何不同。

Perl 5 中的实验性签名

如果您正在从 Perl 5 代码迁移到 Perl 6,您可能没有使用 Perl 5.20 中提供的实验性签名功能,或者任何较旧的 CPAN 模块,如 signaturesFunction::Parameters,或 CPAN 上任何其他名称中带有 “signature” 的 Perl 5 模块。

此外,根据我的经验,原型在世界上的 Perl 程序中并不常用(例如,DarkPAN)。

由于这些原因,我将仅将 Perl 6 功能与“经典” Perl 5 参数传递的最常见用法进行比较。

Perl 5 中的参数传递

您传递给 Perl 5 子例程的所有参数都会被展平并放入子例程内部自动定义的 @_ 数组变量中。基本上,这就是 Perl 5 对子例程传递参数所做的全部工作。仅此而已。然而,Perl 5 中有几种惯用法可以从那里开始。根据我的经验,最常见的(我会说是“标准”)惯用法是

# Perl 5
sub do_something {
    my ($foo, $bar) = @_;
    # actually do something with $foo and $bar
}

这种惯用法执行列表赋值(复制)给两个(新的)词法变量。Perl 6 也支持这种访问子例程参数的方式,但它仅旨在使迁移更容易。

如果您期望固定数量的参数后跟可变数量的参数,则通常使用以下惯用法

# Perl 5
sub do_something {
    my $foo = shift;
    my $bar = shift;
    for (@_) {
        # do something for each element in @_
    }
}

这种惯用法依赖于 shift 的魔术行为,它在此上下文中从 @_ 中移位。如果子例程旨在作为方法调用,则通常会看到类似这样的情况

# Perl 5
sub do_something {
    my $self = shift;
    # do something with $self
}

因为传递的第一个参数是 Perl 5 中的 调用者

顺便说一句,这种惯用法也可以用第一个惯用法编写

# Perl 5
sub do_something {
    my ($foo, $bar, @rest) = @_;
    for (@rest) {
        # do something for each element in @rest
    }
}

但这效率较低,因为它会涉及复制一个可能很长的值列表。

第三种惯用法围绕直接访问 @_ 数组。

# Perl 5
sub sum_two {
    return $_[0] + $_[1];  # return the sum of the two parameters
}

这种惯用法通常用于小型、单行子例程,因为它是处理参数的最有效方法之一,因为不发生复制。

如果您想更改作为参数传递的任何变量,也会使用这种惯用法。由于 @_ 中的元素是指定变量的别名(在 Perl 6 中您会说:“绑定到变量”),因此可以更改内容

# Perl 5
sub make42 {
    $_[0] = 42;
}
my $a = 666;
make42($a);
say $a;      # 42

Perl 5 中的命名参数

命名参数(本身)在 Perl 5 中不存在。但是有一种常用的惯用法可以有效地模拟命名参数

# Perl 5
sub do_something {
    my %named = @_;
    if (exists %named{bar}) {
        # do stuff if named variable "bar" exists
    }
}

这通过交替从 @_ 数组中获取键和值来初始化哈希 %named。如果您使用胖逗号语法调用带参数的子例程

# Perl 5
frobnicate( bar => 42 );

它将传递两个值,“foo”42,它们将作为值 42 与键 “foo” 关联放置到 %named 哈希中。但是,如果您指定

# Perl 5
frobnicate( "bar", 42 );

=> 是自动引用左侧的语法糖。否则,它的功能就像逗号一样(因此得名“胖逗号”)。

如果子例程作为带有命名参数的方法调用,则这种惯用法与标准惯用法结合使用

# Perl 5
sub do_something {
    my ($self, %named) = @_;
    # do something with $self and %named
}

或者

# Perl 5
sub do_something {
    my $self  = shift;
    my %named = @_;
    # do something with $self and %named
}

Perl 6 中的参数传递

在最简单的形式中,Perl 6 中的子例程签名非常类似于 Perl 5 的“标准”惯用法。但它们不是代码的一部分,而是子例程定义的一部分,您不需要执行赋值

# Perl 6
sub do-something($foo, $bar) {
    # actually do something with $foo and $bar
}

对比

# Perl 5
sub do_something {
    my ($foo, $bar) = @_;
    # actually do something with $foo and $bar
}

在 Perl 6 中,($foo, $bar) 部分称为子例程的签名

由于 Perl 6 具有实际的 method 关键字,因此无需考虑调用者,因为可以使用 self 术语自动获得调用者

# Perl 6
class Foo {
    method do-something-else($foo, $bar) {
        # do something else with self, $foo and $bar
    }
}

此类参数在 Perl 6 中称为位置参数。除非另有说明,否则在调用子例程时必须指定位置参数。

如果您需要直接在 Perl 5 中使用 $_[0] 的别名行为,您可以通过指定 is rw 特征将参数标记为可写

# Perl 6
sub make42($foo is rw) {
    $foo = 42;
}
my $a = 666;
make42($a);
say $a;      # 42

当您将数组作为参数传递给子例程时,它不会在 Perl 6 中展平。您只需要在签名中接受数组作为数组

# Perl 6
sub handle-array(@a) {
    # do something with @a
}
my @foo = "a" .. "z";
handle-array(@foo);

您可以传递任意数量的数组

# Perl 6
sub handle-two-arrays(@a, @b) {
    # do something with @a and @b
}
my @bar = 1..26;
handle-two-arrays(@foo, @bar);

如果您想要 Perl 5 的(可变参数)展平语义,您可以使用所谓的“吞噬数组”来指示这一点,方法是在签名中以星号为数组添加前缀

# Perl 6
sub slurp-an-array(*@values) {
    # do something with @values
}
slurp-an-array("foo", 42, "baz");

吞噬数组只能作为签名中的最后一个位置参数出现。

如果您希望使用 Perl 5 的方式在 Perl 6 中指定参数,您可以通过在签名中指定吞噬数组 *@_ 来实现

# Perl 6
sub do-like-5(*@_) {
    my ($foo, $bar) = @_;
}

Perl 6 中的命名参数

在调用端,Perl 6 中的命名参数可以非常类似于 Perl 5 中的表示方式

# Perl 5 and Perl 6
frobnicate( bar => 42 );

但是,在子例程的定义端,情况却大相径庭

# Perl 6
sub frobnicate(:$bar) {
    # do something with $bar
}

普通(位置)参数和命名参数之间的区别在于冒号,它位于定义中的 类型符号 和变量名称之前

$foo      # positional parameter, receives in $foo
:$bar     # named parameter "bar", receives in $bar

除非另有说明,否则命名参数是可选的。如果未指定命名参数,则关联的变量将包含默认值,通常是类型对象 Any

如果您想捕获任何(其他)命名参数,您可以使用所谓的“吞噬哈希”。与吞噬数组一样,它用哈希之前的星号表示

# Perl 6
sub slurp-nameds(*%nameds) {
    say "Received: " ~ join ", ", sort keys %nameds;
}
slurp-nameds(foo => 42, bar => 666); # Received: bar, foo

与吞噬数组一样,签名中只能有一个吞噬哈希,并且它必须在任何其他命名参数之后指定。

通常,您想从具有相同名称的变量将命名参数传递给子例程。在 Perl 5 中,它看起来像这样:do_something(bar => $bar)。在 Perl 6 中,您可以以相同的方式指定它:do-something(bar => $bar)。但是您也可以使用快捷方式:do-something(:$bar)。这意味着更少的打字——以及更少的拼写错误机会。

Perl 6 中的默认值

Perl 5 具有以下惯用法,用于使带有默认值的参数成为可选参数

# Perl 5
sub dosomething_with_defaults {
    my $foo = @_ ? shift : 42;
    my $bar = @_ ? shift : 666;
    # actually do something with $foo and $bar
}

在 Perl 6 中,您可以通过指定等号和表达式来指定默认值作为签名的一部分

# Perl 6
sub dosomething-with-defaults($foo = 42, :$bar = 666) {
    # actually do something with $foo and $bar
}

如果为位置参数指定了默认值,则位置参数变为可选参数。命名参数保持可选,无论是否有任何默认值。

总结

Perl 6 提供了一种描述如何将子例程的参数捕获到该子例程的形参中的方法。位置参数由它们的名称和适当的类型符号指示(例如,$foo)。命名参数以冒号为前缀(例如,:$bar)。位置参数可以标记为 is rw,以允许更改调用者范围内的变量。

位置参数可以在吞噬数组中展平,吞噬数组以星号为前缀(例如,*@values)。意外的命名参数可以使用吞噬哈希收集,吞噬哈希也以星号为前缀(例如,*%nameds)。

默认值可以在签名内指定,方法是在等号后添加表达式(例如,$foo = 42),这会使该参数成为可选参数。

Perl 6 中的签名还有许多其他有趣的功能,除了此处总结的功能之外;如果您想了解有关它们的更多信息,请查看 Perl 6 签名对象文档

Elizabeth Mattijsen
Elizabeth Mattijsen 自 1978 年以来一直以编程为生,使用各种(现在大多已废弃的)编程语言,然后才开始使用 Perl 4 编程。1994 年,她创立了荷兰第一家商业网站开发公司,使用 Perl 5 作为主要编程语言。从 2003 年起,她参与了一项在线酒店预订服务的快速增长。

3 条评论

这真的非常清晰和有用,但我认为“Perl 5 中的命名参数”下的示例“frobnicate( bar => 42 );”然后使用 foo 进行了解释,这令人困惑……或者无论如何我在那时感到困惑。

在代码中

```
sub do_something4 {
my %named = @_;
if (exists %named{bar}) {
# 如果命名变量 "bar" 存在,则执行操作
}
}

do_something4( bar => 42 );
do_something4( "bar", 42 );
```

错误:exists 参数不是 HASH 或 ARRAY 元素或子例程,位于 ...

您完全正确。被 Perl 6 主义在 Perl 5 中抓住:那当然应该写成 "if (exists $named{bar}) {"。感谢您的发现和报告!

回复 作者 quest (未验证)

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 获得许可。
© . All rights reserved.