内容

名称

perlreftut - Mark 关于引用的简短教程

描述

Perl 5 中最重要的一个新特性是管理复杂数据结构的能力,例如多维数组和嵌套哈希。为了实现这些功能,Perl 5 引入了名为“引用”的特性,使用引用是管理 Perl 中复杂结构化数据的关键。不幸的是,需要学习很多有趣的语法,并且主手册页可能难以理解。手册非常完整,有时人们会发现这是一个问题,因为很难区分什么是重要的,什么是不重要的。

幸运的是,你只需要了解主手册页中 10% 的内容就能获得 90% 的好处。本页将向你展示这 10% 的内容。

谁需要复杂的数据结构?

一个经常出现的问题是需要一个值是列表的哈希。当然,Perl 有哈希,但值必须是标量;它们不能是列表。

为什么你想要一个列表的哈希?让我们举一个简单的例子:你有一个包含城市和国家名称的文件,如下所示

Chicago, USA
Frankfurt, Germany
Berlin, Germany
Washington, USA
Helsinki, Finland
New York, USA

你想要生成一个这样的输出,每个国家只出现一次,然后是该国家所有城市的字母顺序列表

Finland: Helsinki.
Germany: Berlin, Frankfurt.
USA:  Chicago, New York, Washington.

自然的方法是使用一个哈希,其键是国家名称。与每个国家名称键关联的是该国家所有城市的列表。每次读取一行输入时,将其拆分为国家和城市,查找已知属于该国家的城市列表,并将新城市追加到列表中。完成读取输入后,像往常一样迭代哈希,在打印每个城市列表之前对其进行排序。

如果哈希值不能是列表,你就输了。你可能需要将所有城市组合成一个字符串,然后在需要写入输出时,将字符串分解成一个列表,对列表进行排序,然后将其转换回字符串。这很混乱且容易出错。而且令人沮丧的是,Perl 已经拥有完美的列表,如果可以的话,它们可以解决问题。

解决方案

到 Perl 5 出现时,我们已经陷入了这种设计:哈希值必须是标量。解决这个问题的方法是引用。

引用是一个标量值,它引用整个数组或整个哈希(或几乎任何其他东西)。名称是你已经熟悉的引用的一种。每个人都是一个混乱、不便的细胞集合。但是,要引用一个特定的人,例如第一个计算机程序员,没有必要描述他们的每个细胞;你只需要一个简单、方便的标量字符串“艾达·洛芙莱斯”。

Perl 中的引用就像数组和哈希的名称。它们是 Perl 的私有内部名称,因此你可以确保它们是明确的。与人类姓名不同,引用只引用一个东西,并且你总是知道它引用的是什么。如果你有一个数组的引用,你可以从中恢复整个数组。如果你有一个哈希的引用,你可以从中恢复整个哈希。但是引用仍然是一个简单、紧凑的标量值。

你不能拥有一个值是数组的哈希;哈希值只能是标量。我们被困住了。但是一个引用可以引用整个数组,而引用是标量,因此你可以拥有一个指向数组的引用的哈希,它将非常像一个数组的哈希,并且它将与数组的哈希一样有用。

我们将在稍后回到这个城市-国家问题,在我们了解了一些管理引用的语法之后。

语法

创建引用只有两种方式,使用引用也只有两种方式。

创建引用

创建规则 1

如果在变量前面加上 \,你将获得该变量的引用。

$aref = \@array;         # $aref now holds a reference to @array
$href = \%hash;          # $href now holds a reference to %hash

一旦引用被存储在一个变量中,例如 $aref 或 $href,你就可以像其他标量值一样复制或存储它。

$xy = $aref;             # $xy now holds a reference to @array
$p[3] = $href;           # $p[3] now holds a reference to %hash
$z = $p[3];              # $z now holds a reference to %hash

这些例子展示了如何创建对有名称的变量的引用。有时你可能想要创建一个没有名称的数组或哈希。这类似于你想要使用字符串 "\n" 或数字 80 的方式,而无需先将它们存储在一个有名称的变量中。

创建规则 2

[ ITEMS ] 创建一个新的匿名数组,并返回该数组的引用。{ ITEMS } 创建一个新的匿名哈希,并返回该哈希的引用。

$aref = [ 1, "foo", undef, 13 ];
# $aref now holds a reference to an array

$href = { APR => 4, AUG => 8 };
# $href now holds a reference to a hash

从规则 2 获得的引用与从规则 1 获得的引用类型相同。

# This:
$aref = [ 1, 2, 3 ];

# Does the same as this:
@array = (1, 2, 3);
$aref = \@array;

第一行是以下两行的缩写,除了它没有创建多余的数组变量 @array

如果你只写 [],你将获得一个新的空匿名数组。如果你只写 {},你将获得一个新的空匿名哈希。

使用引用

