美文网首页编程学习程序员首页投稿(暂停使用,暂停投稿)
第三章 Perl语言(五)-包、引用、复杂数据

第三章 Perl语言(五)-包、引用、复杂数据

作者: 可以没名字吗 | 来源:发表于2016-02-22 14:04 被阅读570次

    Perl中的包就是单一名字空间下的代码集合。包与名字空间的区别是:包关注点是源代码;而名字空间是Perl用来组织和管理代码的内部数据结构。
    使用package来声明一个包和名字空间(创建了包也就创建了名字空间):

    package MyCode;
    our @boxes;
    sub add_box { ... }
    

    声明之后,接下来所有定义的变量和函数都将处在MyCode的名字空间中。这个包声明的管辖范围是直到遇到下一个package声明或者是持续直到文件结尾。
    还可以使用块来指定声明范围:

    package Pinball::Wizard
    {
    our $VERSION = 1969;
    }
    
    

    如果没有包声明,默认的都是在main包中。以前我们讲过了,要访问其他包(名字空间)里面的变量和函数要使用完全限定名。比如在main包中使用@MyCode::boxes来访问MyCode中的boxes数组。

    包除了有名字,还有版本号和三个函数:import()、unimport()、VERSION() 。函数VERSION() 返回包的版本号,也就是包中$VERSION的值。Perl对版本号有格式上的要求:以字母v开头和至少三个以点号(.)隔开整数。

    package MyCode v1.2.1;
    
    package Pinball::Wizard v1969.3.7 { ... }
    
    #分开写,老式的版本号写法
    package MyCode;
    our $VERSION = 1.21;
    

    每个包都从UNIVERSAL类中继承了VERSION()函数。

    my $version = Some::Plugin->VERSION;
    #返回$VERSION的值
    
    你也可以向函数传递(版本号)参数,如果模块的版本号小于你传递的版本号,函数会抛出异常:
    # require at least 2.1
    Some::Plugin->VERSION( 2.1 );
    die "Your plugin $version is too old" unless $version > 2;
    
    

    包和名字空间

    在编译期间和运行时你可以在任何地方访问或修改一个包的内容,不过这可能会让代码非常难读。
    很多项目都会创建自己的顶级名字空间,这样就减少全局变量冲突的可能性,同时也能更好的组织代码,例如:

    • StrangeMonkey 是项目名字
    • StrangeMonkey::UI 用户接口代码
    • StrangeMonkey::Persistence 数据处理代码
    • StrangeMonkey::Test 测试代码
    

    这只是一种约定,不是强制的,但是这种约定很通用。

    引用

    先看个例子:

    sub reverse_greeting
    {
    my $name = reverse shift;
    return "Hello, $name!";
    }
    
    my $name = 'Chuck';
    say reverse_greeting( $name );
    say $name;
    

    我们期望通过一个函数来实现反转功能。但是例子中的代码却是这样工作的:将变量值传递给函数,在函数内部进行反转。但是完成后,出了函数,变量值还是没变(还是Chuck)。

    再考虑一种情形:有一个值,你复制了很多份在不同的地方,现在这个值需要修改下,那么你想在每个地方都去改一下?有没有一种机制可以实现一处修改,多出生效呢?可以,这就要用到引用了。

    标量引用

    使用反斜杠创建引用。在标量语境,创建单个的引用;在列表语境,创建一系列的引用。
    对引用加记号($)就能访问所引用的值了,这就是解引用。

    my $name = 'Larry';
    my $name_ref = \$name;
    
    #通过引用访问值
    say $$name_ref;
    

    将上面反转的例子用引用来实现:

    sub reverse_in_place
    {
    my $name_ref = shift;
    $$name_ref = reverse $$name_ref;  #解引用的用法
    }
    my $name = 'Blabby';
    reverse_in_place( \$name );
    say $name;
    

    参数@_就是参数变量的别名,所以还能这样也能实现同样的效果:

    sub reverse_value_in_place
    {
    $_[0] = reverse $_[0];
    }
    my $name = 'allizocohC';
    reverse_value_in_place( $name );
    say $name;
    
    #这样的写法很少见
    

    调用函数传递引用可以降低内存使用。
    这个很容易理解,对比下传递一个很大的字符串作为参数,和传递字符串的引用。

    复杂的引用可能需要使用花括号来消除歧义,这才是完整的语法,只不过大部分时候都省略了:

    sub reverse_in_place
    {
    my $name_ref = shift;
    ${ $name_ref } = reverse ${ $name_ref };
    }
    

    如果在使用时忘记了解引用,Perl会根据语境强制将转换为字符串SCALAR(0x93339e8)或数字0x93339e8。(不过这并不一定是变量实际的内存地址啊。

    数组引用

    数组引用有以下用途:

    • 在函数中传递和返回数组,而不是扁平的列表
    • 创建多维数组结构
    • 避免不必要的数组复制
    • 匿名数组

    声明一个数组引用:

    my @cards = qw( K Q J 10 9 8 7 6 5 4 3 2 A );
    my $cards_ref = \@cards;
    

    任何通过引用$cards_ref进行的修改将直接修改数组@cards。
    你可以通过使用记号@来解引用,访问整个数组。

    my $card_count = @$cards_ref;
    my @card_copy = @$cards_ref;
    

    使用箭头符号(->)访问单个元素:

    my $first_card = $cards_ref->[0];
    my $last_card = $cards_ref->[-1];
    

    也可以这样访问:

    my $first_-card = $$cards_ref[0];
    # 丑!
    

    切片:

    my @high_cards = @{ $cards_ref }[0 .. 2, -1];
    #强烈推荐使用花括号,提高可读性
    
    

    匿名数组,只能通过引用访问

    my $suits_ref = [qw( Monkeys Robots Dinos Cheese )];
    

    区分以下2种情况:

    #引用
    my @meals = qw( soup sandwiches pizza );
    my $sunday_ref = \@meals;
    my $monday_ref = \@meals;
    
    
    my @meals = qw( soup sandwiches pizza );
    my $sunday_ref = [ @meals ];
    my $monday_ref = [ @meals ];
    #这2个引用并不能修改@meals数组,自己体会下。
    

    哈希引用

    创建一个哈希引用:

    my %colors = (
    blue => 'azul',
    gold => 'dorado',
    red => 'rojo',
    yellow => 'amarillo',
    purple => 'morado',
    );
    my $colors_ref = \%colors;
    

    通过在引用前面使用记号%来访问和使用整个哈希:

    my @english_colors = keys %$colors_ref;
    my @spanish_colors = values %$colors_ref;
    

    使用箭头操作符(->)解引用来访问元素:

    sub translate_to_spanish
    {
    my $color = shift;
    return $colors_ref->{$color};
    # or return $$colors_ref{$color};
    }
    

    切片:

    my @colors = qw( red blue green );
    my @colores = @{ $colors_ref }{@colors};
    

    使用花括号,创建匿名哈希:

    my $food_ref = {
    'birthday cake' => 'la torta de cumpleaños',
    candy => 'dulces',
    cupcake => 'bizcochito',
    'ice cream' => 'helado',
    };
    

    函数引用

    对函数名使用引用操作符和&符号来创建函数引用。

    sub bake_cake { say 'Baking a wonderful cake!' };
    my $cake_ref = \&bake_cake;
    #使用&符号表示创建函数引用,若没有&则会执行函数,对返回值创建引用。
    

    使用关键字sub创建匿名函数:

    my $pie_ref = sub { say 'Making a delicious pie!' };
    

    通过引用调用函数:

    $cake_ref->();
    $pie_ref->();
    

    文件句柄引用

    在open和opendir操作符中使用词法变量时,这个变量就是文件句柄引用。这个句柄就是IO::File对象,可以直接调用对象方法:

    use autodie 'open';
    open my $out_fh, '>', 'output_file.txt';
    $out_fh->say( 'Have some text!' );
    

    老的代码可能使用的是IO::Handle对象,更老的代码可能使用的是符号引用:

    local *FH;
    open FH, "> $file" or die "Can't write '$file': $!";
    my $fh = \*FH;
    

    这些方式都仍然可以使用,但是我们建议最新的词法变量方式。

    引用计数

    Perl使用引用计数的方式来管理内存。
    每一个Perl变量都有一个计数器。增加一个引用,Perl就将计数器加一,引用减少就减一。当计数器为零时,Perl认为就可以安全的回收该变量了。

    考虑以下代码:

    say 'file not open';
    {
    open my $fh, '>', 'inner_scope.txt';
    $fh->say( 'file open here' );
    }
    say 'file closed here';
    

    变量$fh的作用域就是在块里面,当超出范围时变量失效,Perl将它的计数减一,变成零,Perl就回收对应的内存。
    你无需明白所有的技术细节。你只要明白在使用引用时是如何影响Perl的内存管理就够了。

    引用和函数

    当你向函数传递引用时,要小心了:在函数中可能会修改原来的值,这可能不是你想要的。如果要避免这个情况,你应该先将变量值复制到一个新变量:

    my @new_array = @{ $array_ref };
    my %new_hash = %{ $hash_ref };
    
    #对于复杂数据结构引用的复制,你可以考虑使用系统模块Storable的dclone()函数。
    

    嵌套数据结构

    有时候你可能需要嵌套的数据结构,比如你想创建一个多维数组,但下面这种方式可得不到你想要的:

    my @counts = qw( eenie miney moe );
    my @ducks = qw( huey dewey louie );
    my @game = qw( duck duck goose );
    my @famous_triplets = (
    @counts, @ducks, @game
    );
    

    这会把每一个数据展开合并成一个列表,赋值进一个数组,并不是什么多维数组。

    解决办法就是引用:

    #使用有名字的数组引用
    my @famous_triplets = (
    \@counts, \@ducks, \@game
    );
    
    
    #匿名引用创建多维结构:
    my @famous_triplets = (
    [qw( eenie miney moe )],
    [qw( huey dewey louie )],
    [qw( duck duck goose )],
    );
    
    #匿名哈希
    my %meals = (
    breakfast => { entree => 'eggs',
    side => 'hash browns' },
    lunch => { entree => 'panini',
    side => 'apple' },
    dinner => { entree => 'steak',
    side => 'avocado salad' },
    );
    

    最后一个元素项后面的逗号是可选的,加上它会方便以后添加元素。

    使用引用访问多维数据结构,箭头是可选的:

    #省略箭头
    my $nephew = $famous_triplets[1][2];
    my $meal = $meals{breakfast}{side};
    
    #某些情况有箭头反而显得多余
    my $last_nephew = $famous_triplets[1]->[2];
    my $meal_side = $meals{breakfast}->{side};
    
    #通过引用调用函数建议加上箭头:
    $actions{generous}{buy_food}->( $nephew, $meal );
    

    嵌套数据结构的内容如果是数组和哈希,通过括号增加可读性:

    my $nephew_count = @{ $famous_triplets[1] };
    my $dinner_courses = keys %{ $meals{dinner} };
    

    嵌套数据的切片:

    my ($entree, $side) =
    @{ $meals{breakfast} }{ qw( entree side ) };
    
    
    更清楚的方式:
    my $meal_ref = $meals{breakfast};
    my ($entree, $side) = @$meal_ref{qw( entree side )};
    
    my ($entree, $side) = @{ $_ }{qw( entree side )}
    for $meals{breakfast};
    

    perldoc perldsc 可以查看Perl中各种数据结构的使用例子。

    自动激活

    当你写下嵌套数据结构的一部分时,Perl会自动创建中间必要的过程:

    my @aoaoaoa;
    $aoaoaoa[0][0][0][0] = 'nested deeply';
    

    Perl会自动创建这个四维数组,每层数组包含一个元素。
    类似的,下面这句会自动创建一个哈希的哈希:

    my %hohoh;
    $hohoh{Robot}{Santa} = 'mostly harmful';
    

    这个行为就叫做自动激活。这很方便,但是也有可能在不经意间误解了你的真实意图(你自己写错了)。CPAN上有个autovivification的编译指令可以控制该特性启用和范围。

    调试嵌套数据结构

    调试嵌套数据结构是困难的,幸好有几个好工具。
    系统模块Data::Dumper可以把数据结构显示出来:

    use Data::Dumper;
    
    my $complex_structure = {
    numbers => [ 1 .. 3 ];
    letters => [ 'a' .. 'c' ],
    objects => {
    breakfast => $continental,
    lunch => $late_tea,
    dinner => $banquet,
    },
    };
    
    print Dumper( $complex_structure );
    
    $VAR1 = {
    'numbers' => [
    1,
    2,
    3
    ],
    'letters' => [
    'a',
    'b',
    'c'
    ],
    'meals' => {
    'dinner' => bless({...}, 'Dinner'),
    'lunch' => bless({...}, 'Lunch'),
    'breakfast' => bless({...}, 'Breakfast'),
    },
    };
    

    当然还有其他模块也能干这个事情:YAML::XS 和 JSON模块。开发者可能更倾向于使用这2模块,因为这2模块不会产生Perl代码,输出结果更清晰。,Data::Dumper则显示得非常详细。

    回路引用

    当存在回路引用(相互引用)的时候,引用记录器将永远不可能为0,Perl也就永远无法回收其内存。

    my $alice = { mother => '', father => '' };
    my $robin = { mother => '', father => '' };
    my $cianne = { mother => $alice, father => $robin };
    push @{ $alice->{children} }, $cianne;
    push @{ $robin->{children} }, $cianne;
    

    可以使用Scalar::Util's 的weaken()函数(弱引用),使用弱引用不会增加引用计数。

    use Scalar::Util 'weaken';
    my $alice = { mother => '', father => '' };
    my $robin = { mother => '', father => '' };
    my $cianne = { mother => $alice, father => $robin };
    push @{ $alice->{children} }, $cianne;
    push @{ $robin->{children} }, $cianne;
    weaken( $cianne->{mother} );
    weaken( $cianne->{father} );
    

    当然绝大部分正常的数据结构都不会出现回路引用,也就不需要使用弱引用。

    替代嵌套数据结构

    对Perl来说无所谓,再复杂的嵌套数据结果都能顺利处理,但是人不行,嵌套超过2层或3层人就难以理解了。这时就可以考虑使用类和对象技术来让代码更加清晰。

    相关文章

      网友评论

      • Abelfourier:还也可(可以)使用块来指定声明范围:

        包除了有名字,还有版本号和三个函数:import()、unimport()、VERSION() 。
        后面两个函数就没介绍了?

        Perl对版本号有格式上的要求:以字母v开头和至少三个以点号(.)隔开整数。
        按这个说法,那么例子错了?
        #分开写
        package MyCode;
        our $VERSION = 1.2(.)1;

        还有后面比较版本号的例子也没看懂,不是要比较版本号大于2.1么,怎么变成2了?
        你也可以向函数传递(版本号)参数,如果模块的版本号小于你传递的版本号,函数会抛出异常:
        # require at least 2.1
        Some::Plugin->VERSION( 2.1 );
        die "Your plugin $version is too old" unless $version > 【2】;


        调试嵌套数据结构的例子
        use Data::Dumper;
        my $complex_structure = {...};
        print Dumper( $my_complex_structure );
        两变量名不一致啊?
        可以没名字吗:@Abelfourier 个别函数会在后面适当的时候介绍;
        1.21是老式写法;
        最低版本号为2.1所以大于2就行了。
        其他的文字错误改好了。

      本文标题:第三章 Perl语言(五)-包、引用、复杂数据

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