Perl5学习笔记-第八章-子程序
2010-07-03 15:12:06 阿炯

一、定义
二、调用
   1、用&调用
   2、先定义后调用
   3、前向引用
   4、用do调用
三、返回值
四、局部变量
五、子程序参数传递
   1、形式
   2、传送数组
六、递归子程序
七、用别名传递数组参数
八、预定义的子程序

一、定义
函数可分为系统函数和用户函数。子程序即执行一个特殊任务的一段分离的代码,它可以使减少重复代码且使程序易读。Perl中,子程序可以出现在程序的任何地方。定义方法为:
sub subroutine{
    statements;
}

函数是一个命名的代码块,通常是用于处理指定到的输出值的输入值,虽然这并不总是这样。例如,打印函数需要变量和静态文本,并打印在屏幕上的值。
像许多的其它语言,Perl提供了为用户自定义子程序。
这些可以位于在主程序中的任何地方。
通过从其他文件加载,要求(require),也可以使用关键字。
任何传入的参数显示在数组@_。
return语句可用于退出子程序。
如果没有返回,如果最后的语句是一个表达式,那么就返回它的值。
如果最后语句是像一个foreach或while循环控制结构,返回的值是不确定的。
子程序可以返回标量,数组或哈希。

子程序,如变量,可以声明(不用定义他们来做了什么)或声明和定义。简单地声明一个子程序,可以使用下列形式之一:
sub NAME
sub NAME PROTO
sub NAME ATTRS
sub NAME PROTO ATTRS

其中,NAME是您所创建的子程序的名称,Proto是期望的子程序调用时的参数的原型,ATTRS是一个属性的子程序呈现。

如果想声明和定义一个函数,那么就需要包括块定义其操作:
sub NAME BLOCK
sub NAME PROTO BLOCK
sub NAME ATTRS BLOCK
sub NAME PROTO ATTRS BLOCK

还可以创建匿名函数 - 没有名字的子程序省略NAME组件:
sub BLOCK
sub PROTO BLOCK
sub ATTRS BLOCK
sub PROTO ATTRS BLOCK

要调用一个函数,你可以使用下列形式之一:
NAME
NAME LIST
NAME (LIST)
&NAME

二、调用
一个子程序或语句的上下文中被定义为预期的返回值的类型。这使您可以使用一个单一的函数,返回不同的值,根据用户期望接收。例如,下面的两个调用的getpwent函数返回一个列表或一个标量,根据分配使用:
$name = getpwent();
($name, $passwd, $uid, $gid, $quota,$comment, %gcos, $dir, $shell) = getpwent();

在第一种情况下,用户的期望由该函数返回一个标量值,因为返回值将被赋值。在第二种情况下,用户期望的阵列作为返回值再次由于已被指定为要插入到的信息的标量的列表。

下面是另一个例子,从内置Perl函数显示灵活性:
my $timestr = localtime(time);

在这个例子中,当前的日期和时间所组成的字串$timestr的值。例如,2018年11月30日15时21分33秒,星期四。相反的:
($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);

现在单个变量包含相应的值返回的本地时间。

调用方法如下:
1、用&调用
&subname;
...
sub subname{
...
}

2、先定义后调用 ,可以省略&符号
sub subname{
...
}
...
subname;

建议采取这种方式。

3、前向引用 ,先定义子程序名,后面再定义子程序体
sub subname;
...
subname;
...
sub subname{
...
 }
 
4、用do调用
do my_sub(1, 2, 3);等价于&my_sub(1, 2, 3);

再来看do语句块结构

do语句块结构如下:do {...}

do语句块像是匿名子程序一样,没有名称,给定一个语句块,直接执行。且和子程序一样,do语句块的返回值都是最后一个执行的语句的返回值。

例如将使用if-elsif-else结构进行赋值的行为改写成do。以下是if-elsif-else结构:
my $name;
if($gender eq "male"){
    $name="FreeOA";
}elsif($gender eq "female"){
    $name="IvySong";
}else{
    $name="Other";
}

