Perl中,函數(又稱子程序)是一個封裝的行為單元。
函數可以有自己的名字,可以接受輸入,可以產生輸出,它是Perl程序用來抽象、封裝和重用的一種主要機制。
聲明函數
使用關鍵字sub來聲明并定義一個函數:
sub greet_me { print "ok"; }
聲明后你就可以使用這個函數了。
就像聲明變量不用立即賦值,你也可以先聲明一個函數而不立即定義它。
sub greet_sun;
#先聲明,后續再定義
調用函數
對函數名字使用后綴括號來調用該函數,參數放在括號內:
greet_me( 'Jack', 'Tuxie' );
greet_me( 'Snowy' );
greet_me();
括號并不是必須的,不過有括號能極大的提高可讀性。這是個好習慣。
函數參數可以是任意類型:
greet_me( $name );
greet_me( @authors );
greet_me( %editors );
greet_me( get_readers() );
函數參數
在 “Perl哲學” 那章我們介紹過,一個函數收到的參數都在@_數組里面;當調用函數時,Perl會將所有的參數都“壓平”放進一個列表里。
函數可以讀取@_的內容并賦值給新的變量,也可以直接操作@_數組:
sub greet_one
{
my ($name) = @_;
say "Hello, $name!";
}
sub greet_all
{
say "Hello, $_!" for @_;
}
通常編寫函數時會使用shift來卸載參數。當然也可以使用列表賦值來讀取參數,還可以直接用數組索引來訪問需要的參數:
sub greet_one_shift
{
my $name = shift;
say "Hello, $name!";
}
sub greet_two_list_assignment
{
my ($hero, $sidekick) = @_;
say "Well if it isn't $hero and $sidekick. Welcome!";
}
sub greet_one_indexed
{
my $name = $_[0];
say "Hello, $name!";
# or, less clear
say "Hello, $_[0]!";
}
@_就是一個普通的數組,你可以使用所有數組相關的操作符來操作@_
,如unshift, push, pop, splice, slice 。
有些操作符的默認操作數就是@_
,這時你就可以偷懶了:
my $name = shift;
某些情形下使用列表賦值會更清晰,參照以下2段功能相同的代碼:
#單個卸載
my $left_value = shift;
my $operation = shift;
my $right_value = shift;
#列表賦值
my ($left_value, $operation, $right_value) = @_;
這種情況時第2種寫法更簡單,可讀性更好,效率也更高。
通常來說,****當你只需要一個參數時使用shift;讀取多個參數時使用列表賦值。****
展平
調用函數時,參數列表會被壓平放進@_里面,所以將哈希作為參數傳遞進去時,就會展平成一系列的鍵值對:
my %pet_names_and_types = (
Lucky => 'dog',
Rodney => 'dog',
Tuxedo => 'cat',
Petunia => 'cat',
Rosie => 'dog',
);
show_pets( %pet_names_and_types );
sub show_pets
{
my %pets = @_;
while (my ($name, $type) = each %pets)
{
say "$name is a $type";
}
}
當哈希展平成一個列表時,鍵值對與鍵值對之間的順序是不確定的,但是鍵和值之間是有規律的:鍵后面肯定是對應的值。
在標量參數和列表參數混合使用時需要小心處理參數,看下面這個例子:
sub show_pets_by_type
{
my ($type, %pets) = @_; #急得要先將標量分出來
while (my ($name, $species) = each %pets)
{
next unless $species eq $type;
say "$name is a $species";
}
}
my %pet_names_and_types = (
Lucky => 'dog',
Rodney => 'dog',
Tuxedo => 'cat',
Petunia => 'cat',
Rosie => 'dog',
);
show_pets_by_type( 'dog', %pet_names_and_types );
show_pets_by_type( 'cat', %pet_names_and_types );
show_pets_by_type( 'moose', %pet_names_and_types );
吞(slurping)
列表賦值是貪婪的,所以上面例子中%pets
會吞掉@_
里面所有剩下的值。所以如果$type參數的位置是在后面,那么就會報錯,因為所有的值先被哈希吞掉,但是卻發現單了一個。這時可以這樣做:
sub show_pets_by_type
{
my $type = pop;
my %pets = @_;
...
}
當然還可以使用傳遞引用的方式來實現。
別名
@就是參數的別名,如果你修改@的元素,就會改變原始參數,所以要小心!
sub modify_name
{
$_[0] = reverse $_[0];
}
my $name = 'Orange';
modify_name( $name );
say $name;
# prints egnarO
函數和名字空間
跟變量一樣,函數也有名字空間。如果未指定,默認就是main的名字空間。
你也可以明確指定名字空間:
sub Extensions::Math::add { ... }
如果在同一個名字空間中聲明了多個同名的函數,Perl會報錯。
你可以直接使用函數名來調用所在名字空間內的函數;要調用其他名字空間的函數則需要使用完全限定名:
#調用其他名字空間的函數
package main;
Extensions::Math::add( $scalar, $vector );
導入(Importing)
當使用關鍵字use 加載一個模塊時,Perl就會自動調用一個叫import()的方法。
模塊可以有自己的的import()方法。放在模塊名字后面的內容會成為模塊import()方法的參數。
use strict;
#這句的意思就是加載strict.pm模塊,
#然后調用strict->import()方法(沒有參數)。
use strict 'refs';
use strict qw( subs vars );
#加載strict.pm模塊,
#然后調用strict->import( 'refs' ),
#再調用 strict->import( 'subs', vars' )。
你也可以直接顯式調用import()方法。和上面的例子等價:
BEGIN
{
require strict;
strict->import( 'refs' );
strict->import( qw( subs vars ) );
}
報告錯誤
使用內置函數caller可獲取該函數被調用的情況。
無參數caller返回一個列表,包含有調用者的包名,調用者的文件名,和調用發生的位置(在文件中的哪一行調用的):
package main;
my_call();
sub my_call
{
show_call_information();
}
sub show_call_information
{
my ($package, $file, $line) = caller();
say "Called from $package in $file:$line";
}
caller還接受一個整型參數n,返回n層嵌套外調用的情況。本例中:
caller(0)會上溯到在my_call中被調用的信息;
caller(1) 會上溯到在程序中被調用的信息;
#額外的會返回一個函數名
sub show_call_information
{
my ($package, $file, $line, $func) = caller(0);
say "Called $func from $package in $file:$line";
}
Carp模塊就是使用caller來報告錯誤和警告信息的。croak()從調用者的角度拋出異常,carp()報告位置。
驗證參數
某些時候參數驗證是很容易的,比如驗證參數的個數:
sub add_numbers
{
croak 'Expected two numbers, received: ' . @_
unless @_ == 2;
...
}
有時則比較麻煩,比如要驗證參數的類型。因為Perl中,類型可以發生轉換。如果你有這方面的需求,可以看看這些模塊:Params::Validate和MooseX::Method::Signatures。
函數進階
語境感知
Perl的內置函數wantarray具有感知函數調用語境的功能。
wantarray在空語境下返回undef;標量語境返回假;列表語境返回真。
sub context_sensitive
{
my $context = wantarray();
return qw( List context ) if $context;
say 'Void context' unless defined $context;
return 'Scalar context' unless $context;
}
context_sensitive();
say my $scalar = context_sensitive();
say context_sensitive();
CPAN上也有提供語境感知功能的模塊如Want 和 Contextual::Return,功能非常強大。
遞歸
遞歸是算法中常用的思想。
假設你現在要在一個排序后的數組中找到某個值,可以對數組中的每一個元素進行迭代,挨個對比,這肯定能找到。但是平均來說需要訪問一半的數組元素才能找到目標值。
還有另一種思路,就是先找出數組的中間位置元素,將目標值和中間元素對比,如果比中間元素的值大就只需要在后半組找,否則就在前半組找,這樣更有效率。代碼如下:
use Test::More;
my @elements =
(
1, 5, 6, 19, 48, 77, 997, 1025, 7777, 8192, 9999
);
ok elem_exists( 1, @elements ),
'found first element in array';
ok elem_exists( 9999, @elements ),
'found last element in array';
ok ! elem_exists( 998, @elements ),
'did not find element not in array';
ok ! elem_exists( -1, @elements ),
'did not find element not in array';
ok ! elem_exists( 10000, @elements ),
'did not find element not in array';
ok elem_exists( 77, @elements ),
'found midpoint element';
ok elem_exists( 48, @elements ),
'found end of lower half element';
ok elem_exists( 997, @elements ),
'found start of upper half element';
done_testing();
sub elem_exists
{
my ($item, @array) = @_;
# break recursion with no elements to search
return unless @array;
# bias down with odd number of elements
my $midpoint = int( (@array / 2) - 0.5 );
my $miditem = $array[ $midpoint ];
# return true if found
return 1 if $item == $miditem;
# return false with only one element
return if @array == 1;
# split the array down and recurse
return elem_exists(
$item, @array[0 .. $midpoint]
) if $item < $miditem;
# split the array and recurse
return elem_exists(
$item, @array[ $midpoint + 1 .. $#array ]
);
}
需要注意的是,每次調用時參數都不一樣,否則就會出現死循環(一直做同樣的事情,跳不出來),所以終止條件非常重要。
遞歸的程序都可以使用非遞歸的方式來替代實現。
詞法變量
函數中盡量使用詞法變量,這樣才能保證作用域最小,函數之間能保持相互獨立互不影響。比如在遞歸中,使用詞法變量,每次重復調用自身就不會引起沖突。
尾部調用
遞歸有個缺點:如果處理不小心就容易進入死循環--調用自身無限多次。
遞歸過深,還會消耗大量內存。使用尾部調用可以避免這個問題。
# split the array down and recurse
return elem_exists(
$item, @array[0 .. $midpoint]
) if $item < $miditem;
# split the array and recurse
return elem_exists(
$item, @array[ $midpoint + 1 .. $#array ]
);
尾部調用會直接返回函數的結果。而不是等待子函數返回后,再返回給調用者。也可以使用goto達到相同的效果:
# split the array down and recurse
if ($item < $miditem)
{
@_ = ($item, @array[0 .. $midpoint]);
goto &elem_exists;
}
# split the array up and recurse
else
{
@_ = ($item, @array[$midpoint + 1 .. $#array] );
goto &elem_exists;
}
```
有時候這些寫法看起來確實丑,但是如果你的代碼高度遞歸以至于會跑爆內存,就顧不上這么多了。
#不合理的特性
由于歷史原因,Perl還支持老舊的函數調用方法:
```
# outdated style; avoid
my $result = &calculate_result( 52 );
# Perl 1 style; avoid
my $result = do calculate_result( 42 );
# crazy mishmash; really truly avoid
my $result = do &calculate_result( 42 );
```
忘了這些吧, 使用括號!
####作用域
作用域就是指生命周期和作用范圍。
Perl中任何有名字的東西都有作用域。控制作用域有助于進行良好的封裝。
****詞法作用域****
使用關鍵字my來聲明詞法作用域變量。
詞法作用域變量的有效范圍(作用域)有兩種情況:
1從聲明開始持續到該文件結尾;
2由大括號限定,括號內持續有效(當然內部嵌套也有效)。
```
{
package Robot::Butler
# 括號內作用域1
my $battery_level;
sub tidy_room{
# 嵌套函數作用域2
my $timer;
do {
#最內層函數的作用域3
my $dustpan;
...
} while (@_);
#
for (@_){
#最內層函數的作用域4
my $polish_cloth;
...
}
}
}
# 超出了作用域
#$battery_level在1234中均有效;
#$timer在34中有效;
#$dustpan在3中有效;
#$polish_cloth在4中有效
#超出作用域后4個變了均失效了。
```
在嵌套范圍內聲明同名變量會暫時屏蔽外面的那個變量:
```
my $name = 'Jacob';
{
my $name = 'Edward';
say $name;
#Edward
}
say $name;
#Jacob
```
****全局作用域****
比詞法作用域更廣的是全局作用域。全局作用域變量使用關鍵字our來聲明。
****動態作用域****
有些場景可能會需要用到全局變量,但是要限制在小范圍內暫時賦值,這就得用到關鍵字local了。
使用local可以對全局變量進行賦值,但是作用范圍僅限制在本詞法作用域內,超出后回歸原值。
```
our $scope;
sub inner
{
say $scope;
}
sub main
{
say $scope;
local $scope = 'main() scope';
middle();
}
sub middle
{
say $scope;
inner();
}
$scope = 'outer scope';
main();
say $scope;
#outer scope
#main() scope
#main() scope
#outer scope
```
詞法作用變量依附于代碼塊,存儲在一個叫“詞法板”的數據結構里,程序每進入到一個作用域時就創建一個新的詞法板來記錄詞法變量以供臨時使用。
全局變量存儲在符號表里,每個包都有一個符號表,里面存儲著包全局變量和函數記錄。導入機制就使用符號表來工作,這就是為什么要使用local本地化(臨時化)全局變量,而不直接使用詞法變量的原因。
local有個常見的使用場景就是和魔法變量一起使用。比如讀取文件時本地化$/;本地化緩沖控制變量&|等。
****state****
使用關鍵字state聲明的變量,行為上類似詞法變量但只初始化一次:
```
sub counter
{
state $count = 1;
return $count++;
}
say counter();
say counter();
say counter();
sub counter {
state $count = shift;
return $count++;
}
say counter(2);
say counter(4);
say counter(6);
#打印的是2 3 4
```
#匿名函數
沒有名字的函數就叫匿名函數。匿名函數的行為和有名字的函數類似,但是因為沒有名字所以只能通過引用來訪問。
Perl中的一個經典用法:調度表。
```
my %dispatch =
(
plus => \&add_two_numbers,
minus => \&subtract_two_numbers,
times => \&multiply_two_numbers,
);
sub add_two_numbers { $_[0] + $_[1] }
sub subtract_two_numbers { $_[0] - $_[1] }
sub multiply_two_numbers { $_[0] * $_[1] }
sub dispatch
{
my ($left, $op, $right) = @_;
return unless exists $dispatch{ $op };
return $dispatch{ $op }->( $left, $right );
}
```
####聲明匿名函數
使用關鍵字sub不帶名字來創建和返回一個匿名函數。可以在使用函數(有名字)引用的地方使用這個匿名函數引用。現在就用匿名函數來改寫調度表:
```
my %dispatch =
(
plus => sub { $_[0] + $_[1] },
minus => sub { $_[0] - $_[1] },
times => sub { $_[0] * $_[1] },
dividedby => sub { $_[0] / $_[1] },
raisedto => sub { $_[0] ** $_[1] },
);
```
你可能也見過匿名函數作為參數傳遞的:
```
sub invoke_anon_function
{
my $func = shift;
return $func->( @_ );
}
sub named_func
{
say 'I am a named function!';
}
invoke_anon_function( \&named_func );
invoke_anon_function( sub { say 'Who am I?' } );
```
####偵測匿名函數
偵測一個函數是不是匿名函數,要用到之前提過的知識:
```
package ShowCaller;
sub show_caller
{
my ($package, $file, $line, $sub) = caller(1);
say "Called from $sub in $package:$file:$line";
}
sub main
{
my $anon_sub = sub { show_caller() };
show_caller();
$anon_sub->();
}
main();
#Called from ShowCaller::main
#in ShowCaller:anoncaller.pl:20
#Called from ShowCaller::__ANON__
#in ShowCaller:anoncaller.pl:17
```
其中__ANON__就表示是匿名函數。CPAN上也有模塊可以允許你用為匿名函數“命名"。
```
use Sub::Name;
use Sub::Name;
use Sub::Identify 'sub_name';
my $anon = sub {};
say sub_name( $anon );
my $named = subname( 'pseudo-anonymous', $anon );
say sub_name( $named );
say sub_name( $anon );
say sub_name( sub {} );
#__ANON__
#pseudo-anonymous
#pseudo-anonymous
#__ANON__
```
####隱式匿名函數
Perl允許你不使用關鍵字sub就能聲明一個匿名函數作為函數參數。如map和eval。
CPAN模塊Test::Fatal也可以,將匿名函數作為第一個參數傳給exception()方法:
```
use Test::More;
use Test::Fatal;
my $croaker = exception { die 'I croak!' };
my $liver = exception { 1 + 1 };
like( $croaker, qr/I croak/, 'die() should croak' );
is( $liver, undef, 'addition should live' );
done_testing();
```
更詳細的寫法:
```
my $croaker = exception( sub { die 'I croak!' } );
my $liver = exception( sub { 1 + 1 } );
```
當然也可傳有名字的函數引用:
```
sub croaker { die 'I croak!' }
sub liver { 1 + 1 }
my $croaker = exception \&croaker;
my $liver = exception \&liver;
like( $croaker, qr/I croak/, 'die() should die' );
is( $liver, undef, 'addition should live' );
```
但是不能傳遞標量引用:
```
my $croak_ref = \&croaker;
my $live_ref = \&liver;
# BUGGY: does not work
my $croaker = exception $croak_ref;
my $liver = exception $live_ref;
#這是原型限制的問題
```
函數接受多個參數并且第一個參數是匿名函數時,函數塊后不能有逗號:
```
use Test::More;
use Test::Fatal 'dies_ok';
dies_ok { die 'This is my boomstick!' } 'No movie references here';
```
#閉包
計算機科學中的術語--高階函數,指的就是函數的函數.
每一次當程序運行進入到一個函數時,函數就得到了表示該詞法范圍的新環境(當然匿名函數也一樣)。這個機制蘊含的力量是強大的,閉包就展示了這種力量。
####創建閉包
閉包是這樣一個函數:它使用詞法變量,并且在超出詞法作用域后還可以讀取該詞法變量。
你可能沒有意識到你已經使用過了:
```
use Modern::Perl '2014';
my $filename = shift @ARGV;
sub get_filename { return $filename }
```
get_filename函數可以訪問詞法變量$filename,沒什么神奇的,就是正常的作用域。
現在設想你要迭代一個列表,但是又不想自己來管理迭代器,你可以這樣做:返回一個函數,并且在調用時,迭代下一個項目。
```
sub make_iterator
{
my @items = @_;
my $count = 0;
return sub
{
return if $count == @items;
return $items[ $count++ ];
}
}
my $cousins = make_iterator(qw(
Rick Alex Kaycee Eric Corey Mandy Christine Alex
));
say $cousins->() for 1 .. 6;
```
盡管make_iterator()已經結束并返回,但是函數中的匿名函數已經和里面的環境關聯起來了,(還記得Perl的內存管理機制,引用計數么),所以仍然能夠訪問。
每次調用make_iterator()都會產生獨立的詞法環境,匿名函數創建并保持這個獨立的環境。(所以每次產生的匿名函數環境互不影響)
```
my $aunts = make_iterator(qw(
Carole Phyllis Wendy Sylvia Monica Lupe
));
say $cousins->();
say $aunts->();
```
這種情況下只有子函數($aunts->())能夠訪問里面的變量,其他任何Perl代碼都不能訪問它們,所以這也是一個很好的封裝。
```
{
my $private_variable;
sub set_private { $private_variable = shift }
sub get_private { $private_variable }
}
```
不過要知道,你不能嵌套有名字的函數。有名字的函數是包名全局的。
####使用閉包
使用閉包來迭代列表非常好用,但是閉包能做的遠不限于此。考慮一個函數來創建非波拉契數列:
```
sub gen_fib
{
my @fibs = (0, 1);
return sub
{
my $item = shift;
if ($item >= @fibs)
{
for my $calc (@fibs .. $item)
{
$fibs[$calc] = $fibs[$calc - 2]
+ $fibs[$calc - 1];
}
}
return $fibs[$item];
}
}
# calculate 42nd Fibonacci number
my $fib = gen_fib();
say $fib->( 42 );
```
此段代碼不僅實現了功能,內部還附帶緩存,代碼非常簡潔!
使用閉包可以制作靈活多變的函數,對此《高階Perl》里面有非常精彩的講述。
#state還是閉包
了解了前面的介紹,我們發現使用state也能實現和閉包相似的功能。這意味著某些情況下你可以任意挑選一個喜歡的方式來實現你的需求。
另外關鍵字state也是可以和匿名函數一起工作的:
```
sub make_counter
{
return sub
{
state $count = 0;
return $count++;
}
}
```
#屬性
Perl中所有有名字的東西--比如變量、函數,都可以附帶額外的信息數據,這個就是屬性。不過這種語法通常都很丑,所以并不常見。有興趣的可以自行查看系統文檔。
#AUTOLOAD
如果你沒有調用一個沒有聲明的函數通常會有異常:
```
use Modern::Perl;
bake_pie( filling => 'apple' );
```
Perl會說調用了未定義的函數。現在我們在后面增加一個AUTOLOAD()的函數:
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD {}
```
再運行,居然不報錯了。這是因為當調度失敗時,Perl會去調用一個叫AUTOLOAD()的函數。
增加點信息就更明確了:
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD { say 'In AUTOLOAD()!' }
#輸出:In AUTOLOAD()!
```
所有傳給未定義的函數的參數都被AUTOLOAD()函數接受并將放到@_ ,并且會將完全限定函數名放在$AUTOLOAD變量里。
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD
{
our $AUTOLOAD;
# pretty-print the arguments
local $" = ', ';
say "In AUTOLOAD(@_) for $AUTOLOAD!"
}
```
AUTOLOAD()函數很有用,利用它我們可以做很多事,但是會一定程度上讓程序變得不易讀,所以應避免使用。