Perl回呼函數和閉包

來源:互聯網
上載者:User

標籤:意義   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回呼函數和閉包

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.