获得引用后,你可以用它做什么?它是一个标量值,我们已经看到你可以像任何标量一样存储它并再次获取它。还有两种使用它的方法。

使用规则 1

你始终可以使用数组引用(用花括号括起来)来代替数组的名称。例如,@{$aref} 代替 @array

以下是一些示例:

数组

@a		@{$aref}		An array
reverse @a	reverse @{$aref}	Reverse the array
$a[3]		${$aref}[3]		An element of the array
$a[3] = 17;	${$aref}[3] = 17	Assigning an element

每行包含两个表达式,它们执行相同的操作。左侧版本操作数组 @a。右侧版本操作由 $aref 引用的数组。一旦它们找到要操作的数组,两个版本对数组执行相同的操作。

使用哈希引用与之完全相同。

%h		%{$href}	      A hash
keys %h		keys %{$href}	      Get the keys from the hash
$h{'red'}	${$href}{'red'}	      An element of the hash
$h{'red'} = 17	${$href}{'red'} = 17  Assigning an element

无论你想对引用做什么,**规则 1** 都能告诉你如何做。你只需编写你原本要编写用于对普通数组或哈希执行相同操作的 Perl 代码,然后用 {$reference} 替换数组或哈希名称。例如,"如何循环遍历一个数组,而我只有它的引用?" 为了循环遍历一个数组,你会编写

for my $element (@array) {
  ...
}

所以用引用替换数组名称 @array

for my $element (@{$aref}) {
  ...
}

"如何打印出哈希的内容,而我只有它的引用?" 首先编写打印哈希的代码

for my $key (keys %hash) {
  print "$key => $hash{$key}\n";
}

然后用引用替换哈希名称。

for my $key (keys %{$href}) {
  print "$key => ${$href}{$key}\n";
}

规则 2

规则 1 是你真正需要的,因为它告诉你如何执行所有你需要对引用执行的操作。但对数组或哈希最常见的操作是提取单个元素,而 规则 1 的表示法很繁琐。所以有一个简写。

${$aref}[3] 太难读了,所以你可以写 $aref->[3] 来代替。

${$href}{red} 太难读了,所以你可以写 $href->{red} 来代替。

如果 $aref 保存对数组的引用,那么 $aref->[3] 是数组的第四个元素。不要将其与 $aref[3] 混淆,后者是完全不同数组的第四个元素,该数组被误称为 @aref$aref@aref 是不相关的,就像 $item@item 一样。

类似地,$href->{'red'} 是由标量变量 $href 引用的哈希的一部分,甚至可能是一个没有名称的哈希。$href{'red'} 是名为 %href 的哈希的一部分,这个名字有点误导性。很容易忘记省略 ->,如果你这样做,当你的程序从完全意想不到的哈希和数组中获取数组和哈希元素时,你会得到奇怪的结果,而这些哈希和数组并不是你想要使用的那些。

一个例子

让我们看一个快速示例,说明这一切如何有用。

首先,请记住 [1, 2, 3] 创建一个包含 (1, 2, 3) 的匿名数组,并为你提供对该数组的引用。

现在考虑

	@a = ( [1, 2, 3],
               [4, 5, 6],
	       [7, 8, 9]
             );

@a 是一个包含三个元素的数组,每个元素都是对另一个数组的引用。

$a[1] 是这些引用之一。它引用一个数组,该数组包含 (4, 5, 6),并且因为它是对数组的引用,所以 使用规则 2 指出我们可以写 $a[1]->[2] 来获取该数组的第三个元素。$a[1]->[2] 是 6。类似地,$a[0]->[1] 是 2。我们这里拥有的就像一个二维数组;你可以写 $a[ROW]->[COLUMN] 来获取或设置数组中任何行和任何列的元素。

这种表示法看起来仍然有点笨拙,所以还有另一种简写

箭头规则

在两个下标之间,箭头是可选的。

我们可以写 $a[1][2] 来代替 $a[1]->[2];它们的意思相同。我们可以写 $a[0][1] = 23 来代替 $a[0]->[1] = 23;它们的意思相同。

现在它看起来真的像二维数组了!

你可以看到箭头为什么很重要。如果没有它们,我们必须写 ${$a[1]}[2] 而不是 $a[1][2]。对于三维数组,它们让我们写 $x[2][3][5] 而不是不可读的 ${${$x[2]}[3]}[5]

解决方案

这是我之前提出的问题的答案,即重新格式化城市和国家名称文件。

 1   my %table;

 2   while (<>) {
 3     chomp;
 4     my ($city, $country) = split /, /;
 5     $table{$country} = [] unless exists $table{$country};
 6     push @{$table{$country}}, $city;
 7   }

 8   for my $country (sort keys %table) {
 9     print "$country: ";
10     my @cities = @{$table{$country}};
11     print join ', ', sort @cities;
12     print ".\n";
13	}

