概要

在5.0发布之前,Perl最大的一个缺憾就是无法处理复杂的数据结构。虽然语言本身并不支持,还是有许多用于创新的程序员尝试来实现这一特性。然而这必竟是很困难的事情。某些情况下,或许可以从awk中借用类似$m{$AoA,$b}的语法,这里的键看上去更像是”$AoA$b”这种字符串的拼接。但是采用这种形式,做遍历和排序就非常困难了。更有甚者,直接修改Perl内置的符号表,该方法被证明难以继续和维护。

 

Perl5.0版给我们引入了复杂数据结构。如今,你可以写出下面的代码,然后便出乎意料的有了一个三维数组。

    for $x (1 .. 10) {
        for $y (1 .. 10) {
            for $z (1 .. 10) {
                $AoA[$x][$y][$z] =
                    $x ** $y + $z;
            }
        }
    }

虽然看上去非常简单,但是,隐藏在其中的则是为了实现此功能而做的精心设计。

 

如何将其输出呢?为什么不直接print@AoA?如何排序?如何传递给一个函数,或者如何从函数的参数中读取?是一个对象么?能够保存到磁盘并供后续读取么?如何获取这个矩阵的整行或者整列呢?所有的元素都必须是数值么?

 

看到了吧,很容易就高不清楚了。一个小小的原因,是因为该数据结构是基于引用实现的。但是真正的大部分的原因,是因为缺少面向初学者的有丰富实例的文档。

 

本文档将详细而又清晰的介绍多种不同的经常会遇到的数据结构。同时还将提供许多例子。因此,如果你需要创建其中某些复杂的数据结构,你可以直接拿这里的例子修改一下使用。

 

 

下面逐个详细介绍这些数据结构。每一节介绍一种:

* 数组的数组

* 数组的散列

* 散列的数组

* 散列的散列

* 更复杂的结构

但是首先来看一个所有数据结构共同的问题

引用

理解Perl中所有数据结构(包括多维数组)的关键就是,不管以何种形式表现,Perl里面的数组和散列根本上都是一维的。且只能保存标量值(也就是字符串、数字或者引用),不能直接保存数组或者散列,但可以保存数组和散列的引用。

对数组或者散列的引用的使用方法与直接操作数组和散列是很不同的。对C或者C++程序员来说,这比较不爽,因为他们常常不区分数组名和指向数组的指针。但是可以换个思路,考虑一个结构体和指向该结构体的指针之间的区别。

更多信息可以参考perlref。简单的说,引用就好像是指针,而且引用知道他们所指向的东东是什么。(对象也是一种指针,但是至少现在我们不需要它)这就意味着,如果你发现某个东东看上去很像是多维的数组或者散列,那么本质上这个东东就是一个一维的集合,只不过其中的元素是指向下一维数据的引用。好处是你可以像二维结构那样的使用这个东东。这也正与大多数的C多维数组相似。

    $array[7][12]                       # array of arrays
    $array[7]{string}                   # array of hashes
    $hash{string}[7]                    # hash of arrays
    $hash{string}{'another string'}     # hash of hashes

由于第一层只包含引用,因此,如果想要输出这个数组,只用print函数的话,将得到一些奇怪的东东:

    @AoA = ( [2, 3], [4, 5, 7], [0] );
    print $AoA[1][2];
      7
    print @AoA;
      ARRAY(0x83c38)ARRAY(0x8b194)ARRAY(0x8b1d0)

这是因为Perl并不会,并且从来都不会,隐式的解引用。如果想要得到一个引用所指向的东东,就必须显示的使用如下方法:加前缀,如${$blah},@{$blah}, @{$blah[$i]},或者加后缀箭头,如$a->[3],$h->{fred},$ob->method()->[3]。

 

常见错误

在创建复杂数据结构,如数组的数组时,常见的两个错误,一个是本来要用引用但却只是取了数组元素的个数,另一个就是重复的取一个内存空间的地址。下面的例子就是没有取到引用而只是取到了元素个数。

    for $i (1..10) {
        @array = somefunc($i);
        $AoA[$i] = @array;      # WRONG!
    }

这只是一个简单的例子,把一个数组赋值给一个标量,从而得到了数组元素的个数。如果这真的是你期望的结果,那么你最好是考虑采用一种更加明确的形式,如

    for $i (1..10) {
        @array = somefunc($i);
        $counts[$i] = scalar @array;
    }

下面的例子就是反复的取一个内存空间的引用:

    for $i (1..10) {
        @array = somefunc($i);
        $AoA[$i] = \@array;     # WRONG!
    }

这样有什么问题么?好像没问题额,真的有问题么?毕竟,我只是告诉你需要一个数组,其元素为引用,结果呢,你创建了一个。

 

然而不幸的是,虽然是个引用的数组,但结果仍然不对。@AoA里面的所有引用都指向了同一个地方,因此,他们都将指向@array的最新状态。这跟如下的C程序中的问题很相似:

    #include
    main() {
        struct passwd *getpwnam(), *rp, *dp;
        rp = getpwnam("root");
        dp = getpwnam("daemon");
        printf("daemon name is %s\nroot name is %s\n",
                dp->pw_name, rp->pw_name);
            }

结果将是打印:

    daemon name is daemon
    root name is daemon

