perl语言编程 第六章 子过程 上
上一篇 / 下一篇 2008-04-18 11:51:07 / 个人分类:关于脚本语言
象其他的语言一样,Perl 也支持自定义的子过程.(注:我们也把它们叫做函数,不过函数和子过程在 Perl 里是一样的东西.有时候我们甚至叫它们方法,方法和函数或子过程是同样的方式定义的,只是调用方式不同.)这些子过程可以在主程序中的任何地方定义,也可以用 do,require 或 use 关键字从其他文件中加载.或者直接使用 eval 在运行的时候产生.你甚至可以使用第十章"包"中"自动装载"一节描述的机制在运行时加载它们.你可以间接调用子过程,使用一个包含该子过程名字或包含指向该子过程引用的变量来调用,或者通过对象,让对象决定调用哪个子过程.你可以产生只能通过引用使用的匿名子过程,如果必要,你还可以通过闭合,用匿名子过程克隆几乎相同的函数.我们将在第八章"引用"中的相关小节中讲述.
1.0 语法
声明一个命名子过程,但不定义它,使用下面的形式:
subNAMEsubNAME PROTOsubNAME ATTRSsubNAME PROTO ATTRS
声明并且定义一个命名子过程,加上一个 BLOCK:
subNAME BLOCKsubNAME PROTO BLOCKsubNAME ATTRS BLOCKsubNAME PROTO ATTRS BLOCK
创建一个匿名子过程或子句,把 NAME 去掉就可以:
subBLOCKsubPROTO BLOCKsubATTRS BLOCKsubPROTO ATTRS BLOCK
PROTO 和 ATTRS表示原型和属性,分别将在本章下面的章节中讨论.相对于 NAME 和 BLOCK 它们并不很重要.NAME 和 BLOCK 是基本部分,甚至有时候它们也可以省略.
对于没有 NAME 的形式,你还必须提供调用子过程的方法.因此你必须保存返回值,因为这种形式的 sub 声明方法不但在编译的时候编译,同时也产生一个运行时的返回值,所以我们就可以保证保存它:
$subref = sub BLOCK;
可以用下面的方法引入在另一个模块中定义的子过程:
use MODULE qw(NAME1 NAME2 NAME2...)
直接调用子过程可以用下面的方法:
NAME(LIST) # 有圆括弧时 & 是可选的
NAME LIST # 如果预声明/输入了子过程,那么圆括弧是选的
&NAME # 把当前的 @_ 输出到该子过程
#(并且绕开原型).间接调用子过程(通过名字或引用),可以使用下面的任何一种方法:
- &$subref(LIST) # 在间接调用的时候,& 不能忽略
- $subref->(LIST) # (除非使用中缀表示法)
- &$subref # 把当前的 @_ 输出到该子过程
正式情况下,一个子过程的名字包括 & 前缀,一个子过程可以使用 & 前缀调用,但通常情况下 & 是可选的,如果预先定义了子过程,那么圆括弧也是可选的.但是,在只使用子过程名字的时候,& 不能省略,例如当子过程名字被用做一个参数来判断是否它已经定义过的时候,或者当你使用 $subref = \&name 来获取一个命名子过程的引用的时候.同样,当你使用 &$subref() 或 &{$subref()} 进行一个间接子过程调用的时候也不能省略 &.不过,如果使用一种更方便的形式 $subref->(),则不需要 &.参看第八章,那里有更多有关子过程引用的内容.
Perl 并不强制子过程名字使用大写风格.但是按惯例由 perl 的运行时系统间接调用的函数都是大写的 (BEGIN, CHECK, INIT, END, AUTOLOAD, DESTORY,和所有第十四章 "捆绑变量"涉及到的函数).因此你应该避免使用这种大写风格.(但是操作常量值的子过程通常也写成大写的).
2.0 语意
在你记住所有语法前,你只需要记住下边这种定义子过程的普通方法:
subrazzle {print"Ok, you've been razzled.\n";
}和调用子过程的正常方法就是:
razzle();
在上边的写法中,我们省略了输入(参数)和输出(返回值).但是 Perl 向子过程中传入数据和子过程传出数据的方法非常简单:所有传入的参数被当成单个平面标量列表,类似的多个返回值也被当成单个平面标量列表返回给调用者.当使用任意 LIST 时也一样,任何传入的数组或散列的值都代换到一个平面的列表里面,同时也失去了它们的标识,不过有几种方法可以绕开这个问题,这种自动的列表代换在很多场合非常有用.参数列表和返回值列表都可以根据你的需要包含任意多个标量成员(当然你可以使用原型定义来约束参数的类型).实际上,Perl 是按照支持可变参函数(可以支持任何数量的参数)概念来设计的.C 则不同,虽然 C 也勉强支持一些变参的函数,例如 printf (3).
现在,如果你将设计一种可以支持不定数量的任意参数的语言,你最好让你的语言在处理这些任意长的参数列表上容易些.所有传入 Perl 过程的参数都是以 @_ 身份传入的.如果你调用一个有两个参数的函数,它们在函数内部可以作为 @_ 数组的前两个成员访问: $_[0] 和 $_[1].因为 @_ 只是一个有着奇怪名字的普通数组,所以你可以象处理普通数组一样随意处理它.(注:这个领域是 Perl 和传统的编程语言冲突得最厉害的地方.)数组 @_ 是一个本地数组,但是它的值是实际标量参数的别名(通常称为引用传参)因而如果修改了 @_ 中的成员那么同时也修改了对应的实际参数的值.(通常的语言中很少这么做,但是采用这种方法在 Perl 中可以很容易的返回所需要的值).
子过程(其他的程序块也一样)的返回值是过程最后一个表达式的值.或者你可以在子过程的任何一个地方明确使用一个 return 语句来返回值并且退出子过程.不管是那种方法,当在一个标量或列表环境中调用子过程时,最后一个表达也将在同样的标量或列表环境中求值.
2.1 参数列表的技巧
Perl 没有命名的正式参数,但是在实际中你可以将 @_ 的值拷贝到一个 my 列表,这样就可以方便使用这些正式参数(不一样的是,这样拷贝就将引用传参的语义变为了传值传参,也许传值传参正是很多用户通常希望参数被处理的方法,即使他们不知道这些计算机术语),下面是一个典型的例子:
subaysetenv {my($key, $value) = @_;
$ENV{$key} = $valueunless$ENV{$key};
}但是没人要你一定要给你的参数命名,这就是 @_ 数组的全部观点.例如,计算一个最大值,你可以简单直接遍历 @_ 数组:
submax {
$max =shift(@_);formy$item (@_) {
$max = $itemif$max < $item;
}return$max;
}
$bestday = max($mon, $tue, $wed, $thu, $fri);或者你可以一次将 @_ 填入一个散列:
subconfiguration {my%options = @_;print"Maximum verbosity.\n"if$options{VERBOSE} == 9;
}
configuration(PASSWORD => 'xyzzy', VERBOSE => 9, SOCRE => 0);下面是一个例子,这里不命名正式参数,这样你可以修改实际参数的值:
upcase_in($v1, $v2);# 这里改变 $v1 和 $v2subupcase_in {for(@_) {tr/a-z/A-Z/ }
}但是你不允许用这种方法修改常量,如果一个参数是一个象 "hobbit" 这样的实际标量值或象 $1 这样只读标量,当你试图修改它时,Perl 会抛出一个例外(可能的致命错误或者可能的威胁).例如,下面的例子将不能工作:
upcase_in("fredrick");如果将 upcase_in 函数写成返回它的参数的一个拷贝会比直接改变参数安全得多:
($v3, $v4) = upcase($v1, $v2);subupcase {my@parms = @_;for(@parms) {tr/a-z/A-Z/ }# 检查我们是否在列表环境中被调用的returnwantarray? @parms : $parms[0];
}注意这个函数(没有原型)并不在意传进来的参数是真的标量还是数组.Perl 将所有的参数粉碎成一个又大又长的平面 @_ 数组列表.这是 Perl 简单传参方式闪光的地方之一.甚至我们可以给它象下面这样的参数的时候都不需要修改 upcase 的定义,upcase 将照样工作得呗棒:
@newlist = upcase(@list1, @list2); @newlist = upcase( split /:/, $var);
但是,如果象下边这样用,就不会得到你想要的的结果:
(@a, @b) = upcase( @list1, @list3); # 错
因为,和 @_ 一样,返回列表同样是一个平面列表.因此所有的返回值将存储在 @a 中, @b 是空的.可以在下面"传递引用"部分看到替代的办法.
2.2 错误指示
如果你希望你的函数能够以特定的方式返回,使调用者能够得知发生了一个错误.在 Perl 中实现这个目的最自然的一种方法就是用一个不带参数的 return 语句.这样当函数在标量环境中使用时,调用者得到一个 undef,如果在列表环境中使用,调用者得到一个空列表.
特殊情况下,你可以选者产生一个例外来指示错误,但是必须谨慎使用这种方法.因为你的程序将被例外处理程序终结.例如,在一个文件操作函数中,打开文件失败几乎不是例外的事件.因此,最好能够忽略这种失败.当你在无效的环境下调用函数时,wantarray 内建函数将返回 undef.因此如果你想忽略它,你可以使用下面的方法:
if($something_went_awry) {returnifdefinedwantarray;# 很好,不是空环境die"Pay attention tomyerror, you danglesocket!!!\n";
}2.3 范围问题
因为每次调用都有自己的参数数组,因此子过程可以递归调用,甚至可以调用它自己.如果使用 & 的形式调用子过程,那么参数列表是可选的.如果使用了 & 并且省略了参数列表,那么有一些特殊的规则:调用过程中的 @_ 数组将做为被调用子过程的参数.新用户可能不想使用这种有效的机制.
&foo(1,2,3) # 传递三个参数
foo(1,2,3) # 和上面一样
foo(); # 传递一个空列表
&foo(); # 和上面一样
&foo; # foo() 获取当前的参数,和 foo(@_) 一样,但更快!
foo; # 如果预定义了子过程 foo,那么和 foo() 一样,否则
# 就是光字 "foo"使用 & 形式调用子过程不仅可以省略掉参数列表,同时对你提供的参数也不进行任何原型检查.这种做法一部分是因为历史原因形成,另一部分原因是为了在用户清楚自己在干什么的情况下提供一个方便的办法.你可以参看本章后面的"原型"小节.
在函数中访问一个并没有定义成该函数私有的变量不一定是全局变量;它们遵循第二章 "集腋成裘"中"名字"一节中提到的块作用范围规则,这意味着他们首先在词法作用范围里面决定该变量,然后才扩展到单个包作用范围.从子过程的角度看来,任何在一个闭合的词法作用域中的 my 变量仍然优先使用.
例如,下面例子中的 bumpx 函数使用了文件作用范围中的 $x 变量,这是因为 my 变量被定义的作用范围 --- 也就是文件本身 --- 并没有在定义子过程之前结束.
# 文件顶部
my $x = 10; # 声明和初始化变量
sub bumpx { $x++ } # 函数可以看到外层词法变量C 和 C++ 程序员很可能认为 $x 是一个"文件静态"变量.它对其他文件中的函数是私有的,但是在上例中在 my 后面定义的函数可以透视到这个变量.那些由 C 程序员转变而来的 Perl 程序员在 Perl 中找不到他们熟悉的文件或函数的"静态变量".Perl 程序员避免使用 "static"这个词.因为静态系统过时而且乏味,并且因为在历史使用中这个词被搞得一团糟.
虽然 Perl 语法中没有包括"static"这个词,但是 Perl 程序员同样能够创建函数的私有变量,并且保持跨函数访问.Perl 中没有专门的词来表述他们.利用 Perl 丰富的作用范围规则结合自动内存管理,就可以有很多方式实现"static"关键字的功能.
词法变量并不会只是因为退出了它们的作用范围后就被自动内存垃圾收集回收,它们要等到不再使用后才被回收,这个概念十分重要.为了创建一个在跨函数调用中不被重置的私有变量,你可以将整个函数用花括弧括起来,并将 my 定义和函数定义放入该函数块中.你甚至可以放入多个函数定义,这样该私有变量就可以被这些函数共享访问.
{my$counter = 0;subnext_counter {return++$counter }subprev_counter {return--$counter }
}通常,对词法变量的访问被限制在同一个词法作用域中.两个函数的名字可以被全局访问 (在同一个包内),并且因为它们是在 $counter 的作用域中定义的,它们仍然可以访问该变量,即使其他函数访问不到也无妨.
如果这个函数是通过 require 或 use 加载的,那么也可以.如果它全在主程序中,那么你就要确保使任何运行时的 my 赋值要足够地早,你可以将整个程序块放在主程序的最前边,也可以使用 BEGIN 或 INIT 程序块来保证它在你的程序之前运行:
BEGIN{my@scale = ('A' .. 'G');my$note = -1;subnext_pitch {return$scale[ ($note += 1) %= @scale ] );
}BEGIN 既不会影响子过程的定义,也不会影响子过程里使用的任意词法的一致性.这里它仅仅保证在子程序被调用之前变量就被初始化.想了解定义私有变量和全局变量更多的内容,请分别参考29章"函数"的 my 和 our 的说明,BEGIN 和 INIT 在第十八章"编译"中解释.
3.0 传入引用
如果你想在一个函数中传入或传出不止一个的数组或散列结构,同时你希望它们保持它们的一致性,那么你就需要使用一个更明确的传递引用的机制.在你使用传递引用之前,你需要懂得第八章里有关引用的细节.本小节不着重讲述引用的内容.
这里有几个简单的例子,首先,让我们定义一个函数,这个函数使用数组的引用作为参数.当这个数组非常大时,作为一个引用传递要比传入一长列值要快得多:
$total = sum (\@a );subsum {my($aref) = @_;my($total) = 0;foreach(@$aref) { $total += $_ }return$total;
}下面让我们将几个数组传入一个函数,并且使用使用 pop 得到每个数组的最后一个元素,并返回每个数组最后一个元素组成的一个新的数组:
@tailings = popmany (\@a, \@b, \@c, \@d );subpopmany {my@retlist = ();formy$aref (@_) {push@retlist,pop@$aref;
}return@retlist;
}下面是一个函数,能够返回一个列表,这个列表包含在每个传入的散列结构中都出现的键字.
@common = inter (\%foo, \%bar, \%joe );subinter {my%seen;formy$href (@_) {while(my$k =each%$href ) {
$seen{$k}++;
}
}returngrep{ $seen{$_} == @_ }keys%seen;
}这里我们只用了普通的列表返回机制.当你想传送或返回一个散列结构时会发生什么?如果你仅用其中的一个,或者你不在意它们连在一起,那么使用普通的调用方法就行了,如果不是,那么就会稍微复杂一些.
我们已经在前面提到过,人们常会在下面的写法中遇到麻烦:
(@a, @b) = func(@c, @d);
或这里:
(%a, %b) = func(%c, %d);
这些表达式将不会正确工作,它只会设置 @a 或%a,而 @b 或 %b 则是空的.另外函数不会得到两个分离的数组和散列结构作为参数:它和往常一样从 @_ 中得到一个长列表.
你也许想在函数的输入和输出中都使用引用.下面是一个使用两个数组引用作为参数的函数,并且根据数组中包含元数的多少为顺序返回两个数组的引用:
($aref, $bref) = func(\@c, \@d);print"@$aref has more than @$bref\n";subfunc {my($cref, $dref) = @_;if(@$cref > @$dref) {return($cref, $dref);
}else{return($dref, $cref);
}
}如何向函数传入或传出文件句柄或目录句柄,请参阅第八章的"文件句柄引用"和"符号表句柄"小节.
导入论坛 引用链接 收藏 分享给好友 推荐到圈子 管理 举报
TAG:
标题搜索
日历
|
|||||||||
| 日 | 一 | 二 | 三 | 四 | 五 | 六 | |||
| 1 | 2 | 3 | 4 | ||||||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 | |||
| 12 | 13 | 14 | 15 | 16 | 17 | 18 | |||
| 19 | 20 | 21 | 22 | 23 | 24 | 25 | |||
| 26 | 27 | 28 | 29 | 30 | 31 | ||||
数据统计
- 访问量: 6154
- 日志数: 555
- 建立时间: 2008-01-07
- 更新时间: 2008-06-24

