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

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层人就难以理解了。这时就可以考虑使用类和对象技术来让代码更加清晰。

你可能感兴趣的:(第三章 Perl语言(五)-包、引用、复杂数据)