第五章 Perl函数

作者: 可以没名字吗 | 来源:发表于2016-02-26 11:28 被阅读1366次

    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()函数很有用,利用它我们可以做很多事,但是会一定程度上让程序变得不易读,所以应避免使用。

    相关文章

      网友评论

        本文标题:第五章 Perl函数

        本文链接:https://www.haomeiwen.com/subject/wxcikttx.html