作為一門動態(tài)語言,perl的強(qiáng)大之處源于它靈活的的子程序。要定義一個子程序很簡單,我們無需告訴它將會收到多少個參數(shù),以及每個參數(shù)的數(shù)據(jù)類型,直接把數(shù)據(jù)列表傳給子程序就好了,之后再來決定如何處理。
perl里面的子程序根本不需要提前定義。我們可以在運行程序時動態(tài)創(chuàng)建一個,也可以在之后重新定義。子程序本身也可以構(gòu)建其他子程序,每一個子程序都能包含各自獨立的私有數(shù)據(jù)
理解my和local的差異
my和local之間的差異是perl中非常微妙和復(fù)雜的方面之一
全局變量
在perl里面所有的變量、子程序和其他可以被命名的實體默認(rèn)都擁有包作用域(或稱為“全局作用域”)。也就是說它們存在當(dāng)前包的符號表中
大多數(shù)情況下,perl會在編譯階段將全局名稱放到合適的包符號表中
print join ' ' ,keys %::,"\n"; #打印符號列表%::
$complie_time; #出現(xiàn)在符號列表中
在實際編程中,應(yīng)該避免使用全局變量,因為全局變量就像隱藏接口,指不定什么時候就變掉了,代碼也會難于理解和修改。我們更多的使用本地變量,perl提供了兩種本地變量,一個是my,一個是local
my變量詞法作用域(編譯時)
perl的my操作符用于創(chuàng)建詞法作用域變量,通過my創(chuàng)建的變量存活于聲明開始的地方,直到閉合作用域結(jié)束
$a = 3; #全局的
{
my $a = 4; #詞法的
print $a; #4
}
print $a; #3
#這種方式是編程語言最常見的處理作用域的方式
my $compline_time;
$compline_time;
print join ' ',keys %:: ; #詞法變量不存在符號列表中
local作用域(運行時)
perl的另一種作用域機(jī)制是local,它比my的歷史更悠久,事實上,my是直到perl 5才引入的,那么local到底哪里不好?
local是運行時作用域機(jī)制,和my不同,my基本上時在私有變量符號表中創(chuàng)建新變量,而local則是在運行時起作用:它會將參數(shù)的值保存在一個運行時棧中,當(dāng)執(zhí)行線程離開作用域后,原先作用域外暫存的變量會被恢復(fù)
$a = 3; #全局的
{
my $a = 4; #詞法的
print $a; #4
}
print $a; #3
#把my換成local
$a = 3;
{
local $a = 4;
print $a; #4
}
print $a; #3
從上面的代碼看出,local和my做的事情貌似非常相似,但是在perl內(nèi)部,卻是完全不同的
在my的例子中my創(chuàng)建的變量a不存在包符號表中,執(zhí)行內(nèi)部的變量a時,外部的變量a仍存在于符號表中
而在local中,在閉合代碼塊中,perl只是將變量a替換成了新的內(nèi)容,當(dāng)程序離開代碼塊后,perl將local所保存的值恢復(fù),整個程序中,只有一個a變量
因此,my創(chuàng)建了不同的變量,而local只是將已存在的變量值暫時保存起來
何時該用my
通常情況下,我們應(yīng)該用my而不是local。理由是my比local快,因為local將值存入棧的過程,是需要消耗時間的,而且my創(chuàng)建的詞法變量是perl閉包的實現(xiàn)基礎(chǔ)
何時該用local
有一些必須用local才能解決的問題。大部分$開頭的變量,或者是其他perl特別對待的變量,只能用local來進(jìn)行本地化,而用my試圖對特殊變量進(jìn)行本地化是錯誤的
my $contents = do {local $/; ... };
local和my對列表的操作
local和my的語法相同,無論是單個標(biāo)量,還是數(shù)組或者散列,都可以用這兩種類型聲明
my $a;
local @a;
#本地化某個變量時對它初始化
local $a = 4;
my @a = (1,2,3);
#my和local的參數(shù)用圓括號闊起來,參數(shù)就成了列表,perl會在列表上下文中對賦值操作進(jìn)行求值
local ($one,$two,$three) = @a;
#常見的列表賦值陷阱:
my ($a) = <IN>; #錯誤,讀入的是所有內(nèi)容
請勿直接使用@_
在perl中子程序的參數(shù)都是通過@_傳遞的。一般來說,我們都是在子程序開始時復(fù)制傳進(jìn)來的參數(shù),并分別命名,常用my聲明參數(shù)變量
sub fun {
my ($str) = @_;
$str =~ tr/0-9//d;
$str;
}
讀取子程序傳入?yún)?shù)的慣用方式是用shift函數(shù)逐個提取,或使用列表賦值一次性獲取
sub fun {
my $str = shfit;
my @chars = @_;
my @counts;
for (@chars) {
push @counts,eval "\$str =~ tr/$_//";
}
@counts;
}
@的元素實際上就是我們傳進(jìn)來的參數(shù)的別名,所以修改@的元素其實也就是修改了子程序外部參數(shù)變量的值,即“引用傳參”,如果外部參數(shù)實際為只讀的話,在子程序內(nèi)部修改參數(shù)會導(dǎo)致錯誤
sub txt {
$_[0] .= '.txt' unless /\.txt$/;
-s $_[0];
}
txt "test"; #報錯,修改只讀參數(shù)
有時侯利用這種“別名”式的特性也確實挺有用,比如借助子程序修改原始參數(shù)
sub normalize {
my $max = 0;
for (@_) {
$max = abs($_) if abs($_) > $max;
return unless $max
}
for (@_) {
$_ /= $max;
}
return;
}
雖然子程序的參數(shù)是以別名方式進(jìn)行傳遞的,但數(shù)組作為參數(shù)傳進(jìn)后,會被展開為列表,所以就算修改收到的參數(shù)元素,也不會影響原來的數(shù)組元素
sub no_bad {
for $i (0 .. $#_) {
if ($_[$i] =~ /^bad$/) {
splice @_,$i,1;
print "in no bad: @_\n";
return;
}
}
return;
}
my @a = qw (ok better bad good);
no_bad @a; # ok better good
print "after no_bad:@a\n"; #ok better bad good
# 雖然在no_bad()里面修改了@_,但子程序返回后,@a的值還和之前一樣
如果未加參數(shù)調(diào)用子程序,那么子程序會有一個默認(rèn)的空的@數(shù)組,而如果以&符號調(diào)用子程序并不加圓括號時,情況又會不同,它會繼承當(dāng)前環(huán)境中的@數(shù)組
sub inner {
print "\@_ = @_\n";
}
sub outer {
&inner;
}
outer 1 .. 3; #print @_ = 1 2 3
傳遞引用而非副本
“老式”的子程序參數(shù)傳遞方式有兩個缺點:首先,盡管我們可以修改參數(shù)中的元素,但卻無法修改數(shù)組或散列本身;其次,將數(shù)組和散列復(fù)制到@_花費時間過長。而通過傳遞變量引用的方式,我們可以將這些缺點都克服掉。
傳遞引用參數(shù)
當(dāng)我們給子程序傳遞參數(shù)時,perl會將它們別名后放入@中。之后如果我們從@提取出來保存到變量中時,perl才會真正的復(fù)制它們,所以傳遞的參數(shù)越多,perl要做的工作也就越多
sub sum {
my @numbers = @_;
my $sum = 0;
foreach my $num (@numbers) {
$sum += $num;
}
$sum;
}
sum (1 .. 1000000);
# 在sum子程序中,perl必須復(fù)制1000000個元素到數(shù)組變量@numbers中去,如果只傳遞一個指向數(shù)組的引用,那么就可以省略這些無謂工作
sub sum {
my ($numbers_ref) = @_;
my $sum = 0;
foreach my $num (@$numbers_ref) {
$sum += $num;
}
$sum;
}
由于perl的參數(shù)永遠(yuǎn)是展開的列表,所以子程序?qū)υ紨?shù)據(jù)結(jié)構(gòu)一無所知。如果參數(shù)列表由兩個或多個數(shù)組構(gòu)成,那么子程序最后只會看到兩個數(shù)組展開后串接合并到一起的完整列表。為了有所區(qū)分,我們可以分別傳遞它們的引用
process_refs (\@array1,\@array2);
#在子程序中,我們會得到一個包含兩個數(shù)組引用的列表,隨后便可逐個進(jìn)行處理
sub process_arrays {
my (@array_refs) = @_;
foreach my $ref (@array_refs) {
... ;
}
}
#任何一種數(shù)據(jù)結(jié)構(gòu)的引用都可以采取這樣的方式傳遞。而在子程序內(nèi)部,只需要逐個提取以正確方式使用即可:
process_refs (\@array,\%hash,\&sub_name);
返回引用參數(shù)
返回結(jié)果和傳入?yún)?shù)的過程恰好相反,既然傳遞引用能免去傳入?yún)?shù)時的復(fù)制操作,那么返回數(shù)據(jù)時同樣也可以采取傳遞引用的方式返回。特別是要返回的數(shù)據(jù)結(jié)構(gòu)復(fù)雜龐大時,更應(yīng)該直接返回引用
my $string_ref = slurp_file ($file);
print "the file was:\n$$string_ref\n";
sub slurp_file {
my $file = shift;
open my ($fh),"<",$file or die;
local $/;
my $string = <$fh>;
\$string;
}
#當(dāng)然我們也可以在子程序中返回多個數(shù)據(jù),這就和給子程序傳遞參數(shù)一樣
my ($array_ref,$hash_ref) = make_data_structure();
sub make_data_structure {
return \@array,\%hash;
}
用散列傳遞命名參數(shù)
盡管perl沒有提供自動命名參數(shù)的傳遞方法,但我們在調(diào)用子程序時仍然有很多方法,可以同時傳遞包含名字和值的參數(shù)列表
sub uses_named_params {
my %param = (
foo => 'val1',
bar => 'val2',
);
my %input = @_;
@param{keys %input} = values %input;
}
#現(xiàn)在我們可以使用鍵值對的方式調(diào)用子程序
uses_named_params (bar => 'myval1', bletch => 'myval2');
通過函數(shù)原型聲明以特殊方式解析參數(shù)
perl支持子程序參數(shù)原型聲明,函數(shù)原型能夠讓你聲明的子過程能夠像很多內(nèi)建函數(shù)一樣 獲得參數(shù),就是獲得一定數(shù)目和類型的參數(shù).我們雖然稱之為函數(shù)原型。參數(shù)原型聲明只不過是提示perl應(yīng)該如何解析代碼
編寫pop函數(shù)
#要想實現(xiàn)pop函數(shù),就得用參數(shù)引用的方式,以便修改原始參數(shù)內(nèi)容
sub pop2_ref {
splice @{$_[0]},-2,2
}
#但是這樣的話,我們就必須在使用時給出原始數(shù)組的引用,而不是直接給出數(shù)組變量
my @a = 1 .. 10;
my ($a,$b) = pop2_ref \@a;
#現(xiàn)在我們可以引入?yún)?shù)原型聲明了,通過對參數(shù)列表做一些特殊處理,實現(xiàn)內(nèi)置函數(shù)pop一樣的功能
sub pop2 (\@) {
splice @{$_[0]},-2,2
}
原型是由原型原子構(gòu)成的。原型原子是一些字符,有時會以反斜杠開頭表明子程序所接受的參數(shù)類型(反斜杠告訴perl傳遞該參數(shù)的引用),所以pop2后面的數(shù)組會被取引用后傳入,而不是作為一個包含多個值的列表整體傳入
原型還涉及對參數(shù)類型和數(shù)量是否合適的檢查