改写成do结构:
my $name=do{
    if($gender eq "male"){"FreeOA"}
    elsif($gender eq "female") {"IvySong"}
    else {"Other"}
};    # 注意结尾的分号

在perl中,使用表达式修饰符改写流程控制结构的时候,控制符左边只能写一个语句。例如下面的if,左边有了print后就不能再有其它语句。
print "..." if(...);

使用do结构,可以将多个语句包围,然后执行:
#!/usr/bin/perl
use 5.010;
$a=3;
do {
    say "statement1";
    say "statement2";
} if $a > 2;

因为do有自己的代码块,有时候可以在这个代码块中使用自己的私有变量。例如读取一个文件,将文件中的内容赋值给一个变量。
my $file_content = do {
    local $/;
    local @ARGV = ("/tmp/fa.txt");
    <>;
};

或者:
my $file_content = do {
    local $/;
    open my $fh,'<',"/tmp/fa.txt" or die;
    <$fh>;
};


三、返回值
缺省的,子程序中最后一个语句的值将用作返回值。语句return (retval);也可以推出子程序并返回值retval,retval可以为列表。

四、局部变量
子程序中局部变量的定义有两种方法:my和local。其区别是:my定义的变量只在该子程序中存在;而local定义的变量不存在于主程序中,但存在于该子程序和该子程序调用的子程序中(在Perl4中没有my)。定义时可以给其赋值,如:
my($scalar) = 43;
local(@array) = (1, 2, 3);
 
五、子程序参数传递
函数参数第一个参数传递给子程序是在函数的$_[0],第二个参数是$_[1]等等。

1、形式
&sub1(&number1, $number2, $nubmer3);
  ...
sub sub1{
    my($number1, $number2, $number3) = @_;
    ...
}
 
2、传送数组
&addlist (@mylist);
&addlist ("14", "6", "11");
&addlist ($value1, @sublist, $value2);
  ...
sub addlist {
    my (@list) = @_;
    ...
}
 
参数为数组时,子程序只将它赋给一个数组变量。如
sub twolists {
 my (@list1, @list2) = @_;
}
中@list2必然为空。但简单变量和数组变量可以同时传递:
&twoargs(47, @mylist); # 47赋给$scalar,@mylist赋给@list
&twoargs(@mylist); # @mylist的第一个元素赋给$scalar,其余的元素赋给@list
  ...
sub twoargs {
 my ($scalar, @list) = @_;
 ...
}

将列表传递给子程序

因为@_变量是一个数组,它可以被用来提供列表子程序。然而,因为在其中Perl接受并解析列表和数组的方式,它可以是难以提取从@_的单个元素。以下是有效的。
mysub(1,2,3);
@args = (2,3);

mysub(1,@args);
@args = (1,2,3);

mysub(@args);

最后当我们收到的值@_变量,我们可以不承认如果我们通过一个数组或两个值数组,因为它最后要合并成一个。如果你想使用并确定传递给Perl的个别列表,那么你需要使用参考:
(@listc, @listd) = simplesort(\@lista, \@listb);

开头的\字符告诉Perl提供参考,指针,数组的。 引用是实际上只是一个标量,因此我们可以找出每个列表分配给每个阵列在我们的子程序。
 
六、递归子程序
Perl中的子程序可以互相调用,其调用方法与上述相同,当调用该子程序本身时,即成了递归子程序。递归子程序有两个条件:1、除了不被子程序改变的变量外,所有的变量必须的局部的;2、该子程序要含有停止调用本身的代码。

七、用别名传递数组参数
1、用前面讲到的调用方法&my_sub(@array)将把数组@array的数据拷贝到子程序中的变量@_中,当数组很大时,将会花费较多的资源和时间,而用别名传递将不做这些工作,而对该数组直接操作。形式如:
@myarray = (1, 2, 3, 4, 5);
&my_sub(*myarray);
sub my_sub {
 my (*subarray) = @_;
}
 
