perlreftut - Mark's very short tutorial about referencesMark写的一篇关于引用的简短入门 概要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的开发过程中,我们仍然坚持这种设计:散列的值必须是标量。而对这个问题的解决方案则是采用引用。引用是一个标量,它的值指向一个数组或者散列(或其他任意值)。名字就是我们大家都非常熟悉的一种引用。以美国总统为例,这是一个人,一个生命体。但是谈到他的时候,或者是在计算机程序中要表示他的时候,我们仅仅需要一个简单的,易用的字符串“GeorgeBush”而Perl里面的引用就好像是数组和散列的名字。这些名字是Perl私有的,内部使用的名字,因此不会产生混乱。与“GeorgeBush”不同,一个引用只能指向一个事物,并且我们始终知道它所指向的究竟是什么。如果有一个散列的引用,那么我们可以完全操作整个散列。而引用本身,仍然只是一个简单的标量值。散列的值不能是数组,只能是标量。这是不变的。但是一个引用可以表示整个的数组,而且引用是标量,所以我们可以创建一个引用的散列,这些引用指向一些数组。这样用起来就好像是一个数组的散列了,而且功能也相似。下面来看一些使用引用的语法,之后再回到这个城市和国家名字的问题上来。 语法创建引用的方法只有两种,访问引用的方法也只有两种。创建引用创建方法1通过在变量名称前面加一个\可以得到该变量的引用 $aref = \@array; # $aref now holds a reference to @array $href = \%hash; # $href now holds a reference to %hash $sref = \$scalar; # $sref now holds a reference to $scalar只要这个引用被保存在像$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,所有需要引用完成的工作都可以实现。但是对数组和散列来说,常见的操作是读取其中一个元素,此时方法1的形式就显得有些麻烦,于是有了简化形式。${$aref}[3]太难读了,于是可以用$aref->[3]来替换。${$href}{red}太难读了,于是可以用$href->{red}来替换。如果$aref保存了一个数组的引用,那么$aref->[3]就是这个数组的第四个元素。不要与$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]可以得到该数组的第三个元素,也就是那个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;只是数组的名字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的记录,那么第5行是否存在并无不同。第6行会找到这个元素$table{$country},也就是数组的引用,然后将$city加入该数组中。但是,如果散列%talbe中没有这个国家名字$country,如Greece,会如何?这是Perl,因此会按照正确的方法处理。如果它看到你想把Athens加入一个不存在的数组中,它会帮助你创建一个新的、空的、匿名数组,并把它赋值给%table,然后把Athens加入其中。这称为“自动代入”——自动创建事物。Perl看到了这个键不存在于散列中,因此它自动创建了该键的元素。Perl看到了你想把散列的值赋值为数组,于是它自动创建了一个新的空的匿名数组并将其引用赋值给散列。进而,Perl给该数组增加了一个元素来保存新加入的城市名字。其他我说过要通过10%的细节来实现90%的功能,这意味着我省略了90%的细节。至此,你应该已经大致了解了重要的部分,接下来阅读perlfef手册将会简单一些,手册中有100%的完整细节。下面给出perlref手册中的几个重要点:l 可以为任意事物创建引用,包括标量、函数以及其他引用。2 在使用方法1中,如果花括号里面的东东是一个原子标量,如$aref,则可以省略花括号。例如,@$aref等价于@{$aref},$$aref[1]等价于${$aref}[1]。如果你是初学,最好是不要省略花括号。3 注意下列形式并不复制数组$aref2 = $aref1该操作只是得到了指向同一个数组的两个引用。如果修改$aref1[23],然后读取$aref2->[23],将会看到同样的改动。要复制数组,用如下形式$aref2 = [@{$aref1}];使用[…]来创建新的匿名数组,于是$aref2被赋值为新数组的引用。新数组用$aref1所指向的数组做初始化。同样的,要复制一个散列,可以采用如下形式$href2 = {%{$href1}};4 用ref函数来查看一个变量是否是引用。如果该变量是引用,返回true。其实,返回值的意义还要丰富,如果是个散列的引用,会返回HASH,如果是数组的引用,会返回ARRAY。5 如果想字符串那样使用一个引用,则得到类似下面的字符串ARRAY(0x80f5dec) or HASH(0x826afc0)如果看到了类似的字符串,就知道你错误的打印了一个引用。这种表示方法的另一个作用是,你可以用eq来判断两个引用是否指向同一个东东。(不过最好还是用==,因为会更快一些。)6 字符串可以当引用来使用。如果把字符串“foo”当做数组的引用,它将被看作是数组@foo的引用。这称为软引用或者符号引用。usestrict ‘refs’声明禁止了这个特性,因为如果你不小心用到它,可能会产生各种麻烦。或许,与perlref手册先比,你更想去看perllol手册,因为它详细的讨论了列表的列表一级多维数组。此外,可以查阅perldsc手册,这是数据结构手册,包含了使用和输出散列的数组、数组的散列以及其他数据结构的方法。总结人人都需要复合的数据结构,而在Perl中,实现的方法就是引用。处理引用有四个主要的方法:两个创建方法和两个使用方法。只要掌握了这四种方法,就可以用引用来实现大部分的功能。 致谢作者:Mark Jason Dominus, Plover Systems ([email protected])本文最初发表于The Perl Journal ( http://www.tpj.com/ ) volume 3, #2.Reprinted with permission.原标题为Understand References Today Distribution ConditionsCopyright 1998 The Perl Journal.This documentation is free; you can redistribute it and/ormodify it under the same terms as Perl itself.Irrespective of its distribution, all code examples in thesefiles are hereby placed into the public domain. You are permittedand encouraged to use this code in your own programs for fun or forprofit as you see fit. A simple comment in the code giving creditwould be courteous but is not required.(译注:法律词汇不知道该怎么翻译,直接原文复制过来) 09-03 17:50