Perl高级编程:引用、多维数组与哈希引用
1. 引用与二维数组
在编程中,我们常常会遇到需要处理复杂数据结构的情况。在Perl里,引用和二维数组就是处理这类数据的重要工具。
1.1 一维数组回顾
到目前为止,我们使用的数组大多是一维的。例如:
my @array = ('Mouse', 'Mus musculus', 'Rodent');
这个数组按线性顺序存储了三个值。如果把这些数据放到电子表格中,可以将它们放在同一行或同一列。
| Mouse | Mus musculus | Rodent |
但如果要将整个电子表格的内容存储在数组中,该如何表示像下面这样的数据呢?
| Mouse | Mus musculus | Rodent |
| Human | Homo sapiens | Primate |
| Cow | Bos taurus | Ungulate |
一种自然的表示方式是使用二维数组。不过,在介绍如何创建二维数组之前,我们需要先了解Perl中的引用。
1.2 引用的概念
引用可以类比为Windows系统中的快捷方式或Mac系统中的别名。它们就像可点击的“书签”,指向经常访问的应用程序或文件。创建快捷方式或别名时,会得到一个模仿原始文件的图标,但不会复制文件本身。可以为单个应用程序创建多个快捷方式或别名,并将它们放在文件系统的不同文件夹中。打开文档的快捷方式或别名时,可以进行一些编辑,更改会反映在原始文件中。但删除快捷方式或别名不会删除原始文件。Perl中的引用与快捷方式或别名的行为非常相似,虽然这个类比并不完美,但它是理解引用的一个有用起点。
1.3 数组引用
下面是一个简单的数组赋值:
my @author = ('Keith', 'Ian');
如果要创建这个数组的引用,需要使用反斜杠运算符:
my $author_ref = \@author;
这里创建了
$author_ref
,它是一个标量变量,“指向”
@author
数组。如果没有反斜杠字符,只是将数组赋值给一个列表,
$author_ref
最终会包含
@author
的大小。因此,反斜杠字符对于使
$author_ref
成为引用至关重要。
需要注意的是,
$author_ref
仍然是一个标量,因为它只保存一个东西的内容,但它指向的东西可以是一个包含多个元素的数组。如果尝试打印
$author_ref
的值,会得到一个奇怪的字符串,如
ARRAY(0x100800f40)
,这表明该变量是一个数组引用,括号中的十六进制数字是其内存地址。
Perl的
ref()
函数可以用来判断一个标量变量是否是引用(与普通标量变量相对)。在本章中,我们只关注数组引用。如果对不是引用的东西使用
ref()
,它会返回假。
print ref($author_ref); # 输出 ARRAY
print ref("something"); # 无输出
1.4 使用数组引用
数组引用的使用方式与数组非常相似,但语法略有不同。由于
$author_ref
是一个标量变量,不能直接使用
$author_ref[0]
,而需要使用箭头运算符
->
:
print $author_ref->[0]; # 输出 'Keith'
可以将其理解为“打印由名为
$author_ref
的标量指向的数组的第零个元素”。
$author[0]
和
$author_ref->[0]
在计算机内存中占据完全相同的位置。如果更改其中一个,另一个也会随之更改:
$author_ref->[0] = 'Keith Bradnam';
print $author[0]; # 现在会输出 'Keith Bradnam'
相反,如果将数组引用设置为未定义或转换为普通标量变量,不会影响它以前指向的数组:
$author_ref = undef; # 使引用未定义
print $author[0]; # 仍然会输出 'Keith'
$author_ref = "cheese"; # 可以将变量重新用作普通标量
箭头运算符
->
可以用来解引用数组的单个元素。但有时需要解引用整个数组,例如在排序时。要解引用整个数组,可以在标量前加上数组符号,或者在包含标量的块前使用数组符号。以下三个语句是等价的:
push @author, 'Nigel';
push @$author_ref, 'Nigel';
push @{$author_ref}, 'Nigel';
这里唯一的区别是,第一个示例是将字符串添加到数组中,而后两个示例是将字符串添加到解引用的数组中,但修改的始终是同一个数组。
刚开始可能需要一些时间来适应
@
和
$
符号的组合。随着经验的积累,看到这样的代码时,会本能地意识到正在处理整个数组的引用。对数组执行的任何操作都可以同样轻松地对解引用的数组执行。例如,遍历数组引用的元素非常简单:
foreach my $author (@$author_ref) {
print "$author\n";
}
1.5 匿名数组构造器
在前面的示例中,
$author_ref
是
@author
数组的引用。当在需要创建引用之前已经创建了数组时,这种创建引用的方式是有意义的。但如果想在创建引用的同时创建数组,可以使用匿名数组构造器。它的名字听起来很复杂,但实际上只是用方括号代替了表示数组的圆括号:
my $author_ref = ['Keith', 'Ian'];
之所以称其为“匿名”,是因为数组没有名字。数组引用有名字(
$author_ref
),但它指向的数组没有名字。以下两种方法的效果是相同的:
| 命名数组 | 匿名数组 |
|---|---|
perl<br>my @array = ('Keith', 'Ian');<br>my $array_ref = \@array;<br>print $array_ref->[0];<br>
|
perl<br>my $array_ref = ['Keith', 'Ian'];<br>print $array_ref->[0];<br>
|
讨论引用和匿名数组构造器的原因是,它们是创建多维数组的关键。如果只需要处理一维数据,使用匿名数组可能可以,但使用普通数组可能更有意义。
1.6 二维数组
现在,让我们来创建一个二维数组。以下是要表示的数据:
| Mouse | Mus musculus | Rodent |
| Human | Homo sapiens | Primate |
可以为每一行创建普通数组:
my @row0 = ('Mouse', 'Mus musculus', 'Rodent');
my @row1 = ('Human', 'Homo sapiens', 'Primate');
然后将这两个一维数组放入另一个数组中,使其成为二维数组。有四种不同的方法可以实现这一点。
方法一:使用数组引用
my $row_ref0 = \@row0;
my $row_ref1 = \@row1;
my @table = (
$row_ref0,
$row_ref1,
);
现在有了一个包含两个元素的数组,每个元素都是对另一个数组的引用。值得停下来思考一下我们刚刚创建的数据结构。
@table
是一个普通数组,可以像使用其他数组一样使用它。唯一的区别是,它包含的不是普通标量,而是对其他数组的引用。需要注意的是,在解引用任何数组元素的内容之前,它仍然是一个一维数组。只有在那个时候,才会处理二维数据结构。
方法二:直接添加数组引用
my @table = (
\@row0,
\@row1,
);
除非有特定需求需要为特定行创建命名引用,否则这是一种更直接的创建二维表的方法。
方法三:使用匿名数组构造器和命名变量
my $row0_ref = ['Mouse', 'Mus musculus', 'Rodent'];
my $row1_ref = ['Human', 'Homo sapiens', 'Primate'];
my @table = (
$row0_ref,
$row1_ref,
);
最终结果与之前相同,但现在避免了创建数组来保存每一行的数据。然而,这又回到了创建可能不再需要使用的命名数组引用的情况。
方法四:只命名二维数组
my @table = (
['Mouse', 'Mus musculus', 'Rodent'],
['Human', 'Homo sapiens', 'Primate'],
);
这是一种更简单、更整洁的创建二维数组的方法。注意,方括号外的逗号表示第一维的每个元素,方括号内的逗号表示第二维的元素。可以使用
$table[1]
访问整行,它包含一个数组引用。如果打印
$table[1]
,会得到一个奇怪的字符串,如
ARRAY(0x1008000c0)
,因为它是一个数组引用。要访问单个单元格,可以使用箭头运算符
->
:
print $table[1]->[1]; # 输出 Homo sapiens
在这种情况下,箭头是可选的,
$table[1][1]
看起来更好。如果要打印表中第一个“单元格”的内容,可以使用以下两种方法之一:
print $table[0]->[0];
print $table[0][0];
可以将这两行代码理解为“打印由
@table
数组的第零个元素中的引用指向的数组的第零个元素”。
下面是一个完整的示例脚本:
#!/usr/bin/perl
# zoo.pl
use strict;
use warnings;
my @zoo = (
["Mouse", "Mus musculus", "Rodent"],
["Human", "Homo sapiens", "Primate"],
["Cow", "Bos taurus", "Ungulate"],
);
print "$zoo[1][2]\n"; # 输出 'Primate'
for (my $i = 0; $i < @zoo; $i++) {
for (my $j = 0; $j < @{$zoo[$i]}; $j++) {
print "$zoo[$i][$j]\n";
}
}
foreach my $row (@zoo) {
foreach my $column (@$row) {
print "$column\n";
}
}
这个脚本的执行流程如下:
graph TD;
A[开始] --> B[定义二维数组 @zoo];
B --> C[打印 @zoo[1][2]];
C --> D[使用嵌套 for 循环遍历 @zoo];
D --> E[打印每个元素];
E --> F[使用嵌套 foreach 循环遍历 @zoo];
F --> G[打印每个元素];
G --> H[结束];
在编写这个脚本时,由于它比大多数脚本长,建议分部分编写,编写一部分后检查是否能运行,然后再继续编写。例如,先编写第1 - 9行,然后到第11行,接着到第17行,最后到第23行。编写第14行时,确保使用
@{$zoo[$i]}
而不是
@$zoo[$i]
,因为
@$zoo
的绑定比
$zoo[$i]
更紧密,Perl会认为
$zoo
是一个标量变量。为了让Perl识别数组,必须将其放在块
{$zoo[$i]}
中。
1.7 从文件中读取二维数组
如果有大量数据,将所有内容作为Perl代码的一部分写出来会很繁琐,而且可能会在输入时出错。因此,从文件中读取数据要好得多。即使在互联网时代,基于逗号分隔值(CSV)和制表符分隔值(TSV)格式的纯文本文件仍然非常常见。下面是一个从CSV文件读取数据的示例:
首先,创建一个名为
species.csv
的文件,内容如下:
Mouse,Mus musculus,Rodent
Human,Homo sapiens,Primate
Cow,Bos taurus,Ungulate
然后创建以下程序:
#!/usr/bin/perl
# zoo_reader.pl
use strict;
use warnings;
my @table;
while (<>) {
my ($common, $scientific, $family) = split(/,/, $_);
push @table, [$common, $scientific, $family];
}
这个脚本的执行流程如下:
graph TD;
A[开始] --> B[定义空数组 @table];
B --> C[使用 while 循环读取文件];
C --> D[使用 split 函数分割每行数据];
D --> E[将分割后的数据放入匿名数组并添加到 @table];
E --> F[结束];
这个脚本的
while
循环从文件操作符
<>
指定的文件中读取数据,该文件可以是命令行指定的任何文件,甚至是标准输入。第7行将文件中的每行输入分割成三个变量,下一行将这三个变量添加到一个匿名数组中,然后将该匿名数组作为单个元素添加到
@table
数组的末尾。
然而,这个脚本存在一些问题,它硬编码为只处理输入文件中的三列数据,并且选择的变量名暗示了特定类型的数据。为了使脚本更通用,应该能够处理任意数量的列,可以修改第7行和第8行:
my @array = split(/,/, $_);
push @table, \@array;
现在,CSV文件的每一行都被分割成一个数组。如果有100列,数组将有100个元素,然后将其添加到
@table
中,形成一个二维数组。
要从使用CSV文件改为使用TSV文件,只需要编辑
split()
函数中的模式:
my @array = split(/\t/, $_);
为了更具通用性,可以将
split
中的字符串改为一个变量,该变量可以在程序的其他地方定义,甚至更好的是,由用户作为命令行参数定义:
my @array = split(/$separator/, $_);
再看一下
Example 6.3.2
中的代码,第8行可能看起来有些奇怪。如果担心
@array
在每次
while
循环迭代时都会被删除,说明思考得很深入。词法变量通常在花括号内创建和销毁,但
@array
虽然每次循环都会创建,但由于它仍然有引用,其对应的内存不会被销毁,关于这个有点令人困惑的话题,会在垃圾回收的子部分进行讨论。
2. 记录和其他哈希引用
哈希引用在Perl编程中也扮演着重要角色,它能让我们更高效地处理复杂的数据结构。
2.1 哈希引用和匿名哈希构造器
之前介绍了数组引用,现在来看如何创建哈希引用。哈希引用的工作方式与数组引用非常相似,创建哈希引用同样要使用反斜杠运算符。下面是一个简单的哈希创建及引用示例:
my %sound = (dog => 'woof', cat => 'meow');
my $sound_ref = \%sound;
可以看到,创建哈希引用的语法和创建数组引用的语法完全相同。此外,还有匿名哈希构造器,它允许我们在不命名哈希的情况下创建哈希引用,语法上只需用花括号代替圆括号:
my $sound_ref = {dog => 'woof', cat => 'meow'};
这会创建一个没有名字的哈希,由变量
$sound_ref
引用它。要解引用哈希的单个元素,需要使用
->
运算符:
print $sound_ref->{dog}; # 输出 'woof'
可以理解为“打印由
$sound_ref
引用所指向的哈希中键为
dog
的值”。要解引用整个哈希,在标量前加上
%
符号,这样就可以在任何能使用哈希的地方使用解引用的哈希,有时也需要使用块表示法:
my @animals = keys %{$sound_ref};
下面通过一个脚本测试这些哈希引用的概念:
#!/usr/bin/perl
# hash_ref.pl
use strict;
use warnings;
my $sound = {dog => 'woof', cat => 'meow'};
$sound->{cow} = 'moo';
foreach my $animal (keys %$sound) {
print "$animal says $sound->{$animal}\n";
}
这个脚本的执行流程如下:
graph TD;
A[开始] --> B[创建匿名哈希引用 $sound];
B --> C[向 $sound 中添加键值对];
C --> D[遍历 $sound 的键];
D --> E[打印每个键值对];
E --> F[结束];
脚本中第5行创建了一个匿名哈希的引用,第6行向哈希中添加了另一个键值对,最后第7 - 9行遍历整个哈希,显示键值对。
2.2 哈希引用作为数据库记录
哈希引用最常见的用途之一是表示数据库中的记录。例如,有一个简单的地址簿,地址簿中的每张“卡片”包含一个人的姓名和电子邮件属性,一张特定的卡片可以这样表示:
my $card = {
name => 'Ian',
email => 'atagcgaat@gmail.com'
};
一个地址簿可能包含多张卡片,当然要用数组来保存多个卡片。哈希引用数组是一种二维数据结构,第一维包含卡片,第二维包含每张卡片上的属性。要将哈希引用添加到数组中,只需使用
push
操作:
my @address_book;
push @address_book, $card;
这里
$card
是一个指向匿名哈希的引用,由于它表现得像一个普通标量变量,所以可以作为单个项添加到数组中。
@address_book
是一个普通数组,只是它的元素是哈希引用。
也可以不创建哈希的命名引用,直接将
@address_book
数组的元素定义为哈希引用:
my @address_book = (
{name => 'Ian', email => 'atagcgaat@gmail.com'},
{name => 'Keith', email => 'whykeith@me.com'}
);
下面是一个实际应用的示例脚本:
#!/usr/bin/perl
# address_book.pl
use strict;
use warnings;
my @address_book = (
{name => 'Ian', email => 'atagcgaat@gmail.com'},
{name => 'Keith', email => 'whykeith@me.com'}
);
foreach my $card (@address_book) {
print "$card->{name} $card->{email}\n";
}
这个脚本的执行流程如下:
graph TD;
A[开始] --> B[定义地址簿数组 @address_book];
B --> C[遍历 @address_book 中的每个卡片];
C --> D[打印每个卡片的姓名和电子邮件];
D --> E[结束];
脚本中第5 - 8行创建了一个包含两张卡片的地址簿,注意使用空格对齐属性,使代码更易读。第10 - 12行展示了如何遍历数组中的所有哈希引用,
$card
是一个临时变量,代表
@address_book
中的每个元素,每个元素都是一个哈希引用。
2.3 从文件中读取记录
之前我们将CSV数据读入仅使用数组的二维数据结构中,例如:
Mouse,Mus musculus,Rodent
Human,Homo sapiens,Primate
Cow,Bos taurus,Ungulate
但二维数组并不是描述这类数据的最佳方式,哈希可以为属性命名,而不是使用编号。比较下面两种方式,哪种更具描述性一目了然:
$animal->[0];
$animal->{common_name};
下面将数据读入哈希数组,并使用
while
循环遍历数据结构:
#!/usr/bin/perl
# zoo_reader2.pl
use strict;
use warnings;
my @database;
while (<>) {
my ($com, $sci, $fam) = split(/,/, $_);
push @database, {
common_name => $com,
scientific => $sci,
family => $fam
};
}
foreach my $animal (@database) {
print "$animal->{common_name}\n";
}
这个脚本的执行流程如下:
graph TD;
A[开始] --> B[定义空数组 @database];
B --> C[使用 while 循环读取文件];
C --> D[使用 split 函数分割每行数据];
D --> E[将分割后的数据转换为匿名哈希并添加到 @database];
E --> F[遍历 @database 中的每个动物];
F --> G[打印每个动物的通用名称];
G --> H[结束];
脚本中第5 - 12行从命令行指定的CSV文件中逐行读取数据,第7 - 11行将数据转换为匿名哈希,并将哈希引用添加到
@database
数组中。注意第8 - 10行的键值对相互对齐,每个属性占一行,这是一种常见的编程习惯,使逻辑更清晰。第14 - 16行遍历哈希数组,只打印每个动物的通用名称。
2.4 哈希的哈希和哈希的数组
前面探讨了二维数组和哈希数组,在这两种情况中,第一维都是数组。其实也可以将其改为哈希,只需为元素分配名称而不是编号,这样做的一个原因是可以加快搜索速度。
以地址簿为例,假设地址簿中有很多“卡片”,并且已经有代码从文件中检索这些数据:
my @address_book = read_address_book("some_file");
假设
@address_book
包含数百万条记录,使用以下结构搜索单个记录会比较耗时:
foreach my $card (@address_book) {
if ($card->{name} eq 'Ian') {
do_something($card);
}
}
使用未排序的列表意味着平均需要搜索列表的一半才能找到匹配项。如果将姓名放在哈希中,检索速度会快很多。例如,按姓名索引地址簿,可以使用以下代码:
my %index;
foreach my $card (@address_book) {
$index{$card->{name}} = $card;
}
这段代码创建了一个新的哈希
%index
,哈希的每个键是哈希数组中的“姓名”字段,与每个键关联的值是指向该人所有信息数组的引用。如果觉得这个语法难以理解,记住Perl通常是从内到外工作的。上述代码中,Perl首先解析
$card->{name}
部分,得到一个字符串,然后将其分配给新哈希
%index
的键,最后将该键与一个值关联。有了这个哈希,只需简单查找就能检索任何卡片:
my $card = $index{'Ian'};
这种方法假设姓名是唯一的,但实际数据可能并非如此。如果存在重复姓名,需要为每个姓名存储一个卡片数组:
foreach my $card (@address_book) {
push @{ $index{$card->{name}} }, $card;
}
此时
%index
是一个三维数据结构,第一维是一个哈希,键是姓名,关联的值是数组的引用;第二维是与每个姓名关联的卡片数组;第三维是卡片上的属性。为了更清楚地说明维度,假设要获取姓名为“Ian”的第一张卡片的电子邮件地址,可以这样做:
print $index{Ian}[0]{email};
通过以上对引用、二维数组和哈希引用的介绍,我们可以看到Perl在处理复杂数据结构方面的强大能力,合理运用这些特性可以让代码更加简洁、高效。
超级会员免费看
10

被折叠的 条评论
为什么被折叠?