该程序有两个部分:第 2-7 行读取输入并构建数据结构,第 8-13 行分析数据并打印报告。我们将有一个哈希 %table,它的键是国家名称,它的值是对城市名称数组的引用。数据结构将如下所示

   %table
+-------+---+
|       |   |   +-----------+--------+
|Germany| *---->| Frankfurt | Berlin |
|       |   |   +-----------+--------+
+-------+---+
|       |   |   +----------+
|Finland| *---->| Helsinki |
|       |   |   +----------+
+-------+---+
|       |   |   +---------+------------+----------+
|  USA  | *---->| Chicago | Washington | New York |
|       |   |   +---------+------------+----------+
+-------+---+

我们将首先查看输出。假设我们已经有了这个结构,我们如何打印它呢?

 8   for my $country (sort keys %table) {
 9     print "$country: ";
10     my @cities = @{$table{$country}};
11     print join ', ', sort @cities;
12     print ".\n";
13	}

%table 是一个普通的哈希表,我们从中获取键的列表,对键进行排序,并像往常一样循环遍历键。引用只在第 10 行使用。$table{$country} 在哈希表中查找键 $country 并获取其值,该值是对该国家城市数组的引用。 使用规则 1 指出我们可以通过 @{$table{$country}} 来恢复数组。第 10 行就像

@cities = @array;

除了名称 array 被引用 {$table{$country}} 替换之外。@ 告诉 Perl 获取整个数组。获取城市列表后,我们对其进行排序、连接并像往常一样打印出来。

第 2-7 行负责构建结构。以下是它们再次出现的情况

2   while (<>) {
3     chomp;
4     my ($city, $country) = split /, /;
5     $table{$country} = [] unless exists $table{$country};
6     push @{$table{$country}}, $city;
7   }

第 2-4 行获取城市和国家名称。第 5 行检查国家是否已作为哈希表中的键存在。如果不存在,程序使用 [] 符号(创建规则 2)来制造一个新的空匿名城市数组,并将对它的引用安装到哈希表中,对应于相应的键。

第 6 行将城市名称安装到相应的数组中。$table{$country} 现在保存对该国家迄今为止所见城市数组的引用。第 6 行与

push @array, $city;

完全相同,除了名称 array 被引用 {$table{$country}} 替换之外。 push 将城市名称添加到所引用数组的末尾。

我跳过了一个细节。第 5 行是多余的,我们可以删除它。

2   while (<>) {
3     chomp;
4     my ($city, $country) = split /, /;
5   ####  $table{$country} = [] unless exists $table{$country};
6     push @{$table{$country}}, $city;
7   }

如果 %table 中已经存在当前 $country 的条目,那么没有任何区别。第 6 行将定位 $table{$country} 中的值,该值是对数组的引用,并将 $city 推入数组。但是当 $country 持有一个键,比如 Greece,而它还没有在 %table 中时,它会做什么呢?

这是 Perl,所以它会做正确的事情。它看到你想将 Athens 推入一个不存在的数组,因此它会为你帮助创建一个新的空匿名数组,将其安装到 %table 中,然后将 Athens 推入其中。这被称为自动创建——自动将事物变为现实。Perl 看到键不在哈希表中,因此它自动创建了一个新的哈希表条目。Perl 看到你想将哈希表值用作数组,因此它自动创建了一个新的空数组,并将对它的引用安装到哈希表中。并且像往常一样,Perl 使数组长度增加一个元素,以容纳新的城市名称。

其余部分

我承诺用 10% 的细节为你提供 90% 的益处,这意味着我省略了 90% 的细节。现在你已经了解了重要部分的概述,阅读 perlref 手册页应该更容易,它讨论了 100% 的细节。

一些 perlref 的亮点

您可能更愿意继续学习 perllol 而不是 perlref;它详细讨论了列表的列表和多维数组。之后,您应该继续学习 perldsc;它是一个数据结构食谱,展示了使用和打印数组的哈希、哈希的数组以及其他类型数据的配方。

摘要

每个人都需要复合数据结构,在 Perl 中,你可以通过引用来获取它们。管理引用有四个重要的规则:两个用于创建引用,两个用于使用它们。一旦你了解了这些规则,你就可以完成大多数与引用相关的操作。

版权

作者:Mark Jason Dominus,Plover Systems ([email protected])

本文最初发表于《Perl 杂志》(http://www.tpj.com/)第 3 卷,第 2 期。经许可转载。

原文标题为《今日理解引用》。

分发条件

版权所有 1998 Perl 杂志。

本文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。

无论其分发方式如何,这些文件中所有代码示例均在此归入公有领域。您被允许并鼓励在您自己的程序中使用此代码,无论出于娱乐目的还是盈利目的,您都可以随意使用。在代码中添加一个简单的评论以表示感谢将是礼貌的,但并非必需。