Perl中,函數(shù)(又稱子程序)是一個封裝的行為單元。
函數(shù)可以有自己的名字,可以接受輸入,可以產(chǎn)生輸出,它是Perl程序用來抽象、封裝和重用的一種主要機制。
聲明函數(shù)
使用關鍵字sub來聲明并定義一個函數(shù):
sub greet_me { print "ok"; }
聲明后你就可以使用這個函數(shù)了。
就像聲明變量不用立即賦值,你也可以先聲明一個函數(shù)而不立即定義它。
sub greet_sun;
#先聲明,后續(xù)再定義
調(diào)用函數(shù)
對函數(shù)名字使用后綴括號來調(diào)用該函數(shù),參數(shù)放在括號內(nèi):
greet_me( 'Jack', 'Tuxie' );
greet_me( 'Snowy' );
greet_me();
括號并不是必須的,不過有括號能極大的提高可讀性。這是個好習慣。
函數(shù)參數(shù)可以是任意類型:
greet_me( $name );
greet_me( @authors );
greet_me( %editors );
greet_me( get_readers() );
函數(shù)參數(shù)
在 “Perl哲學” 那章我們介紹過,一個函數(shù)收到的參數(shù)都在@_數(shù)組里面;當調(diào)用函數(shù)時,Perl會將所有的參數(shù)都“壓平”放進一個列表里。
函數(shù)可以讀取@_的內(nèi)容并賦值給新的變量,也可以直接操作@_數(shù)組:
sub greet_one
{
my ($name) = @_;
say "Hello, $name!";
}
sub greet_all
{
say "Hello, $_!" for @_;
}
通常編寫函數(shù)時會使用shift來卸載參數(shù)。當然也可以使用列表賦值來讀取參數(shù),還可以直接用數(shù)組索引來訪問需要的參數(shù):
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]!";
}
@_就是一個普通的數(shù)組,你可以使用所有數(shù)組相關的操作符來操作@_,如unshift, push, pop, splice, slice 。
有些操作符的默認操作數(shù)就是@_,這時你就可以偷懶了:
my $name = shift;
某些情形下使用列表賦值會更清晰,參照以下2段功能相同的代碼:
#單個卸載
my $left_value = shift;
my $operation = shift;
my $right_value = shift;
#列表賦值
my ($left_value, $operation, $right_value) = @_;
這種情況時第2種寫法更簡單,可讀性更好,效率也更高。
通常來說,****當你只需要一個參數(shù)時使用shift;讀取多個參數(shù)時使用列表賦值。****
展平
調(diào)用函數(shù)時,參數(shù)列表會被壓平放進@_里面,所以將哈希作為參數(shù)傳遞進去時,就會展平成一系列的鍵值對:
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";
}
}
當哈希展平成一個列表時,鍵值對與鍵值對之間的順序是不確定的,但是鍵和值之間是有規(guī)律的:鍵后面肯定是對應的值。
在標量參數(shù)和列表參數(shù)混合使用時需要小心處理參數(shù),看下面這個例子:
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參數(shù)的位置是在后面,那么就會報錯,因為所有的值先被哈希吞掉,但是卻發(fā)現(xiàn)單了一個。這時可以這樣做:
sub show_pets_by_type
{
my $type = pop;
my %pets = @_;
...
}
當然還可以使用傳遞引用的方式來實現(xiàn)。
別名
@就是參數(shù)的別名,如果你修改@的元素,就會改變原始參數(shù),所以要小心!
sub modify_name
{
$_[0] = reverse $_[0];
}
my $name = 'Orange';
modify_name( $name );
say $name;
# prints egnarO
函數(shù)和名字空間
跟變量一樣,函數(shù)也有名字空間。如果未指定,默認就是main的名字空間。
你也可以明確指定名字空間:
sub Extensions::Math::add { ... }
如果在同一個名字空間中聲明了多個同名的函數(shù),Perl會報錯。
你可以直接使用函數(shù)名來調(diào)用所在名字空間內(nèi)的函數(shù);要調(diào)用其他名字空間的函數(shù)則需要使用完全限定名:
#調(diào)用其他名字空間的函數(shù)
package main;
Extensions::Math::add( $scalar, $vector );
導入(Importing)
當使用關鍵字use 加載一個模塊時,Perl就會自動調(diào)用一個叫import()的方法。
模塊可以有自己的的import()方法。放在模塊名字后面的內(nèi)容會成為模塊import()方法的參數(shù)。
use strict;
#這句的意思就是加載strict.pm模塊,
#然后調(diào)用strict->import()方法(沒有參數(shù))。
use strict 'refs';
use strict qw( subs vars );
#加載strict.pm模塊,
#然后調(diào)用strict->import( 'refs' ),
#再調(diào)用 strict->import( 'subs', vars' )。
你也可以直接顯式調(diào)用import()方法。和上面的例子等價:
BEGIN
{
require strict;
strict->import( 'refs' );
strict->import( qw( subs vars ) );
}
報告錯誤
使用內(nèi)置函數(shù)caller可獲取該函數(shù)被調(diào)用的情況。
無參數(shù)caller返回一個列表,包含有調(diào)用者的包名,調(diào)用者的文件名,和調(diào)用發(fā)生的位置(在文件中的哪一行調(diào)用的):
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還接受一個整型參數(shù)n,返回n層嵌套外調(diào)用的情況。本例中:
caller(0)會上溯到在my_call中被調(diào)用的信息;
caller(1) 會上溯到在程序中被調(diào)用的信息;
#額外的會返回一個函數(shù)名
sub show_call_information
{
my ($package, $file, $line, $func) = caller(0);
say "Called $func from $package in $file:$line";
}
Carp模塊就是使用caller來報告錯誤和警告信息的。croak()從調(diào)用者的角度拋出異常,carp()報告位置。
驗證參數(shù)
某些時候參數(shù)驗證是很容易的,比如驗證參數(shù)的個數(shù):
sub add_numbers
{
croak 'Expected two numbers, received: ' . @_
unless @_ == 2;
...
}
有時則比較麻煩,比如要驗證參數(shù)的類型。因為Perl中,類型可以發(fā)生轉換。如果你有這方面的需求,可以看看這些模塊:Params::Validate和MooseX::Method::Signatures。
函數(shù)進階
語境感知
Perl的內(nèi)置函數(shù)wantarray具有感知函數(shù)調(diào)用語境的功能。
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,功能非常強大。
遞歸
遞歸是算法中常用的思想。
假設你現(xiàn)在要在一個排序后的數(shù)組中找到某個值,可以對數(shù)組中的每一個元素進行迭代,挨個對比,這肯定能找到。但是平均來說需要訪問一半的數(shù)組元素才能找到目標值。
還有另一種思路,就是先找出數(shù)組的中間位置元素,將目標值和中間元素對比,如果比中間元素的值大就只需要在后半組找,否則就在前半組找,這樣更有效率。代碼如下:
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 ]
);
}
需要注意的是,每次調(diào)用時參數(shù)都不一樣,否則就會出現(xiàn)死循環(huán)(一直做同樣的事情,跳不出來),所以終止條件非常重要。
遞歸的程序都可以使用非遞歸的方式來替代實現(xiàn)。
詞法變量
函數(shù)中盡量使用詞法變量,這樣才能保證作用域最小,函數(shù)之間能保持相互獨立互不影響。比如在遞歸中,使用詞法變量,每次重復調(diào)用自身就不會引起沖突。
尾部調(diào)用
遞歸有個缺點:如果處理不小心就容易進入死循環(huán)--調(diào)用自身無限多次。
遞歸過深,還會消耗大量內(nèi)存。使用尾部調(diào)用可以避免這個問題。
# 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 ]
);
尾部調(diào)用會直接返回函數(shù)的結果。而不是等待子函數(shù)返回后,再返回給調(diào)用者。也可以使用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;
}
```
有時候這些寫法看起來確實丑,但是如果你的代碼高度遞歸以至于會跑爆內(nèi)存,就顧不上這么多了。
#不合理的特性
由于歷史原因,Perl還支持老舊的函數(shù)調(diào)用方法:
```
# 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中任何有名字的東西都有作用域??刂谱饔糜蛴兄谶M行良好的封裝。
****詞法作用域****
使用關鍵字my來聲明詞法作用域變量。
詞法作用域變量的有效范圍(作用域)有兩種情況:
1從聲明開始持續(xù)到該文件結尾;
2由大括號限定,括號內(nèi)持續(xù)有效(當然內(nèi)部嵌套也有效)。
```
{
package Robot::Butler
# 括號內(nèi)作用域1
my $battery_level;
sub tidy_room{
# 嵌套函數(shù)作用域2
my $timer;
do {
#最內(nèi)層函數(shù)的作用域3
my $dustpan;
...
} while (@_);
#
for (@_){
#最內(nèi)層函數(shù)的作用域4
my $polish_cloth;
...
}
}
}
# 超出了作用域
#$battery_level在1234中均有效;
#$timer在34中有效;
#$dustpan在3中有效;
#$polish_cloth在4中有效
#超出作用域后4個變了均失效了。
```
在嵌套范圍內(nèi)聲明同名變量會暫時屏蔽外面的那個變量:
```
my $name = 'Jacob';
{
my $name = 'Edward';
say $name;
#Edward
}
say $name;
#Jacob
```
****全局作用域****
比詞法作用域更廣的是全局作用域。全局作用域變量使用關鍵字our來聲明。
****動態(tài)作用域****
有些場景可能會需要用到全局變量,但是要限制在小范圍內(nèi)暫時賦值,這就得用到關鍵字local了。
使用local可以對全局變量進行賦值,但是作用范圍僅限制在本詞法作用域內(nèi),超出后回歸原值。
```
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
```
詞法作用變量依附于代碼塊,存儲在一個叫“詞法板”的數(shù)據(jù)結構里,程序每進入到一個作用域時就創(chuàng)建一個新的詞法板來記錄詞法變量以供臨時使用。
全局變量存儲在符號表里,每個包都有一個符號表,里面存儲著包全局變量和函數(shù)記錄。導入機制就使用符號表來工作,這就是為什么要使用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
```
#匿名函數(shù)
沒有名字的函數(shù)就叫匿名函數(shù)。匿名函數(shù)的行為和有名字的函數(shù)類似,但是因為沒有名字所以只能通過引用來訪問。
Perl中的一個經(jīng)典用法:調(diào)度表。
```
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 );
}
```
####聲明匿名函數(shù)
使用關鍵字sub不帶名字來創(chuàng)建和返回一個匿名函數(shù)。可以在使用函數(shù)(有名字)引用的地方使用這個匿名函數(shù)引用?,F(xiàn)在就用匿名函數(shù)來改寫調(diào)度表:
```
my %dispatch =
(
plus => sub { $_[0] + $_[1] },
minus => sub { $_[0] - $_[1] },
times => sub { $_[0] * $_[1] },
dividedby => sub { $_[0] / $_[1] },
raisedto => sub { $_[0] ** $_[1] },
);
```
你可能也見過匿名函數(shù)作為參數(shù)傳遞的:
```
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?' } );
```
####偵測匿名函數(shù)
偵測一個函數(shù)是不是匿名函數(shù),要用到之前提過的知識:
```
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__就表示是匿名函數(shù)。CPAN上也有模塊可以允許你用為匿名函數(shù)“命名"。
```
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__
```
####隱式匿名函數(shù)
Perl允許你不使用關鍵字sub就能聲明一個匿名函數(shù)作為函數(shù)參數(shù)。如map和eval。
CPAN模塊Test::Fatal也可以,將匿名函數(shù)作為第一個參數(shù)傳給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 } );
```
當然也可傳有名字的函數(shù)引用:
```
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;
#這是原型限制的問題
```
函數(shù)接受多個參數(shù)并且第一個參數(shù)是匿名函數(shù)時,函數(shù)塊后不能有逗號:
```
use Test::More;
use Test::Fatal 'dies_ok';
dies_ok { die 'This is my boomstick!' } 'No movie references here';
```
#閉包
計算機科學中的術語--高階函數(shù),指的就是函數(shù)的函數(shù).
每一次當程序運行進入到一個函數(shù)時,函數(shù)就得到了表示該詞法范圍的新環(huán)境(當然匿名函數(shù)也一樣)。這個機制蘊含的力量是強大的,閉包就展示了這種力量。
####創(chuàng)建閉包
閉包是這樣一個函數(shù):它使用詞法變量,并且在超出詞法作用域后還可以讀取該詞法變量。
你可能沒有意識到你已經(jīng)使用過了:
```
use Modern::Perl '2014';
my $filename = shift @ARGV;
sub get_filename { return $filename }
```
get_filename函數(shù)可以訪問詞法變量$filename,沒什么神奇的,就是正常的作用域。
現(xiàn)在設想你要迭代一個列表,但是又不想自己來管理迭代器,你可以這樣做:返回一個函數(shù),并且在調(diào)用時,迭代下一個項目。
```
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()已經(jīng)結束并返回,但是函數(shù)中的匿名函數(shù)已經(jīng)和里面的環(huán)境關聯(lián)起來了,(還記得Perl的內(nèi)存管理機制,引用計數(shù)么),所以仍然能夠訪問。
每次調(diào)用make_iterator()都會產(chǎn)生獨立的詞法環(huán)境,匿名函數(shù)創(chuàng)建并保持這個獨立的環(huán)境。(所以每次產(chǎn)生的匿名函數(shù)環(huán)境互不影響)
```
my $aunts = make_iterator(qw(
Carole Phyllis Wendy Sylvia Monica Lupe
));
say $cousins->();
say $aunts->();
```
這種情況下只有子函數(shù)($aunts->())能夠訪問里面的變量,其他任何Perl代碼都不能訪問它們,所以這也是一個很好的封裝。
```
{
my $private_variable;
sub set_private { $private_variable = shift }
sub get_private { $private_variable }
}
```
不過要知道,你不能嵌套有名字的函數(shù)。有名字的函數(shù)是包名全局的。
####使用閉包
使用閉包來迭代列表非常好用,但是閉包能做的遠不限于此??紤]一個函數(shù)來創(chuàng)建非波拉契數(shù)列:
```
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 );
```
此段代碼不僅實現(xiàn)了功能,內(nèi)部還附帶緩存,代碼非常簡潔!
使用閉包可以制作靈活多變的函數(shù),對此《高階Perl》里面有非常精彩的講述。
#state還是閉包
了解了前面的介紹,我們發(fā)現(xiàn)使用state也能實現(xiàn)和閉包相似的功能。這意味著某些情況下你可以任意挑選一個喜歡的方式來實現(xiàn)你的需求。
另外關鍵字state也是可以和匿名函數(shù)一起工作的:
```
sub make_counter
{
return sub
{
state $count = 0;
return $count++;
}
}
```
#屬性
Perl中所有有名字的東西--比如變量、函數(shù),都可以附帶額外的信息數(shù)據(jù),這個就是屬性。不過這種語法通常都很丑,所以并不常見。有興趣的可以自行查看系統(tǒng)文檔。
#AUTOLOAD
如果你沒有調(diào)用一個沒有聲明的函數(shù)通常會有異常:
```
use Modern::Perl;
bake_pie( filling => 'apple' );
```
Perl會說調(diào)用了未定義的函數(shù)?,F(xiàn)在我們在后面增加一個AUTOLOAD()的函數(shù):
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD {}
```
再運行,居然不報錯了。這是因為當調(diào)度失敗時,Perl會去調(diào)用一個叫AUTOLOAD()的函數(shù)。
增加點信息就更明確了:
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD { say 'In AUTOLOAD()!' }
#輸出:In AUTOLOAD()!
```
所有傳給未定義的函數(shù)的參數(shù)都被AUTOLOAD()函數(shù)接受并將放到@_ ,并且會將完全限定函數(shù)名放在$AUTOLOAD變量里。
```
use Modern::Perl;
bake_pie( filling => 'apple' );
sub AUTOLOAD
{
our $AUTOLOAD;
# pretty-print the arguments
local $" = ', ';
say "In AUTOLOAD(@_) for $AUTOLOAD!"
}
```
AUTOLOAD()函數(shù)很有用,利用它我們可以做很多事,但是會一定程度上讓程序變得不易讀,所以應避免使用。