標籤:意義 count() amp 返回 持久化 one ash 預設 cmd
在Perl中,子程式的引用常用來做回呼函數(callback)、閉包(closure),特別是匿名子程式。
回呼函數(callback)
關於什麼是回呼函數,見一文搞懂:詞法範圍、動態範圍、回呼函數、閉包
以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
全路徑不一樣。例如:
起始路徑 $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));
Perl閉包(closure)簡單介紹
關於閉包的詳細內容,見一文搞懂:詞法範圍、動態範圍、回呼函數、閉包
從perl語言的角度來簡單描述下閉包:子程式1中返回另一個子程式2,這個子程式2訪問子程式1中的變數x,當子程式1執行結束,外界無法再訪問x,但子程式2因為還引用著變數x所指向的資料對象,使得子程式2在子程式1結束後可以繼續訪問這個資料對象。
所以,子程式1中的變數x必須是詞法變數,否則子程式1執行完後,變數x可能仍可以被外界訪問、修改,如果這樣,閉包和普通函數就沒有意義了。
一個簡單的閉包結構:
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(); # 將閉包賦值給變數$refhow_many()->(); # (1)調用匿名閉包:輸出3how_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
也不會消失,對於閉包來說這看上去沒什麼必要。
Perl閉包應用
例如,通過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(); # 1one_count(); # 2print 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(); # 11one_count(); # 12print get_count(); # 輸出:12BEGIN{ my $count=10; sub one_count{ ++$count; } sub get_count{ $count; }}
state修飾符替代簡單的閉包
前面閉包的作用已經非常明顯,就是為了讓詞法變數不能被外部存取,但卻讓子程式持續訪問它。
perl提供了一個state修飾符,它和my完全一樣,都是詞法變數,唯一的區別在於state修飾符使得變數持久化,但對於外界來說卻不可訪問(因為是詞法變數),而且state修飾的變數只會初始化賦值一次。
注意:
- state修飾符不僅僅只能用於子程式中,在任何語句塊中都可以使用,例如find、grep、map中的語句塊。
- 只要沒有東西在引用state變數,它就會被回收。
- 目前state只能修飾標量,修飾數組、hash時將會報錯。但可以修飾數組、hash的引用變數,因為引用就是個標量
例如,將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)再次調用命名閉包:輸出4sub how_many2 { return sub {state $count=2;print ++$count,"\n"};}
這是因為state $count=2
是子程式中的一部分,無論在哪裡調用到它,都會執行這一句指派陳述式。
Perl回呼函數和閉包