函数签名

Perl支持函数签名的功能,目前函数签名还是实验阶段的功能,未来可能会发生改变。

要使用函数签名,需要先开启签名特性。且默认会给出该实验特性的警告,可禁用该警告。

use feature 'signatures';
no warnings 'experimental::signatures';

例如:

use feature 'signatures';
no warnings 'experimental::signatures';
sub f($num1, $num2){
  return $num1 + $num2;
}
say f(3, 5);

这表示将调用子程序f(3,5)时传递的两个参数分别保存到标量变量$num1 $num2中,这两个标量变量是该子程序的词法变量,只在该子程序作用域内可见。

如果调用子程序时传递的参数(数量)和函数签名不一致,将报错。

上面使用函数签名的子程序,其功能等价于下面手动处理参数的子程序:

sub f{
  die "arguments too long" if @_ > 2;
  die "arguments too short" if @_ < 2;
  my $num1 = $_[0];
  my $num2 = $_[1];
  return $num1 + $num2;
}

最简单的函数签名是没有任何参数的签名。如下(注意小括号不能省略):

use feature 'signatures';
no warnings 'experimental::signatures';
sub f(){}

这表示调用子程序f时,不能传递任何参数。

函数签名和原型

Perl函数签名和子程序的原型类似,都可以在一定程度上检测参数,且定义子程序时都使用小括号。

为了避免产生歧义,在开启了use feature 'signatures'时可明确使用:prototype属性来声明原型部分。并且,函数签名和原型可以同时存在。例如:

use feature 'signatures';
no warnings 'experimental::signatures';

# 开启了`use feature 'signatures';`后,
# 使用prototype属性定义原型
sub foo :prototype($) {
  return $_[@];
}

# 下面的子程序定义中出现两个小括号是合法的
sub bar :prototype($$) ($left, $right) {
  return $left + $right;
}

函数签名和原型有很大不同:

  • 原型是在编译期间,对子程序调用传递的参数进行检查
  • 函数签名是在运行期间,像其他编程语言一样处理函数参数,而不再需要手动操作@_来管理参数。它在一定程度上也能检测参数

这里需要注意的是,和其他语言不一样,Perl的函数签名是在运行期间(子程序被调用准备开始执行时)生效,而不是在编译期间生效。

函数签名和@_

虽然使用函数签名时,perl会自动将传递的参数赋值给签名中指定的变量。但函数签名和@_不冲突,使用函数签名时,仍然可以去操作@_

实际上,函数签名处理的参数和@_中保存的参数是有区别的:

  • 函数签名的设置保存的变量是参数的拷贝,函数内修改参数变量不会影响外部数据
  • @_中保存的参数是外部数据的别名引用,函数内修改@_内的元素会影响外部数据

因此,函数签名设置的参数变量,等价于手动将@_内的元素的值赋值给了函数内的私有变量。

例如:

use feature 'signatures';
no warnings 'experimental::signatures';
sub ff($a, $b){
  $a++;
  $_[1]++;
  say "in routine: $a, $_[1]";
}

my ($x, $y)=(33, 44);
ff($x, $y);
say "outer data: $x, $y";

输出结果:

in routine: 34, 45
outer data: 33, 45

函数签名的用法细节

定义子程序时,函数签名的小括号总是书写在代码体大括号的前面。函数前面的前面可能是子程序名称、原型或子程序属性定义。总之,函数前面之后必须紧跟着大括号代码体。例如:

sub f($left, $right){
  return $left+$right;
}

当调用带有函数签名的子程序时,总是先处理签名部分:将调用子程序时传递的参数赋值给签名中指定的参数变量。如果参数处理成功,则开始进入执行子程序的代码体,如果签名参数处理失败(比如参数数量不合理),将报错。

位置参数

函数签名中,使用$开头的标量变量是位置参数。调用子程序传递参数时,会根据签名中的参数书写位置决定如何赋值。

例如,下面的函数签名表示接收两个参数,分别赋值给私有的标量变量$left $right

sub f($left, $right){
  return $left+$right;
}

位置参数是强制的,函数签名中有几个位置参数(形参),调用子程序时就必须传递几个实参数据。

如果想忽略传递的某个参数,使用不带变量名的$符号即可:

sub ff($first, $, $third){
  say "$first, $third";
}

虽然ff()的第二个参数被忽略,但调用ff子程序时,也必须传递该位置参数。也即调用ff()时,必须要有三个参数。

可选参数:默认值参数

可以为位置参数设置默认值,带有默认值的参数也被称为非必须的可选参数:

sub ff($num1, $num2=10){
  return $num1 + $num2;
}

此时调用ff子程序可以传递两个参数,也可以只传递一个参数,该参数将被分配给$num1,同时$num2采用默认值10。

注意,参数默认值是在调用子程序时处理参数的运行期间进行评估计算的,因此,某个参数的默认值可以是它前面的参数变量。另一方面,参数默认值只在缺失该参数时才评估。例如:

# 如果只传递一个参数,则默认$num2等价于$num1
sub ff($num1, $num2=$num1){
  return $num1 + $num2;
}

# 如果传递两个参数,则不进行自增操作
my $auto_id = 0;
sub fff($name, $id=$auto_id++){
  return "$name, $id";
}

可选参数(即默认值参数)必须放在位置参数的后面:

sub f($one, $two, $three=3){}

# 错
sub f($one, $two=2, $three){}

可使用多个默认值参数,此时将根据位置对应分配:

sub f($one, $two, $three=3, $four=4){}
f(11,22,33);  # $three=33,$four=4

数组或hash:吸收多个参数

函数签名中可使用@前缀的参数变量,表示定义一个私有的数组变量,并吸收剩余所有参数,将这些参数保存到该数组变量中。

sub f($one, $two, @others){}
f(1,2,3,4,5,6);
#=> @others=(3,4,5,6,7)

上面的子程序中,如果只传递两个参数,则@others数组为空数组。

函数签名中的@也可以不跟名称,这样可以取消调用子程序时的参数数量,同时又无需处理多余参数:

# 可以传递两个或两个以上任意多的参数
sub f($one, $two, @){}

还可以使用%前缀的参数变量,表示定义一个私有的hash变量,并吸收剩余所有参数,将这些参数成对地保存到hash变量中。如果吸收的参数数量是奇数,则报错。将剩余参数保存为hash时,所有作为key的参数都将被转换为字符串,且如果key相同,则后出现的覆盖先出现的键值对。

例如:

sub f($one, %persons){}

同样,函数签名中的%可以不带名称,此时它可以用来检测它所吞掉的剩余参数数量是否是偶数个:

sub f($one, %){}

由于@%会吞掉剩余所有参数,因此在函数签名中,它们必须放在签名的最后面。