Chinaunix首页 | 论坛 | 博客
  • 博客访问: 427708
  • 博文数量: 83
  • 博客积分: 2010
  • 博客等级: 大尉
  • 技术积分: 900
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-02 01:33
文章分类

全部博文(83)

文章存档

2011年(1)

2010年(5)

2009年(10)

2008年(4)

2007年(24)

2006年(39)

我的朋友

分类:

2009-06-28 23:57:56

Perl5 最重要的新特性之一就是可以管理复杂数据结构比如多维数组或者嵌套的哈希。为了实现这一功能,Perl5 引进了一个新的特性称为“引用”,引用是管理 Perl 中复杂数据结构的关键。不幸的是,这其中有大量有意思的语法需要学习,主体手册掌握起来有一定困难。虽然它相当完整,但是有时候也存在问题,它很难讲明哪些是重要的,哪些不是。

幸运的是,你只需要了解主体手册的 10% 就可以得到 90% 的好处。本指南就将向你展示这 10%。

[]

谁需要复杂数据结构

一直以来 Perl4 中都存在着如何表示一个包含的列表值的哈希结构的问题。Perl4 当然也有哈希,但是它的值只能是标量,而不能是列表。

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

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

你想产生这样一个输出,每个国家仅被提到一次,後面跟着按字母排序的该国城市的名称

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

很自然的方法就是用一个键值为国家名称的哈希来表示。与每个国家名称相关联的是这个国家的城市列表。每次你读入一行输入,就将它分割成一个国家名称和城市名称,在该国家已有的城市列表中查找名称或往列表中加入新的城市。当你读完输入之後,按照平常的方式迭代遍历整个哈希,并将每个列表中的城市排序後输出。

如果哈希的取值不能是列表,你就没法实现了。在 Perl4 中,哈希的值不能是列表,只能用字符串表示。你也许只能设法将所有城市组合成一个简单的字符串,然後在输出的时候将字符串分割成列表,将列表排序,再转换回字符串。这种方法凌乱而易出错,并且也很挫伤人,因为 Perl 本身有完美的列表可以解决这个问题,只要你可以使用它。

[]

解决方案在哪里?

在 Perl5 开始普及时,我们已经习惯于这样的设计:哈希的值必须是标量。这就需要使用引用来解决这个问题。

引用是一个指向整个数组或整个哈希(或者其他任何东西)的标量。姓名是你已经非常熟悉的引用之一。考虑一下美国总统:一副乱七八糟的皮肉。但是谈到他或者在程序中表示他,你只需要一个简单的标量字符串“乔治·布什”。

Perl 中的引用就像数组和哈希的名字。他们是 Perl 的私有和内部名字,所以你可以保证他们不会混淆。和“乔治·布什”不同的是,很多人都可以叫这个名字,但是一个引用却只指向一个事物,并且你总是了解它指向什么。如果你拥有一个数组的引用,那么你可以从中取得整个数组。如果你有一个哈希的引用,那么你也能恢复整个哈希。但是引用仍然是一个简单、紧凑的标量值。

你不能得到一个包含数组值的哈希;哈希值只能是标量。我们坚持这个原则。但是一个引用可以表示整个数组,并且引用是标量,所以你可以拥有一个包含哈希引用的数组,它看起来就像是数组的哈希并且和数组的哈希一样有用。

我们待会儿再讨论城市-国家问题,在我们看一下管理引用的语法之後。

[]

语法

有两种创建引用的方法,也只有两种方法可以使用它。

[]

创建引用

[]

创建规则一

如果你在变量前面加一个 \,那么你就得到了它的引用。

$aref = \@array;         # $aref 保存 @array 的引用
   $href = \%hash;          # $href 保存 %hash 的引用

一旦引用被储存在一个像 $aref 或 $href 的变量中,你就可以像其他标量值一样对它进行拷贝或储存。

$xy = $aref;             # $xy 保存 @array的引用
   $p[3] = $href;           # $p[3] 保存 %hash的引用
   $z = $p[3];              # $z 保存 %hash的引用


这些例子显示了如何从名字中创建引用。有时候你想要创建一个没有名字的数组或哈希的引用,这个就像你可以使用 \n 这个字符串或者 80 这样的直接量,而你并没有将他们存储在任何变量中一样。

[]

创建规则二

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

$aref = [ 1, "foo", undef, 13 ];  
   # $aref 保存一个数组的引用
$href = { APR => 4, AUG => 8 };   
   # $href 保存一个哈希的引用

通过规则一创建的引用与通过规则二创建的引用效果完全相同:

# 下面这句 $aref = [ 1, 2, 3 ];

# 其实就相当于 @array = (1, 2, 3); $aref = \@array;


第一行是下面两行的缩写,只不过它不创建多馀的数组变量 @array。

如果你输入只输入 [],你就会得到一个新的空的匿名数组。如果你只输入 {},你就会得到一个新的空的匿名哈希。

[]

使用引用

在得到引用之後你该如何使用它呢?这是一个标量,我们已经看到可以将它当作标量存储起来并像一个标量一样重新取得它。此外还有两种使用它的方法。

[]

用法规则一

你总是可以用花括号括起的数组引用来代替数组名称。例如,@{$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 给哈希的元素赋值

不论你想要用引用来做什么,使用用法规则一都可以做到。你只需要象往常一样使普通的数组或者哈希来写程序,然後再把数组或者哈希的名字换成 $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";
       }
[]

用法规则二

有了用法规则一在大多数时候就已经够用了,因为它告诉了你一个以不变应万变的引用用法。但是大多数时候我们只需要访问数组或者哈希的一个元素,这时候 法规则一就显得太繁琐了。所以下面介绍一种简写方式:

