在Perl中,子程序的引用經常使用來作回調函數(callback)、閉包(closure),特別是匿名子程序。html
關於什麼是回調函數,見一文搞懂:詞法做用域、動態做用域、回調函數、閉包數組
以File::Find
模塊的find函數爲例,它用來搜索給定目錄下的文件,而後對每一個搜索到的文件執行一些操做(經過定義子程序),這些操做對應的函數要傳遞給find函數,它們就是回調函數。就像unix下的find命令同樣,找到文件,而後print、ls、exec CMD操做同樣,這幾個操做就是find命令的回調函數。閉包
use File::Find; sub cmd { print "$File::Find::name\n"; }; find(\&cmd,qw(/perlapp /tmp/pyapp));
其中$File::Find::name
表明的是find搜索到的從起始路徑(/perlapp /tmp/pyapp
)開始的全路徑名,此外,find每搜索到一個文件,就會賦值給默認變量$_
。它表明的是文件的basename,和$File::Find::name
全路徑不同。例如:app
起始路徑 $File::Find::name $_ ------------------------------------------- /perlapp /perlapp/1.pl 1.pl . ./a.log a.log perlapp perlapp/2.pl 2.pl
回到回調函數的問題上。上面的示例中,定義好了一個名爲cmd的子程序,但一直都沒有主動地去執行這個子程序,而是將它的引用放進find函數中,由find函數每次去調用它。這就像是unix的find命令的"-print"選項同樣,其中"-print"選項對應的函數就是一個回調函數。函數
上面的子程序只調用了一次,不必花腦細胞去設計它的名稱,徹底能夠將其設計爲匿名子程序,放進find函數中。設計
use File::Find; find( sub { print "$File::Find::name\n"; }, qw(/perlapp /tmp/pyapp) );
關於閉包的詳細內容,見一文搞懂:詞法做用域、動態做用域、回調函數、閉包unix
從perl語言的角度來簡單描述下閉包:子程序1中返回另外一個子程序2,這個子程序2訪問子程序1中的變量x,當子程序1執行結束,外界沒法再訪問x,但子程序2由於還引用着變量x所指向的數據對象,使得子程序2在子程序1結束後能夠繼續訪問這個數據對象。code
因此,子程序1中的變量x必須是詞法變量,不然子程序1執行完後,變量x可能仍能夠被外界訪問、修改,若是這樣,閉包和普通函數就沒有意義了。htm
一個簡單的閉包結構:對象
sub sub1 { my $var1=N; $sub2 =sub { do something about $var1 } return $sub2 # 返回一個閉包 } $my_closure = sub1(); # 將閉包函數存儲到子程序引用變量
主要目的是爲了讓子程序sub1內部嵌套的子程序$sub2
能夠訪問屬於子程序sub1但不屬於子程序$sub2
的變量,這樣一來,只要把sub1返回的閉包賦值給$my_closure
,就可讓這個閉包函數一直引用$var1
變量對應的數值對象,可是sub1執行完畢後,外界就沒法再經過$var1
去訪問這個數據對象(由於是詞法變量)。也就是說,sub1執行完後,$var1
指向的數據對象只有閉包$my_closure
能夠訪問。
一個典型的perl閉包:
sub how_many { # 定義函數 my $count=2; # 詞法變量$count return sub {print ++$count,"\n"}; # 返回一個匿名函數,這是一個匿名閉包 } $ref=how_many(); # 將閉包賦值給變量$ref how_many()->(); # (1)調用匿名閉包:輸出3 how_many()->(); # (2)調用匿名閉包:輸出3 $ref->(); # (3)調用命名閉包:輸出3 $ref->(); # (4)再次調用命名閉包:輸出4
上面將閉包賦值給$ref
,經過$ref
去調用這個閉包,則即便how_many中的$count
在how_many()執行完就消失了(由於是個詞法變量,外界沒法訪問),但$ref
指向的閉包函數仍然在引用這個變量,因此屢次調用$ref
會不斷修改$count
的值,因此上面(3)和(4)先輸出3,而後輸出改變後的4。而上面(1)和(2)的輸出都是3,由於兩個how_many()函數返回的是獨立的匿名閉包,在語句執行完後數據對象3就消失了。
Perl語言有本身的特殊性,特別是它支持只執行一次的語句塊(即用大括號{}
包圍),這使得Perl要建立一個閉包並不必定須要函數嵌套,只需將一個函數放進語句塊便可:
my $closure; { my $count=1; # 隨語句塊消失的詞法變量 $closure = sub {print ++$count,"\n"}; # 閉包函數 } $closure->(); # 調用一次閉包函數,輸出2 $closure->(); # 再調用一次閉包函數,輸出3
在上面的代碼中,$count
引用數在賦值時爲1,在sub中使用並賦值給$closure
時引用數爲2,當退出代碼塊的時候,count
引用數減爲1,因爲這是個詞法變量,退出代碼塊後外界就沒法經過$count
來訪問了,可是閉包$closure
卻一直能夠繼續訪問。
閉包的形式其實多種多樣。通俗意義上來講,只要一個子程序1能夠訪問另外一個子程序2中的變量,且子程序1不會隨子程序2執行結束就丟失變量,就屬於閉包。固然,對於Perl來講,可能子程序2並不是是必要的,正如上面的例子。
例如,下面的代碼段就不屬於閉包:
$y=3; sub mysub1 { $x=shift; $x+$y; } $nested_ref=\&mysub1; sub mysub2 { $x=1; $z=shift; return $nested_ref->($z); } print mysub2(2);
爲mysub2中返回的$nested_ref
是一個子程序mysub1的一個實例,但mysub1中使用的$y
來自於全局變量,而非mysub2,且mysub2執行完後,$y
也不會消失,對於閉包來講這看上去沒什麼必要。
例如,經過File::Find
模塊的find函數,計算出給定目錄下的文件數量:
use File::Find; my $callback; { my $count = 0; $callback = sub { print ++$count, ": $File::Find::name\n" }; } find($callback, '.'); # 返回數量和文件名 find($callback, '.'); # 再次執行,數量將在上一個find的基礎上遞增
Perl的語法強大,能夠一次性返回多個閉包:
use File::Find; sub sub1 { my $total_size = 0; return(sub { $total_size += -s if -f }, sub { return $total_size }); } my ($count_em, $get_results) = sub1( ); find($count_em, '/bin'); find($count_em, '/boot'); my $total_size = &$get_results( ); print "total size of /bin and /boot: $total_size\n";
上面兩個閉包,由於同時引用同一個對象,因此閉包$count_em
修改的詞法變量,$get_results
也能夠訪問。
或者:
{ my $count=10; sub one_count{ ++$count; } sub get_count{ $count; } } one_count(); one_count(); print get_count();
因爲代碼塊中的子程序有名稱,因此這兩個子程序在代碼塊結束後仍然有效(代碼塊結束後變量無效是由於加了my修飾符)。
可是,若是將調用語句放在代碼塊前面呢?
one_count(); # 1 one_count(); # 2 print get_count(); # 輸出:2 { my $count=10; sub one_count{ ++$count; } sub get_count{ $count; } }
上面輸出2,也就是說$count=10
的賦值10行爲失效。這是由於詞法變量的聲明和初始化(初始化爲undef)是在編譯期間完成的,而賦值操做是在執行到它的時候執行的。因此,編譯完成後執行到one_count()
這條語句時,將調用已編譯好的子程序one_count,但這時尚未執行到語句塊,因此$count
的賦值尚未執行。
能夠將上面的語句塊加入到BEGIN塊中:
one_count(); # 11 one_count(); # 12 print get_count(); # 輸出:12 BEGIN{ my $count=10; sub one_count{ ++$count; } sub get_count{ $count; } }
前面閉包的做用已經很是明顯,就是爲了讓詞法變量不能被外部訪問,但卻讓子程序持續訪問它。
perl 5.10提供了一個state修飾符,它和my徹底同樣,都是詞法變量,惟一的區別在於state修飾符使得變量持久化,但對於外界來講卻不可訪問(由於是詞法變量),並且state修飾的變量只會初始化賦值一次。
注意:
例如,將state修飾的變量從外層子程序移到內層自層序中。下面兩個子程序等價:
use 5.010; # for state sub how_many1 { my $count=2; return sub {print ++$count,"\n"}; } sub how_many2 { return sub {state $count=2;print ++$count,"\n"}; } $ref=how_many2(); # 將閉包賦值給變量$ref $ref->(); # (1)調用命名閉包:輸出3 $ref->(); # (2)再次調用命名閉包:輸出4
須要注意的是,雖然state $count=2
,但同一個閉包屢次執行時不會從新賦值爲2,而是在初始化時賦值一次。
並且,將子程序調用語句放在子程序定義語句前面是能夠如期運行的(前面分析過通常的閉包不會如期運行):
$ref=how_many2(); # 將閉包賦值給變量$ref $ref->(); # (1)調用命名閉包:輸出3 $ref->(); # (2)再次調用命名閉包:輸出4 sub how_many2 { return sub {state $count=2;print ++$count,"\n"}; }
這是由於state $count=2
是子程序中的一部分,不管在哪裏調用到它,都會執行這一句賦值語句。
再例如,state用於while循環的語句塊內部,使得每次迭代過程當中都持續訪問這個變量,而不會每次迭代都初始化:
#!/usr/bin/perl use v5.10; # for state while($i<10){ state $count; $count += $i; say $count; # 輸出:0 1 3 6 10 15 21 28 36 45 $i++; } say $count; # 輸出空