原型子程序和函数签名

通常情况下,使用如下方式定义子程序:

sub subname{
  BODY
}

这种方式定义的子程序参数是很自由的,可以随意传递参数到子程序,它们都会保存到@_,参数相关的处理需要操作@_,也即在运行时处理参数。

Perl还支持定义原型子程序,定义原型子程序要求在子程序名称后加上括号,括号内可能包含一些特殊的用于表示原型的元字符。

例如:

sub proto_sub($){
  say "@_";
}

该原型子程序要求调用proto_sub时,需要传递一个且必须是一个标量参数(严格来说是进入标量上下文,会转换为标量数据)。

定义原型子程序后,在编译期间,会检查所有调用原型子程序的代码,检查调用原型子程序时传递的参数是否符合原型定义。如果不符合,将编译错误(编译错误而非运行时错误)。

因此,在一定程度上,原型可以作为限制子程序的参数类型、参数数量的一种手段。它有点类似于函数签名,但不同于函数签名。在后面,将介绍Perl中支持的函数签名功能。

需要注意,以sigil前缀&调用子程序时会跳过编译期间的原型检测。因此,如非必要,否则不要使用&subname的方式调用原型子程序。

原型字符

Perl的原型由几种具有特殊意义的字符来表示。例如,下面是官方手册中(perldoc perlsub)给出的原型子程序定义示例:

sub mylink ($$)	   mylink $old, $new
sub myvec ($$$)	   myvec $var, $offset, 1
sub myindex ($$;$)	   myindex &getstring, "substr"
sub mysyswrite ($$$;$)  mysyswrite $buf, 0, length($buf) - $off, $off
sub myreverse (@)	   myreverse $a, $b, $c
sub myjoin ($@)	   myjoin ":", $a, $b, $c
sub mypop (+)	   mypop @array
sub mysplice (+$$@)	   mysplice @array, 0, 2, @pushme
sub mykeys (+)	   mykeys %{$hashref}
sub mygrep (&@)	   mygrep { /foo/ } $a, $b, $c
sub myrand (;$)	   myrand 42

其中:

  • $表示该参数处于一个标量上下文中

    因此,sub mylink($$)表示要传递两个参数,这两个参数都会转换为标量数据

  • @%都表示吞掉剩余所有参数,并进入列表上下文

  • \$\@\%分别表示对应的参数必须以$@%开头,即必须传递标量变量(而不是字面量标量)、数组变量和hash变量

    • \$更特殊一些,它除了可以传递$标量变量,还可以传递任意标量左值,例如$foo = 7f()->[0]
    • 实际上,\X表示该参数必须以X开头,支持的反斜线原型包括\$ \@ \% \& \*
    # 必须传递两个参数,第一个参数会被转换为标量数据
    # 第二个参数必须是'@xxx'格式的数组变量
    sub f($\@){}
    
  • 可使用\[]对反斜线原型进行归纳。例如\[$@%]表示该参数可以传递$xx@xx%xx的任意一种

    sub f(\[%@$&]){}
    # 允许的调用方式:
    f %x;
    f @x;
    f $x;
    f &x;
    
  • ;用于分隔强制参数和可选参数,分号左边的参数不可省略,右边的参数可省略。显然,@%前使用;没有意义,因为它们表示的列表本身就允许是空列表

    # 必须传递2个或3个参数,第三个参数可选
    sub f($$;$){}
    
  • +表示该参数可以是数组变量或数组引用,或者是hash变量或hash引用

    • 无论该参数传递的是数组变量还是数组引用,都会自动转换为数组引用,同理hash变量也会转换为hash引用

    • 使用该原型时,有必要在代码体中检测引用的类型是数组引用还是hash引用

      sub f(+){say "@_"}
      my @arr=(1,2,3);
      f @arr;   # ARRAY(0x557c8cb6ba38)
      f \@arr;  # ARRAY(0x557c8cb6ba38)
      
  • &表示该参数是一个匿名子程序或子程序引用,当它作为第一个参数时,可省略sub关键字以及该参数后的逗号分隔符。这是一个非常有趣的语法,甚至它可以实现新的Perl语法,比如可以实现类似于grep {} @arr的语句块用法,在perldoc persub中也给出了通过该功能实现try{}catch{}语法

    # 类似于map的功能
    sub f(&@){
      my $sub_ref = shift;
      my @arr=();
      for (@_){ push @arr, $sub_ref->($_); }
      return @arr;
    }
    
    my @arr = f { $_ * 2 } 11,22,33;
    say "@arr";