谁需要复杂的数据结构?
始终伴随着Perl 4的一个问题是怎么样来描述一个值是列表的哈希表。Perl 4 有哈希但是它的值必须是标量,不能是列表。
你为什么需要一个列表的哈希结构呢?让我们来看一个简单的例子:你有一个包含城市和国家名称的文件,如下:
Chicago, USA
Frankfurt, Germany
Berlin, Germany
Washington, USA
Helsinki, Finland
New York, USA
你想按照下面的格式输出,每一个国家出现一次,然后是一个按照字母顺序的该国家内城市的列表,如下:
Finland: Helsinki.
Germany: Berlin, Frankfurt.
USA: Chicago, New York, Washington.
一个很自然的解决方法是将国家的名称设置为一个哈希结构的键,和国家名称对应的健值是这些国家内的城市的一个数组。 每一次,你读取一行输入,将其分成国家和城市两个数据。查看'国家名称'的键所对应的城市列表,然后把新的城市加入那个列表中。当你完成了整个输入,然后将整个哈希中每个健对应的健值-城市名的列表作一个排序,然后打印出来。
如果哈希值不能是列表,你就无法完成工作了。在Perl 4中,哈希的值不能是列表。它们只能是字符串。所以你必须把所有的城市名合并成一个字符串。当需要输出时,你再把这个字符串分拆成一个列表,然后对列表排序,最后将列表中的数据转成字符串输出。这样做非常烦琐并容易出错。而且让人沮丧的是,Perl已经拥有的优秀的列表功能无法使用了。
--------------------------------------------------------------------------------
解决方法
当Perl进化到Perl 5时,我们必须考虑到哈希原来的设计:哈希的键值必须是标量。解决办法就是采用'引用'。
一个'引用'就是一个指向一个完整列表或完整哈希(或指向其他的东西,比如函数)的标量值。名字就是你比较熟悉的一种'引用'。 考虑一下美国的总统:一包杂乱的血肉。但是当我们谈到它,或者需要在计算机程序中描述它,你需要的只是一个简单的标量'George Bush'。
Perl中的'引用'就像列表和哈希的名字。它们是Perl中私有的,内部的名字,所以你可以确信它们是明确的,不像'George Bush', 一个'引用'只指向一个东西。你总是可以知道它指向什么。如果你有一个指向数组的'引用',你可以从它恢复出整个数组。如果你有一个指向哈希的'引用',你可以根据它恢复出整个哈希。但是这个'引用'仍旧是一个简单、紧凑的标量。
你不能使用一个健值是数组的哈希;哈希的健值必须是标量。我们被这个束缚住了。但是一个简单的'引用'能指向一个完整的数组,'引用'是标量,所以你可以使用指向数组的'引用'组成的哈希,它就像一个数组的哈希一样,和数据的哈希一样有用。
稍后我们会再回到这个‘城市-国家’的问题来。我们先来看一些操作'引用'的语法。
--------------------------------------------------------------------------------
语法
创建'引用'仅有两种方法,使用它也是两种。
创建引用
创建规则 1
如果你在一个变量前加一个'/'号,你就得到了这个变量的'引用'。
$aref = /@array; # $aref 保存着指向@array的'引用'
$href = /%hash; # $href 保存着指向%hash的'引用'
当你把'引用'保存在类似 $aref 或 $href的变量中,你就可以象操作其他标量一样copy或保存它。
$xy = $aref; # $xy 现在保存了指向 @array 的'引用'
$p[3] = $href; # $p[3] 现在保存了指向 %hash 的'引用'
$z = $p[3]; # $z 现在保存了指向 %hash 的'引用'
这些例子展示了如何创建命名变量的'引用',但是有时候,我们创建的数组或哈希没有名字。这个和你使用没有放到变量中去的字符串'/n'或数字'80'类似。
创建规则 2
[ ITEMS ] 创建了一个新的、匿名的数组,并返回一个指向这个数组的'引用'。 { ITEMS } 创建了一个新的、匿名的哈希,并返回那个哈希的一个'引用'。
$aref = [ 1, "foo", undef, 13 ];
# $aref 保存了这个数组的'引用'
$href = { APR =>; 4, AUG =>; 8 };
# $href 保存了这个哈希的'引用'
从规则 2 中得到的'引用'和从规则 1 中得到的'引用'是同一种类型的:
# 这里:
$aref = [ 1, 2, 3 ];
# 和上面一样:
@array = (1, 2, 3);
$aref = /@array;
前面一种方法是后面两行的缩写,除了第一种方法没有创建一个多余的数组变量@array。
如果你只是编写符号 [], 你将得到一个新的、空匿名数组。如果你使用符号 {},就能得到一个新的、空匿名哈希。
使用引用
当你创建了一个'引用'后,你可以对它做什么操作呢?它是标量,你可以象处理任何标量一样保存和取回它。除此之外,还有两种使用方法:
使用规则 1
你可以始终用一个带有大括号的数组'引用',来替换一个数组的名字。例如,用 @{$aref} 代替 @array。
下面是一个用法的一些例子:
数组:
@a @{$aref} 一个数组
reverse @a reverse @{$aref} 对一个数组做倒序排序
$a[3] ${$aref}[3] 数组中的一个成员
$a[3] = 17; ${$aref}[3] = 17 对一个成员赋值
上面每行中,两个表达式实现的是同一种功能。左边那个是对数组@a操作,右边那个是对'引用'$aref所指向的数组操作。它们对数组产生相同的作用。
使用哈希的'引用'和数组的'引用'完全一样。
%h %{$href} 一个哈希
keys %h keys %{$href} 从哈希中将键取出来
$h{'red'} ${$href}{'red'} 哈希中的一个成员
$h{'red'} = 17 ${$href}{'red'} = 17 对一个成员赋值
你对一个'引用'无论想做什么,使用规则 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 foreach $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 foreach $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;
不同的是这里的数组的名字被'引用' {$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;
不同的是这里的数组名被{$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的记录,那么,加不加第5行没有任何区别。第6行会自己定位到$table{$country}这个'引用'指向的数组,把值 $city 放到数组中去。但是如果在%table中没有那个键,比如Greece,那么它会怎么办呢?
这是Perl,它会自己准确地完成工作。你想把一个Athens赋值给一个不存在地数组,那么Perl会帮助你创建一个新的、空的匿名数组,将它放到哈希%table里面去,然后把值 Athens 放到这个数组中。这个被称为'自动生成' --让事物自己自动产生出来。 Perl 发现在哈希里面没有这个键,就自动地创建了一个新的哈希记录。 Perl 发现你想要使用数组作为哈希的健值,它就自动创建一个匿名的空数组,并将指向这个数组的'引用'放到那个哈希中去。一般, Perl 创建的数组只有一个成员大小,用于保存这个新的城市名。
--------------------------------------------------------------------------------
其他集锦
我承诺以10%的细节来使你得到90%的好处,那就意味着我跳过了90%的知识的细节。现在来看一下其中的重要的部分,这个比阅读手册 the perlref manpage 要容易得多,手册讨论了100%的细节。
手册 the perlref manpage中的一些集锦:
你可以对任何东西创建'引用',包括标量,函数和其他的引用。
在 使用规则 1 中,当大括号里面是一个象$aref这样的标量变量时,你可以省略掉这个大括号。例如, @$aref 和 @{$aref}是一样的,$$aref[1] 和 ${$aref}[1]是一样的。 如果你是初学者,建议你还是养成加上大括号的习惯。
下面的操作不会copy '引用'指向的数组:
$aref2 = $aref1;
你将得到两个'引用',它们都指向同一个数组。如果你修改了$aref1->[23]的值,那么你查看变量$aref2->[23]时,它也相应地变了。
要copy这个数组,你需要这样
$aref2 = [@{$aref1}];
使用符号 [...] 来创建一个新的匿名数组, 而且这个新的数组的'引用'被赋值给了$aref2 。 这个新的数组用'引用'$aref1所指向的数组的内容来初始化。
同样的,要copy一个匿名哈希,你需要这样
$href2 = {%{$href1}};
如果要判断一个变量保存的内容是不是'引用',使用函数ref 。如果它的参数是'引用',返回的值是'真'。实际上,它做得更好:如果是一个哈希的引用,它返回'HASH',如果是一个数组的引用,那么就返回'ARRAY'。
如果你想像字符串一样使用'引用'的话,你得到的字符串就像
ARRAY(0x80f5dec) or HASH(0x826afc0)
如果看到一个像这样的字符串,你应该知道你错误地输出了一个'引用'。
这种显示方式的另一个作用是你可以用eq来比较两个'引用',看它们是不是指向相同的东西。(你通常可以使用 == 来比较,因为它更会)
你可以像使用'引用'一样来使用一个字符串。如果你使用"foo"作为一个数组的'引用',它就是指向数组 @foo的一个引用。这被称为'软引用'或‘符号引用 '。 使用申明 use strict 'refs' 可以取消这个功能,如果你不小心使用了它,会导致各种可能的错误。
你可能更喜欢查看 the perllol manpage,而不是手册 the perlref manpage;它详细地讨论了列表的列表和多纬数组。然后,你可以继续学习手册 the perldsc manpage;它是数据结构的Cookbook, 它提供了处理哈希的数组,数组的哈希,以及其他数据结构的方法。
--------------------------------------------------------------------------------
摘要
每个人都需要复合的数据结构,在Perl中我们的办法是通过'引用'来实现。这里有四个重要的操作'引用'的规则:两个用于创建'引用',另外两个用于使用'引用'。 一旦掌握了这些规则,你可以用'引用'来处理很多重要的事情。
--------------------------------------------------------------------------------
感谢
作者: Mark Jason Dominus, Plover Systems ([email protected])
这篇文章最初出现在 The Perl Journal ( http://www.tpj.com/ ) volume 3, #2. 同意再版.
原来的标题是 Understand References Today.
发布要求
Copyright 1998 The Perl Journal.
This documentation is free; you can redistribute it and/or modify it under the same terms as Perl itself.
Irrespective of its distribution, all code examples in these files are hereby placed into the public domain. You are permitted and encouraged to use this code in your own programs for fun or for profit as you see fit. A simple comment in the code giving credit would be courteous but is not required.
perlreftut - Mark's very short tutorial about references
出自:http://www.chinaunix.net/jh/25/504623.html