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 的私有内部名称,因此你可以确保它们是明确的。与人类姓名不同,引用只引用一个东西,并且你总是知道它引用的是什么。如果你有一个数组的引用,你可以从中恢复整个数组。如果你有一个哈希的引用,你可以从中恢复整个哈希。但是引用仍然是一个简单、紧凑的标量值。
你不能拥有一个值是数组的哈希;哈希值只能是标量。我们被困住了。但是一个引用可以引用整个数组,而引用是标量,因此你可以拥有一个指向数组的引用的哈希,它将非常像一个数组的哈希,并且它将与数组的哈希一样有用。
我们将在稍后回到这个城市-国家问题,在我们了解了一些管理引用的语法之后。
创建引用只有两种方式,使用引用也只有两种方式。
如果在变量前面加上 \
,你将获得该变量的引用。
$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 的方式,而无需先将它们存储在一个有名称的变量中。
[ 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
。
如果你只写 []
,你将获得一个新的空匿名数组。如果你只写 {}
,你将获得一个新的空匿名哈希。
获得引用后,你可以用它做什么?它是一个标量值,我们已经看到你可以像任何标量一样存储它并再次获取它。还有两种使用它的方法。
你始终可以使用数组引用(用花括号括起来)来代替数组的名称。例如,@{$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";
}
规则 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 的亮点
你可以对任何东西创建引用,包括标量、函数和其他引用。
在 使用规则 1 中,当括号内的内容是原子标量变量(如 $aref
)时,可以省略花括号。例如,@$aref
等同于 @{$aref}
,$$aref[1]
等同于 ${$aref}[1]
。如果您刚开始学习,建议您养成始终包含花括号的习惯。
这不会复制底层数组。
$aref2 = $aref1;
您将获得指向同一个数组的两个引用。如果您修改了 $aref1->[23]
,然后查看 $aref2->[23]
,您将看到更改。
要复制数组,请使用
$aref2 = [@{$aref1}];
这使用 [...]
符号创建了一个新的匿名数组,并将 $aref2
赋值为指向新数组的引用。新数组使用 $aref1
所指向的数组的内容进行初始化。
类似地,要复制匿名哈希,可以使用
$href2 = {%{$href1}};
要查看变量是否包含引用,请使用 ref
函数。如果其参数是引用,则返回 true。实际上,它比这更好:它对哈希引用返回 HASH
,对数组引用返回 ARRAY
。
如果您尝试将引用用作字符串,您将获得类似的字符串
ARRAY(0x80f5dec) or HASH(0x826afc0)
如果您看到类似的字符串,您将知道您错误地打印了引用。
这种表示形式的一个副作用是,您可以使用 eq
来查看两个引用是否指向同一个东西。(但您通常应该使用 ==
,因为它快得多。)
您可以将字符串用作引用。如果您将字符串 "foo"
用作数组引用,它将被视为指向数组 @foo
的引用。这被称为符号引用。声明 use strict 'refs'
会禁用此功能,如果您意外使用它,可能会导致各种问题。
您可能更愿意继续学习 perllol 而不是 perlref;它详细讨论了列表的列表和多维数组。之后,您应该继续学习 perldsc;它是一个数据结构食谱,展示了使用和打印数组的哈希、哈希的数组以及其他类型数据的配方。
每个人都需要复合数据结构,在 Perl 中,你可以通过引用来获取它们。管理引用有四个重要的规则:两个用于创建引用,两个用于使用它们。一旦你了解了这些规则,你就可以完成大多数与引用相关的操作。
作者:Mark Jason Dominus,Plover Systems ([email protected]
)
本文最初发表于《Perl 杂志》(http://www.tpj.com/)第 3 卷,第 2 期。经许可转载。
原文标题为《今日理解引用》。
版权所有 1998 Perl 杂志。
本文档是免费的;您可以在与 Perl 本身相同的条款下重新分发和/或修改它。
无论其分发方式如何,这些文件中所有代码示例均在此归入公有领域。您被允许并鼓励在您自己的程序中使用此代码,无论出于娱乐目的还是盈利目的,您都可以随意使用。在代码中添加一个简单的评论以表示感谢将是礼貌的,但并非必需。