问题就在于rp和dp都指向了同一块内存空间。在C语言中,要记住在必要的时候用malloc分配新的内存空间。而在Perl中,将可能需要使用数组创建操作符[]或者散列创建操作符{}来分配新的内存空间。下面给出正确的方法:

    for $i (1..10) {
        @array = somefunc($i);
        $AoA[$i] = [ @array ];
    }

方括号的作用就是返回了一个新数组的引用,该数组是用@array进行初始化的。这才是你需要的东东。

 

注意下面的代码起到同样的作用,但是可读性差:

    for $i (1..10) {
        @array = 0 .. $i;
        @{$AoA[$i]} = @array;
    }

真的一样么?额,或许吧,也可能不一样。最根本的区别在于,如果用方括号的形式进行赋值,会明确的知道这是一个新的引用,对数据也做了复制。在这种新的写法下,以@{$AoA[$i]}的解引用形式作左值,可能会出现某些其他的情况。这全取决于$AoA[$i]是否是被undefine过了,还是已经包含了一个引用。如果已经给@AoA中赋值了一个引用,如

    $AoA[3] = \@another_array;

然后采用如下的间接赋值方式,结果就是用到了已经有的那个引用

    @{$AoA[3]} = @array;

当然,结果就是“很有趣的”将@another_array整个重新赋值了。(不知道你有米有注意到,当一个程序员说某件事“很有趣”的时候,他往往并不是说“很有意思,很好玩”,更多的可能是说这件事“很诡异”、“很麻烦”,或者二者兼有)

 

因此最好是只使用数组或者散列的构造符[]和{},将会减少很多错误,尽管可能并不总是最高效的方法。

奇怪的是,下面这段看上去很危险的代码其实没什么问题:
    for $i (1..10) {
        my @array = somefunc($i);
        $AoA[$i] = \@array;
    }
这是因为my()是运行时的声明而不是编译时的声明。也就意味着my()所声明的变量在每次循环时都是重新生成的新变量。因此,虽然看上去好像是每次都保存的同一个变量的引用,但实际上不是!这个微妙的细节能够生成更高效的代码,但是可能会迷惑那些没有经验的程序员。因此我通常不建议将此方法教给初学者。实际上,处了给函数传参之外,我不太希望在代码中使用引用的那个斜杠符号。相反,我建议初学者尽量使用更简单易懂的构造符[]和{}来实现功能,而不是依赖代码上下文或者生命周期。(译注:这几句的意思不太清楚)
总结:
    $AoA[$i] = [ @array ];      # usually best
    $AoA[$i] = \@array;         # perilous; just how my() was that array?
    @{ $AoA[$i] } = @array;     # way too tricky for most programmers
 
关于优先级的注意事项 

提到形如@{$AoA[$i]}的写法,如下几种形式都是相同的:

    $aref->[2][2]       # clear
    $$aref[2][2]        # confusing

这是因为Perl定义的优先级规则,使得五个前缀解引用符(这些符号看上去就好像某人在诅咒一样:$@*%&)与变量名的结合性高于后缀的括号!对C或C++程序员来说,这无疑是非常令人震惊的,因为他们非常习惯于用*a[i]来表示a的第i个元素所指向的数值,也就是首先取下标,然后对此下标的值做解引用。C里面是这样的,但是这里不是C。

在Perl里面看上去相似的结构,却有不同的解析顺序,$$aref[$i]首先对$aref做解引用,把$aref看做是一个数组的引用,解引用之后,取得$aref所指向的数组的第i个元素。如果你希望用C的形式,就必须写成${$AoA[$i]},在开头的$解引用之前首先取得$AoA[$i]的值。(译注:这个地方有些问题,$i是一个变量,并不能理解为第i个元素。另外即便$i=i,说成是第i个元素也容易误解为起始下标为1)

 

为什么要用use strict

如果你开始觉得不知所措了,先放松一下。Perl有其机制来帮助你避免常见的错误。为了避免这些混乱,最好的方法就是每个程序都以下面的形式开始:
    #!/usr/bin/perl -w
    use strict;
这样,你就必须在每个变量前都用my来声明,同时也不允许使用“符号解引用”。因此,如果你用下面的代码:
    my $aref = [
        [ "fred", "barney", "pebbles", "bambam", "dino", ],
        [ "homer", "bart", "marge", "maggie", ],
        [ "george", "jane", "elroy", "judy", ],
    ];
    print $aref[2][2];
编译器会立刻报运行时错误,因为你用了一个没有声明的变量@aref,同时还会建议你用下面的形式来替代:
    print $aref->[2][2]
 
调试 

在5.002版本之前,Perl的标准调试器无法很好的输出复杂数据结构的信息。在5.002及之后版本,调试器引入了一些新特性,包括命令行编辑和可以输出复杂数据结构的x命令。例如,根据上面对$aref的定义,调试器有如下输出(译注:原文都是用的$AoA,但是上面的例子中赋值给了$aref,怀疑是弄混了)

    DB x $AoA
    $AoA = ARRAY(0x13b5a0)
       0  ARRAY(0x1f0a24)
          0  'fred'
          1  'barney'
          2  'pebbles'
          3  'bambam'
          4  'dino'
       1  ARRAY(0x13b558)
          0  'homer'
          1  'bart'
          2  'marge'
          3  'maggie'
       2  ARRAY(0x13b540)
          0  'george'
          1  'jane'
          2  'elroy'
          3  'judy'
09-26 14:27