2、此方法类似于C语言中的传递数组的起始地址指针,但并不一样,在定义数组的别名之后,如果有同名的简单变量,则对该变量也是起作用的。如:
$foo = 26;
@foo = ("here's", "a", "list");
&testsub (*foo);
...
sub testsub {
  local (*printarray) = @_;
  ...
  $printarray = 61;
}
当子程序执行完,主程序中的$foo的值已经成了61,而不再是26了。
 
3、用别名的方法可以传递多个数组,如:
@array1 = (1, 2, 3);
@array2 = (4, 5, 6);
&two_array_sub (*array1, *array2);
sub two_array_sub {
 my (*subarray1, *subarray2) = @_;
}
在该子程序中,subarray1是array1的别名,subarray2是array2的别名。

八、预定义的子程序
Perl5预定义了三个子程序,分别在特定的时间执行,它们是:BEGIN子程序在程序启动时被调用;END子程序在程序结束时被调用;AUTOLOAD子程序在找不到某个子程序时被调用。你可以自己定义它们,以在特定时间执行所需要的动作。如:
BEGIN {
 print("Hi! Welcome to Perl!\n");
}
AUTOLOAD{
 print("subroutine $AUTOLOAD not found\n"); # 变量$AUTOLOAD即未找到的子程序名
 print("arguments passed: @_\n");
}

若同一个预定义子程序定义了多个,则BEGIN顺序执行,END逆序执行。

九、子程序(subroutine)小结

1.perl中的子程序其实就是自定义函数,它使用sub关键字开头,表示声明一个子程序
2.子程序名称有独立的名称空间,不会和其它名称冲突
3.Perl中的子程序中可以定义、引用、修改全局变量,这和几乎所有的语言都不同。当然,也可以定义局部变量
4.perl中使用&SUB_NAME()的方式调用SUB_NAME子程序,&有时候可以省略,括号有时候也可以省略。

示例:
sub mysub{
    $n += 1;
    print "\$n is: $n","\n";
}
&mysub;
print "\$n is: $n","\n";

perl的子程序最后一个执行的语句的结果或返回值就是子程序的返回值(是最后一个执行的语句,不是最后一行语句)。所以子程序总会有返回值,但返回值并不总是有用的返回值。
sub mysub{
    $n += 2;
    print "hello world\n";
    $n + 3;
}
$num = &mysub;

这里的&mysub有返回值,它的返回值是最后执行的语句$n+3,并将返回值赋值给$num,所以$num的值为5。如果把上面的print作为最后一句:
sub mysub {
    $n += 2;
    $n + 3;    # 这不是返回值
    print "hello world\n";    # 这是返回值
}
$num = &sub;

上面的print才是子程序的返回值,这个返回值是什么?这是一个输出语句,它的返回值为1,表示成功打印。这个返回值显然没什么用。所以子程序最后执行的语句一定要小心检查。那上面的$n+3的结果是什么?它的上下文是void context,相加的结果会被丢弃。

子程序还可以返回列表:
($m,$n)=(3,5);
sub mysub{
    $m..($n+1);
}
@arr=&mysub;
print @arr;    # 输出3456

如果想在子程序的中间退出子程序,就可以使用return子句来返回值。当然,如果这个返回语句是最后一行,就可以省略return
# 返回一个偶数
$n=20;
sub mysub{
    if ($n%2 == 0){
        return $n;
    }
    $n-1;    # 等价于 return $n-1;
}
$oushu=&mysub;
print $oushu,"\n";

子程序参数

Perl子程序除了可以直接操作全局变量,还可以传递参数。例如:
&mysub(arg1,arg2...);
&mysub arg1,arg2...;