考虑一下如何通过一个引用去访问数组的第 4 个元素?得这样:${$aref}[3],可是这种写法很难读懂,那你可以写成 $aref->[3] 来代替。

${$href}{red} 也很难懂,那我们可以用 $href->{red} 代替。

如果 $aref 保存了一个指向数组的引用,那幺 $aref->[3] 的意思就是取这个数组的第 4 个元素。千万不好和 $aref[3] 混淆,後者表示的是 @aref 这个数组的第四个元素 ^_^。$ref 和 @aref 完全无关。

同样地,$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),并且,因为它是一个数组的引用,所以根据用法规则二,我们可以用 $a[1]->[2] 来获取这个数组的第三个元素。所以 $a[1]->[2] 就是 6。

同样地,$a[0]->[1] 就是 2,这就好像我们有了一个二维数组一样,你可以通过 $a[行]->[列] 来获取或者设置任意行任意列的元素的值。

这看起来仍然有些繁琐,所以下面再介绍一种更加简便的写法:

[]

箭头规则

在两个下标之间,箭头可以省略。

于是,$a[1]->[2] 就可以写作 $a[1][2],它们是同一个意思。 $a[0]->[1] = 23, 就可以写作 $a[0][1] = 23,它们也是同一个意思。

现在看上去真的和二维数组很像了!

箭头和省略箭头这两个规则非常重要。如果没有了它们,我们就必须把 $a[1][2] 写成是 $a{$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 对应的值,这个值就是一个指向这个国家城市列表的一个引用。根据用法规则一,我们可以用 @{$table{$country}} 还原这个数组。所以第 10 行就好像是:

@cities = @array;

唯一不同的地方是数组名 array 被 $table{$country} 代替了。@ 告诉 Perl 得到整个数组。然後我们对这个数组用 sort 排序、用 join 连接,最後打印它。

第 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 行检查国家名对应的键值是否已经在哈希表中存在。如果不存在,那么就用 [] 创建一个新的空匿名数组,并且把引用挂到哈希表中国家名对应的条目中。

第 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 对应的条目,那么有没有第 5 行并没有什么分别。第 6 行将定位到 $table{$country} 并且把 $city push 进去。但是如果哈希表中并没有 $country 这样一个键,那么会发生什么事呢?

Perl 对这时应该发生的行为做了明确的定义。如果你想要 push Athens 到一个数组,而那个数组并不存在,那幺 Perl 会友好地帮你创建一个新的、空的、匿名数组给你,并且把它挂在 %table 上,然後再把 Athens push 进去。这叫做 `autovivification'--bringing things to life automatically.(flw 注:这句实在不知该如何翻译)。Perl 看见那个键不在哈希中,于是就自动创建一个新的哈希条目;Perl 看见你想要把这个条目作为一个数组使用,于是它又自动地创建一个新的空数组并且自动把引用存储到哈希中。 And as usual, Perl made the array one element longer to hold the new city name.

[]

临了

我曾经答应过用 10% 的内容给你 90% 的好处,这意味着,还有 90% 的内容我没讲 ^_^。不过现在你已经对主要的东西有了一个大概的印象,因此你可以很容易地看懂 ,来品尝 100% 的内容。

阅读 时应该注意以下几点:

  • 你可以给任何东西创建引用,例如标量、函数、甚至是别的引用。
  • 用法规则一中,如果花括号中只有一个简单的引用的话,那么花括号就可以

省略。例如,@$aref 就相当于 @{$aref},$$aref[1] 就相当于 ${$aref}[1]。 If you're just starting out, you may want to adopt the habit of always including the curly brackets.

  • 还有一点,这样并不能复制它们指向的数组:
$aref2 = $aref1;

这样你只能得到两个指向相同的数组的引用。如果你修改了 $aref1->[23] 那幺 $aref2->[23] 会做同样的改变。

要想完全复制数组,你得这样:

$aref2 = [@{$aref1}];

符号 [...] 创建一个新的匿名数组,并且把引用赋值给 $aref2,这个新数组已经用 $aref1 的内容初始化过了。

同样地,要想复制匿名哈希,你得这样:

$href2 = {%{$href1}};
  • 因为引用本来就是一个标量,因此从表面上是无法知道一个标量是否就是一个引

用,这时你可以使用 ref 函数。如果传递给 ref 的参数是一个引用,它将返回真值,事实上,如果是一个哈希引用,那幺 ref 会返回 HASH,如果是一个数组引用,ref 则返回 ARRAY

  • 如果你把一个引用当作一个字符串来用的话,你将得到类似于这样的一个结果:

ARRAY(0x80f5dec) or HASH(0x826afc0)

如果你曾经看到过这样的字符串,那么说明你以前曾经打印过一个引用 ^_^

这个特性的一个副作用就是你可以使用 eq 运算符来比较两个引用是否指向同一个东西。但是建议你不要用 eq,而是用 ==,因为後面的这个更快一些。

  • 你也可以把一个字符串当作一个引用来用。如果你把 "foo" 当作一个数组引

用,那么相当于是在访问 @foo。这个技术在术语里叫做 软引用 或者 符号引用。use strict 'refs' 声明可以关掉这个特性,因为它经常给人们带来各式各样的烦恼。


你可能更喜欢看 而不是 。因为它讨论“列表的列表” 以及多维数组的技术。看完它之後,你还可以看 。这是一个“数据结构烹调指南》,它可以告诉你如何打印数组的哈希、哈希的数组等复杂的数据结构

阅读(818) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~