Perl回調函數和閉包

在Perl中,子程序的引用經常使用來作回調函數(callback)、閉包(closure),特別是匿名子程序。html

回調函數(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全路徑不同。例如: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)
);

Perl閉包(closure)簡單介紹

關於閉包的詳細內容,見一文搞懂:詞法做用域、動態做用域、回調函數、閉包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也不會消失,對於閉包來講這看上去沒什麼必要。

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();  # 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; }
}

state修飾符替代簡單的閉包

前面閉包的做用已經很是明顯,就是爲了讓詞法變量不能被外部訪問,但卻讓子程序持續訪問它。

perl 5.10提供了一個state修飾符,它和my徹底同樣,都是詞法變量,惟一的區別在於state修飾符使得變量持久化,但對於外界來講卻不可訪問(由於是詞法變量),並且state修飾的變量只會初始化賦值一次。

注意:

  • state修飾符不只僅只能用於子程序中,在任何語句塊中均可以使用,例如find、grep、map中的語句塊。
  • 只要沒有東西在引用state變量,它就會被回收。
  • 目前state只能修飾標量,修飾數組、hash時將會報錯。但能夠修飾數組、hash的引用變量,由於引用就是個標量

例如,將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;       # 輸出空
相關文章
相關標籤/搜索