第五章 Perl函數(shù)

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ù)很有用,利用它我們可以做很多事,但是會一定程度上讓程序變得不易讀,所以應避免使用。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容