至于什么时候可以省略括号,后面再解释。
1.调用子程序时传递的参数会传入子程序,它们存储在一个特殊的数组变量@_中;
2.既然@_是数组,就可以使用$_[index]的方式引用数组中的各元素,也就是子程序的各个参数;
3.@_数组只在每个子程序执行期间有效,每个子程序的@_都互不影响。子程序执行完成,@_将复原为原来的值。

# 返回最大值
@_=qw(perl python shell);    # 测试:先定义数组@_
sub mysub{
    if($_[0] > $_[1]){
        $_[0];
    }else{
        $_[1];
    }
}
$max = &mysub(10,20);
print $max,"\n";
print @_;    # 子程序执行完,@_复原为qw(perl python shell)

如果上面的子程序给的参数不是两个,而是3个或者1个(&mysub(10,20,30);、&mysub(10);)会如何?因为参数是存在数组中的,所以给的参数多了,子程序用不到它,所以忽略多出的参数,如果给的参数少了,那么缺少的那些参数被引用时,值将是undef。所以在逻辑上来讲,参数多少不会影响子程序的错误,但结果可能会受到一些影响。但可以在子程序中判断子程序的参数个数:
if(@_ != 2){# 如果参数个数不是2个,就退出
    return 1;
}

这里的比较是标量上下文,@_返回的是参数个数。

调用子程序的几种方式分析

一般情况下,调用我们自定义的子程序时,都使用&符号,有时候还要带上括号传递参数。
&mysub1();
&mysub2;
&mysub3 arg1,arg2;
&mysub4(arg1,arg2);

但有时候,&符号和括号是可以省略的。主要的规则是:
1.只有子程序定义语句在子程序调用语句前面,才可以省略括号
2.当使用了括号,perl已经知道这就是一个子程序调用,这时可以省略&也可以不省略&
3.不能省略&的情况比较少。基本上,只要子程序名称不和内置函数同名,或者有特殊需求时(如需要明确子程序的名称时,如defined(&mysub)),都可以省略&
4.不能在使用了&、有参数传递的情况下,省略括号
5.最安全、最保险的调用方式是:
5.1.有参数时:&subname(arg1,arg2),即不省略&和括号
5.2.无参数时:&subname
5.3.使用&的调用方式是比较古老的行为,虽然安全。但直接使用括号调用也基本无差别,但却更现代,所以建议用func()的方式调用自定义的子程序

sub mysub{
    print @_,"\n";
}

# 先定义了子程序
mysub;    # 正常调用
mysub();    # 正常调用
mysub("hello","world3");    # 正常调用
mysub "hello","world4";    # 正常调用
&mysub;    # 安全的调用
&mysub("hello","world6");    # 安全的调用
&mysub "hello","world7";    # 本调用错误,因为使用了&,且有参数

上面是先定义子程序,再调用子程序的;下面是先调用子程序,再定义子程序的。

mysub;    # 本调用无括号,不报错,当做内置函数执行,但无此内置函数,所以忽略
mysub();    # 有括号,不报错
mysub("hello","world3");    # 有括号,不报错
mysub "hello","world4";    # 无括号,本调用错误
&mysub "hello","world7";    # 本调用错误
&mysub;    # 安全的调用
&mysub("hello","world6");    # 安全的调用

sub mysub{
    print @_,"HELLO","\n";
}

如果子程序名称和内置函数同名,则不安全的调用方式总会优先调用内置函数。

子程序中的私有变量:my关键字

my关键字可以声明局部变量、局部数组。它可以用在任何类型的语句块内,而不限于sub子程序中。

sub mysub {
    my $a=0;    # 一次只能声明一个局部目标
    my $b;    # 声明变量,初始undef
    my @arr;    # 声明空数组
    my($m,$n);    # 一次可以声明多个局部目标
    ($m,$n)=@_;    # 将函数参数赋值给$m,$n
    my($x,$y) = @_;    # 一步操作
}

