1、用多个数组来完成一个简单任务
在Minnow开始一个旅程之前(比如一个三小时的远足), 我们应该事先检查一下每个乘客和乘务人员的行李,保证他们带了旅行所需要的东西。比如说吧,水上安全救生装备。在Minnow船上的每个乘客要生命维持系统,太阳镜和水瓶以及雨衣,代码如下
my @required = qw(preserver sunscreen water_bottle jacket);
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
for my $item (@required) {
unless (grep $item eq $_, @skipper) { # 数组为空,没有匹配
print "skipper is missing $item\n";
}
}
当需要检查其他人的行李时,仍然需要上述代码
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
for my $item (@required) {
unless (grep $item eq $, @gilligan) { # not found in list?
print "gilligan is missing $item.n";
}
}
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
for my $item (@required) {
unless (grep $item eq $, @professor) { # not found in list?
print "professor is missing $item.n";
}
}
代码有重复,重构代码,整合到一个通用的子例程里以便重用
sub check_required_items {
my $who = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $, @_) { # 没有匹配,打印未匹配元素值
print "$who is missing $item\n";
}
}
}
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
check_required_items(‘gilligan’, @gilligan);
子例程接收参数(五个元素):一个gilligan名字以及另外属于数组Gilligan的四个元素。shift操作之后,@_仅包括四个元素,因此,grep用每个出海必备装备来核对这个四个元素的列表。
定义函数,避免一部分重复代码。尽管解决了最初的问题,但还有两个问题需要解决。
• 每次调用函数都需要拷贝整个数组给@_,这是不小的开销,尤其当数组很大时
• 无法改变原有数组,函数调用的方式将无法做到。因为传给函数的@_参数只是原数组的一个拷贝,任何应用在@_上的改变,无法应用到原数组。
2、建立一个对数组的引用
反斜杠'\被用来当作“取址”操作符,即\@skipper,表示数组@skipper的地址值。引用这个数组就是一个指针:指向这个数组。标量合适的操作对于引用都一样合适。数组或散列中可以保存对数组的引用,或简单就是一个标量变量,像下面所示:
# 设置引用变量
my $reference_to_skipper = \@skipper;
# 引用可以被复制
my $second_reference_to_skipper = $reference_to_skipper;
# 相当于
my $third_reference_to_skipper = \@skipper;
# 比较变量的地址值
if ($reference_to_skipper = = $second_reference_to_skipper) {
print "They are identical references.n";
}
打印数组的引用值时输出:ARRAY(0x1a2b3c)
解释:以十六进制表示的(base16)的这个数组唯一内存地址.调试字符串还标明了这个引用指向的是个数组。因为我们可以拷贝一个引用,并且作为参数传给一个子例程,我们可以用如下代码把对数组的引用传给子例程:
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
check_required_items("The Skipper", \@skipper);
sub check_required_items {
my $who = shift;
# $items指向数组 @skipper 的地址值
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
…
}
3、 还原一个指向数组的引用
@skipper, 你会发现它包括两部份:@符号和数组名。相似地,语法$skipper[1]表示取出数组中第二个元素(索引1表示取第二个元素,因为索引起始值是0)。类似的,在数组的引用名外添加大括号{},替代数组的名字,其结果就是访问原始的数组。换句话说,就是我们写sipper数组名字的地方,可以用大括号包起来的指向数组的引用来代替:{$items}(表示数组名)。
举例来说,下面两行都指向同一数组:
@skipper # 正常的数组表示
@{ $items } # 引用的数组表示
同样,下面两行同指这个数组的第二个元素:
$skipper [1] # 表示数组的第二个元素
${ $items }[1] # 表示数组引用的第二个元素
运用引用的形式,我们已经可以分离数组名字和从实际数组中访问数组的方法。我们来看看子例程的余下部分:
sub check_required_items {
my $who = shift; # 获取标量值
my $items = shift; # 获取数组地址值
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $_, @{$items}) { # not found in list?
print "$who is missing $item\n";
}
}
}
# 调度子程序
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
check_required_items(‘Gilligan’, \@gilligan);
用引用传数组解决了我们上面提到的两个问题中的第一个。现在向函数传递一个数组的地址值,而非是该数组的拷贝
# 消除传两个值给子例程,但这样牺牲了可读性:
sub check_required_items {
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
# 数组@_中仍然有两个元素,第一个是标量,第二个是数组引用
unless (grep $item eq $_, @{$_[1]}) { # not found in list?
print "$_[0] is missing $item\n";
}
}
}
把大括号去掉(如果括号里面不是简单标量,而是数组元素,则不能简写. 例如@{$_[1]}),如下几点注意
-- 还原数组引用是一个标量时,可以省略,如:@{$items}(数组) 或者 ${$items}[1](数组第一个元素)
-- 若大括号里的内容不是简单的标量变量,大括号不能省略。如大括号是一个数组元素$_[0]
建议:统一在数组引用 $items 周围加上大括号。如此,{$items}必须是一个指向数组的引用。
因此,看上去比较顺眼的写法应该是:
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
for my $item (@required) {
unless (grep $item eq $_, @$items) { # not found in list?
print "$who is missing $item\n";
}
}
}
4、修改数组
对于每个遗忘的的装备,我们把它放到另一个数组里,要求乘客关注这些装备:
sub check_required_items {
my $who = shift;
my $items = shift;
my @required = qw(preserver sunscreen water_bottle jacket);
my @missing = ( );
for my $item (@required) {
# 未匹配,则输出并保存到数组@missing中
unless (grep $item eq $_, @$items) {
print "$who is missing $item\n";
push @missing, $item;
}
}
if (@missing) {
print "Adding @missing to @$items for $who\n";
push @$items, @missing; # 发现必需品未携带时,添加到随身物品中
}
}
解释:在扫描数组的时候发现有遗忘的装备,将遗忘的装备放到 数组@missing中。 在遍历结束后,如果发现@missing中有元素,就将@missing中的元素添加到数组引用中,从而修改了原数组。
5、数据结构嵌套
在前例中,我们的数组@_有两个元素,其中一个同样是个数组。 如果一个引用所指向的数组中还包含着另外一个指向数组的引用会是什么情况?那就成了非常有用的所谓复杂数据结构。
举个例子,我们首先用个更大点儿的数据结构包含skipper,Gilligan和Professor供应清单的整个列表。
my @skipper = qw(blue_shirt hat jacket preserver sunscreen);
my @skipper_with_name = (‘Skipper’, \@skipper);
my @professor = qw(sunscreen water_bottle slide_rule batteries radio);
my @professor_with_name = (‘Professor’, \@professor);
my @gilligan = qw(red_shirt hat lucky_socks water_bottle);
my @gilligan_with_name = (‘Gilligan’, \@gilligan);
@skipper_with_name的第二个元素是指向数组的引用,将上述三个数组重新放到一个新数组@all_with_names中:
my @all_with_names = (\@skipper_with_name,\@professor_with_name,\@gilligan_with_name,);
注意,数组@all_with_name包含一个多层的数据结构,包括字串和指向另一数组的引用。
这样,$all_with_names[2]里放的是指向数组的引用,其数组存放的是Gilligan的数据。如果将其还原,像这样:@{$all_with_names[2]},你就是得到一个有两个元素的数组:Gilligan和另一个数组引用。
我们如何才能访问那个数组引用呢?用我们的老规矩:${$all_with_names[2]}[1]。 换句话说,我们在一个表达式中像$DUMMY[1]形式那样把$all_with_names[2]还原成一个平常的数组,就是说用{$all_with_names[2]}代替DUMMY的位置。
那我们如何用这个数据结构来调用现存的check_required_items( )? 下面的代码足够简单:
for my $person (@all_with_names) {
my $who = $$person[0];
my $provisions_reference = $$person[1];
check_required_items($who, $provisions_reference);
}
这样对于以前写的子例程不需要做任何改变。随着循环进程,控制变量$person将会是$all_with_names[0],$all_with_names[1]和$all_with_names[2]。
当然,我们可以把这个过程再简化,因为整个还原数组与参数清单精确对应:
for my $person (@all_with_names) {
# 获取第一个元素 $$person[0],得到"Skipper,""Professor,"和"Gilligan"
# 获取第二个元素 $$person[1]是各个乘客所对应的装备清单数组
check_required_items(@$person);
}
# 更简略的写法
check_required_items(@$_) for @all_with_names;
6、用箭头号简化嵌套数组引用
对Gilligan的装备清单的数组引用是:${$all_with_names[2]}[1]。
访问Gilligan的第一个装备需要把这个引用再还原一下,所以要加上另一层大括号:${${$all_with_names[2]}[1]}[0](太麻烦)。
优化:在任何写成${DUMMY}[$y]样子的地方,我们都可以用$DUMMY->[$y]这种形式代替。换句话说,我们可以这样还原一个数组引用: 用定义一个带箭头的数组引用和一个方括号指定下标的形式表达数组里一个特定的元素。
举例:得到对Gilligan数组的引用的话, 我们可以简单写成:$all_with_names[2]->[1];
而指明Gilligan的第一个装备清单的写法是:$all_with_names[2]->[1]->[0]。
如果你觉得还不够简洁的话?那我们还有条规则:如果箭头是在“类似于下标”中间的话,那么箭头也可以省去。$all_with_names[2]->[1]->[0]变成了$all_with_names[2][1][0]。现在样子看上去更简洁了。
那为什么箭头必须在非下标符号间存在呢?好,如果我们有一个指向数组@all_with_names的引用:
my $root = @all_with_names;
现在我们如何时取得Gilligan的第一个装备呢?
$root -> [2] -> [1] -> [0]
很简单,用“去箭头”规则,我们可以写成:
$root -> [2][1][0]
然而,第一个箭头不能舍去,因为这会表示root数组的第三个元素,成了一个完全无关的数据结构。让我们再与它“全包装“的形式再做一个比较:
${${${$root}[2]}[1]}[0]
看来用箭头比较爽。但没有快捷办法从一个数组引用中取出整个数组内容。比如,我们要找出Gilligan装备清单的话,可以这样写:
@{$root->[2][1]}
应该按下面的顺序来从里往外读:
• 取变量$root。
• 把它先还原成一个指向数组的引用,取出那个数组中第三个元素(序号为2)
• 用同样的方法取出那个数组第二个元素(序号为1)
• 然后把整个数组还原出来。