創(chuàng)建閉包鎖住數(shù)據(jù)
在perl中,閉包(一個函數(shù)的返回值里有函數(shù)就是閉包)指的是可以包含能游離于作用域之外的詞法變量的子程序,而這些變量數(shù)據(jù)是所以不消失,并隨同子程序引用一同保留在內(nèi)存中,是因為子程序仍然有指向它們的引用
命名子程序的私有數(shù)據(jù)
有時候我們的子程序需要一些只有它們自己能讀取的數(shù)據(jù)。也就是說,對于任何數(shù)據(jù),我們?nèi)粝雽⑺鼈兊目梢娦韵薅ㄔ谝粋€最小的可控操作范圍內(nèi),最簡單的實現(xiàn)方式是將數(shù)據(jù)直接放在子程序內(nèi)部
sub some_sub {
my $a = '/path/to/my/app';
... ;
}
#這么做的話,perl每次調(diào)用子程序時都得重建這個標(biāo)量變量,如果我們不需要修改該數(shù)據(jù),那么這無疑就是浪費
#我們可以在程序外部定義$a,不過要限定它的作用域在該子程序內(nèi),我們可以將$a的定義和子程序打包在一個區(qū)塊中,$a要先于子程序定義,這樣子程序就可以使用它,一般會放在BEGIN區(qū)塊中打包
BEGIN {
my $a = '/path/to/my/app';
sub some_sub {
... ;
}
}
在perl 5.10 或更高版本中,我們可以通過state靜態(tài)變量實現(xiàn)同樣的效果,現(xiàn)在成為perl的特性之一。首次運行子程序時,perl會定義state靜態(tài)變量并賦值,而在隨后的調(diào)用中,perl會忽略這行代碼(避免初始化),該變量會保留前一次子程序運行時的值
use 5.010;
sub show_letter {
state $letter = 'a';
print "letters is ", $letter++,"\n";
}
foreach (0 .. 5) {
show_letter();
}
#output
letters is a
letters is b
letters is c
letters is d
letters is e
letters is f
子程序引用的私有變量
匿名閉包與使用state變量基本上是一回事,但它的用處更大,采用閉包我們可以靈活自由地創(chuàng)建多個閉包,并按照特別的需求建立每個子程序
my $session = do {
my $a = '/path/to/my/app';
sub {
... ;
}
};
#這樣的好處在于,我們可以動態(tài)創(chuàng)建滿足當(dāng)下需求的閉包子程序,但缺乏靈活性
#按照工廠模式動態(tài)創(chuàng)建閉包子程序的方式,顯然要靈活得多
sub make_cycle {
my ($min,$max) = @_;
my @numbers = $min .. $max;
my $cursor = 0;
sub { $numbers[ $cursor++ % @numbers]}
}
my $cycle_5_10 = make_cycle (5 , 9); #創(chuàng)建子程序的引用
my $cycle_f_m = make_cycle ('f' , 'm');
#當(dāng)我們調(diào)用其中一個閉包,它不會影響其他通過同一個工廠子程序創(chuàng)建的閉包
foreach (0 .. 10) {
print $cycle_5_10->(),$cycle_f_m->(); #瘦箭頭操作符,取引用
}
用子程序創(chuàng)建新子程序
如果經(jīng)常以相同參數(shù)調(diào)用某些固定的子程序,不妨創(chuàng)建一個新的子程序,由它負(fù)責(zé)記住這些參數(shù),這些稱為子程序的柯里化
#下面這個子程序,根據(jù)給定模式找出數(shù)組中符合條件的元素
sub my_sort {
my ($pattern,$array_ref) = @_;
my @results = sort grep /$pattern/o,@$array_ref;
}
#調(diào)用時,必須同時給出匹配模式和列表
my @results = my_sort qr/.../,\@input;
#這段代碼并不長,但如果我們需要在代碼中以同樣的搜索模式做很多遍搜索呢?我們只能重復(fù)輸入這些代碼,顯然不夠方便
my $find = sub {
my ($array_ref) = shift;
my_sort (qr/.../i,@$array_ref);
}
#這樣調(diào)用就很方便
my @results = $find->(\@input);
我們還可以根據(jù)舊函數(shù)創(chuàng)建新函數(shù)
#下面是一些對字符串做轉(zhuǎn)換的簡短子程序
sub my_uc {uc $_[0]}
sub my_ucfirst {ucfirst $_[0]}
sub trim_front {my $s = shift; $s =~ s/^\s+//; $s}
sub trim_back {my $s = shift; $s =~ s/\s+$//; $s}
#現(xiàn)在給定一個字符串,將其開頭和結(jié)尾的空白字符去掉,并將第一個字符轉(zhuǎn)換為大寫
my $string =' ';
$string = my_ucfirst (trim_back (trim_first ($string) ) );
#看起來比較亂,可以將這些函數(shù)組合成一個子程序
#由于上面每一個子程序都有相同的參數(shù),故想到可以封裝一組子程序
my $function = sub {
my $string = shift;
my_ucfirst (trim_back (trim_front (trim_back ($string) ) )
}
$string = $function-> ($string);