foreach my $var (@arr){# 将控制变量定义为局部变量
    my($sq) = $_ * $_;    # 在foreach语句块内定义局部变量
}

在my定义局部变量的时候,需要注意列表上下文和标量上下文:
@_=qw(perl shell python);
my   $num = @_;    # 标量上下文
my (@num) = @_;    # 列表上下文
print $num,"\n";    # 返回3
print @num,"\n";    # 返回perlshellpython

在Perl中除了my可以修饰作用域,还有local和our也可以修饰作用域,它们之间的区别参见:Perl的our、my、local的区别

state关键字

my关键字是让变量、数组、哈希私有化,state关键字则是让私有变量、数组、哈希持久化。注意两个关键字:私有,持久化。

使用state关键字声明、初始化的变量对外不可见,但对其所在子程序是持久的:每次调用子程序后的变量值都保存着,下次再调用子程序会继承这个值。该特性是perl 5.10版才引入的,所以必须加上use 5.010;语句才能使用该功能。
use 5.010;
$n=22;
sub mysub {
    state $n += 1;
    print "Hello,$n\n";
}
&mysub;    # 输出Hello,1
print $n,"\n";    # 输出22
&mysub;    # 输出Hello,2
print $n,"\n";    # 输出22

当然,如果在子程序中每次都用state将变量强制初始化,那么这个变量持久与否就无所谓了,这时用my关键字的效果是一样的。
use 5.010;
sub mysub{
    state $n=0;
    print "hello,$n","\n";
}
&mysub;
&mysub;
&mysub;

state除了可以初始化变量,还可以初始化数组和hash(v5.16之后);但初始化数组和hash的时候有限制:不能在列表上下文初始化。
use 5.010;
sub mysub {
    state $n;    # 初始化为undef
    state @arr1;    # 初始化为空列表()
    state @arr2 = qw(perl shell);  # 错误,不能在列表上下文初始化
}

perl作用域初探

因为perl中支持先定义子程序再调用,也支持先调用再定义的方式。不同的调用方式有可能会有区别,例如:
#!/usr/bin/env perl -w
use strict;

# do_stuff(1);    # (1).在定义的前面
{
    my $last = 1;
    sub do_stuff{
        my $arg = shift;
        print $arg + $last;
    }
}
do_stuff(1);    # (2).在定义的后面

上面在不同的位置调用子程序do_stuff(1),但只有第二种方式是正确的,第一种方式是错误的。

原因在于my定义的变量是词法作用域变量,先不用管词法作用域是什么。只需要知道my定义的变量是在程序编译期间定义的好的,但是赋值操作是在程序执行期间进行的。而子程序sub的名称do_stuff是无法加my关键字的,所以perl中所有的子程序都是全局范围可调用的。子程序的调用是程序执行期间的。

所以上面的程序中,整个过程是先在编译期间定义好词法变量$last(但未赋值初始化),子程序do_stuff。然后开始从程序头部往下执行:

当执行到(1)的位置处也就是调用子程序,这个子程序中引用了变量$last,但$last至今未赋值,所以会报变量未初始化的错误。

如果没有(1),那么从上往下执行将首先执行到my $last = 1,这表示为已在编译期间定义好的变量赋值。然后再继续执行到(2)调用子程序,但子程序引用的$last已经赋值初始化,所以一切正常。

在perl中的子程序是在编译期间定义好的,还是执行期间临时去定义的,目前还不是太确定,按照perl的作用域规则,它应该是在执行期间临时去定义的。但无论如何,它先定义还是后定义,都不影响对变量作用域的判断。

在函数中对数组引用进行修改

use v5.20;
use Data::Dump 'pp';
#对数组中的元素进行修改并增加
sub subname{
    my $aref = shift;
    say "$$aref[0]\t${$aref}[1]";
    #@$aref[2..3]=qw(z x);
    @{$aref}[2..3]=('z','x');
}

my @arr = qw(a b c);
subname(\@arr);

pp(\@arr);