内容

名称

perllol - 在 Perl 中操作数组的数组

说明

声明和访问数组的数组

在 Perl 中构建的最简单的两级数据结构是数组的数组,有时也称为列表的列表。它很容易理解,而且这里应用的几乎所有内容稍后在更高级的数据结构中也适用。

数组的数组只是一个普通的旧数组 @AoA,你可以使用两个下标来获取,例如 $AoA[3][2]。以下是数组的声明

    use v5.10;  # so we can use say()

    # assign to our array, an array of array references
    @AoA = (
	   [ "fred", "barney", "pebbles", "bambam", "dino", ],
	   [ "george", "jane", "elroy", "judy", ],
	   [ "homer", "bart", "marge", "maggie", ],
    );
    say $AoA[2][1];
  bart

现在你应该非常小心,外部括号类型是圆括号,即括号。这是因为你正在分配给 @array,所以你需要括号。如果你希望那里没有 @AoA,而只是对其的引用,你可以执行更类似于以下操作

    # assign a reference to array of array references
    $ref_to_AoA = [
	[ "fred", "barney", "pebbles", "bambam", "dino", ],
	[ "george", "jane", "elroy", "judy", ],
	[ "homer", "bart", "marge", "maggie", ],
    ];
    say $ref_to_AoA->[2][1];
  bart

请注意,外部括号类型已更改,因此我们的访问语法也已更改。这是因为与 C 不同,在 perl 中你不能自由地交换数组及其引用。 $ref_to_AoA 是对数组的引用,而 @AoA 是一个适当的数组。同样,$AoA[2] 不是一个数组,而是一个数组引用。那么你如何编写这些

$AoA[2][2]
$ref_to_AoA->[2][2]

而不是必须写这些

$AoA[2]->[2]
$ref_to_AoA->[2]->[2]

嗯,那是因为规则是,在相邻的括号上(无论是方括号还是花括号),你可以自由地省略指针解引用箭头。但是,如果它是包含引用的标量,那么你不能对第一个括号这样做,这意味着 $ref_to_AoA 总是需要它。

发展你自己的

对于固定数据结构的声明来说,这很好,但是如果你想动态添加新元素,或者从头开始构建它,该怎么办?

首先,让我们看看如何从文件中读取它。这就像一次添加一行。我们假设有一个平面文件,其中每一行都是一行,每个单词都是一个元素。如果你想开发一个包含所有这些内容的 @AoA 数组,这是正确的方法

    while (<>) {
	@tmp = split;
	push @AoA, [ @tmp ];
    }

你也可以从函数中加载它

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

或者你可能有一个临时变量,其中包含数组。

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

重要的是,你必须确保使用 [ ] 数组引用构造函数。那是因为这不起作用

$AoA[$i] = @tmp;   # WRONG!

它不起作用的原因是,将这样的命名数组分配给标量是在标量上下文中获取数组,这意味着只计算 @tmp 中的元素数。

如果你在 use strict 下运行(如果不是,为什么不呢?),你必须添加一些声明才能使其正常工作

    use strict;
    my(@AoA, @tmp);
    while (<>) {
	@tmp = split;
	push @AoA, [ @tmp ];
    }

当然,你根本不需要临时数组有名称

    while (<>) {
	push @AoA, [ split ];
    }

你也不必使用 push()。如果你知道要将它放在哪里,你可以直接赋值

    my (@AoA, $i, $line);
    for $i ( 0 .. 10 ) {
	$line = <>;
	$AoA[$i] = [ split " ", $line ];
    }

或者甚至只是

    my (@AoA, $i);
    for $i ( 0 .. 10 ) {
	$AoA[$i] = [ split " ", <> ];
    }

一般来说,你应该谨慎使用可能在标量上下文中返回列表的函数,而不明确说明这一点。对于普通读者来说,这会更清楚

    my (@AoA, $i);
    for $i ( 0 .. 10 ) {
	$AoA[$i] = [ split " ", scalar(<>) ];
    }

如果你想让 $ref_to_AoA 变量作为对数组的引用,你必须执行类似这样的操作

    while (<>) {
	push @$ref_to_AoA, [ split ];
    }

现在你可以添加新行。添加新列呢?如果你只处理矩阵,通常最简单的方法是使用简单赋值

    for $x (1 .. 10) {
	for $y (1 .. 10) {
	    $AoA[$x][$y] = func($x, $y);
	}
    }

    for $x ( 3, 7, 9 ) {
	$AoA[$x][20] += func2($x);
    }

这些元素是否已经存在并不重要:它会很乐意为你创建它们,根据需要将中间元素设置为 undef

如果你只想追加到一行,你必须做一些看起来有点好笑的事情

# add new columns to an existing row
push $AoA[0]->@*, "wilma", "betty";   # explicit deref

访问和打印

现在是打印数据结构的时候了。你打算怎么做?嗯,如果你只想使用其中一个元素,那很简单

print $AoA[0][0];

不过,如果你想打印整个内容,你不能说

print @AoA;		# WRONG

因为你只会得到列出的引用,而 perl 永远不会自动为你解除引用的内容。相反,你必须自己滚动一两个循环。这将打印整个结构,使用 shell 样式的 for() 构造函数在外部下标集上循环。

    for $aref ( @AoA ) {
	say "\t [ @$aref ],";
    }

如果您想跟踪下标,可以这样做

    for $i ( 0 .. $#AoA ) {
	say "\t elt $i is [ @{$AoA[$i]} ],";
    }

或者甚至这样做。注意内部循环。

    for $i ( 0 .. $#AoA ) {
	for $j ( 0 .. $#{$AoA[$i]} ) {
	    say "elt $i $j is $AoA[$i][$j]";
	}
    }

如您所见,它变得有点复杂。这就是为什么有时在途中使用临时变量会更容易

    for $i ( 0 .. $#AoA ) {
	$aref = $AoA[$i];
	for $j ( 0 .. $#{$aref} ) {
	    say "elt $i $j is $AoA[$i][$j]";
	}
    }

嗯... 这仍然有点丑陋。这个怎么样

    for $i ( 0 .. $#AoA ) {
	$aref = $AoA[$i];
	$n = @$aref - 1;
	for $j ( 0 .. $n ) {
	    say "elt $i $j is $AoA[$i][$j]";
	}
    }

当您厌倦为数据结构编写自定义打印时,可以查看标准 DumpvalueData::Dumper 模块。前者是 Perl 调试器使用的,而后者生成可解析的 Perl 代码。例如

 use v5.14;     # using the + prototype, new to v5.14

 sub show(+) {
	require Dumpvalue;
	state $prettily = new Dumpvalue::
			    tick        => q("),
			    compactDump => 1,  # comment these two lines
                                               # out
			    veryCompact => 1,  # if you want a bigger
                                               # dump
			;
	dumpValue $prettily @_;
 }

 # Assign a list of array references to an array.
 my @AoA = (
	   [ "fred", "barney" ],
	   [ "george", "jane", "elroy" ],
	   [ "homer", "marge", "bart" ],
 );
 push $AoA[0]->@*, "wilma", "betty";
 show @AoA;

将打印出

0  0..3  "fred" "barney" "wilma" "betty"
1  0..2  "george" "jane" "elroy"
2  0..2  "homer" "marge" "bart"

而如果您注释掉我所说的两行,则会以这种方式显示

0  ARRAY(0x8031d0)
   0  "fred"
   1  "barney"
   2  "wilma"
   3  "betty"
1  ARRAY(0x803d40)
   0  "george"
   1  "jane"
   2  "elroy"
2  ARRAY(0x803e10)
   0  "homer"
   1  "marge"
   2  "bart"

切片

如果您想获取多维数组中的一部分(切片),您将不得不进行一些花哨的下标。这是因为虽然我们通过用于取消引用的指针箭头为单个元素提供了很好的同义词,但对于切片不存在这样的便利。

以下是使用循环执行一个操作的方法。我们将像以前一样假设一个 @AoA 变量。

    @part = ();
    $x = 4;
    for ($y = 7; $y < 13; $y++) {
	push @part, $AoA[$x][$y];
    }

相同的循环可以用切片操作替换

@part = $AoA[4]->@[ 7..12 ];

现在,如果您想要一个二维切片,例如让 $x 从 4..8 运行,让 $y 从 7 到 12 运行?嗯... 这是简单的方法

    @newAoA = ();
    for ($startx = $x = 4; $x <= 8; $x++) {
	for ($starty = $y = 7; $y <= 12; $y++) {
	    $newAoA[$x - $startx][$y - $starty] = $AoA[$x][$y];
	}
    }

我们可以减少一些通过切片的循环

    for ($x = 4; $x <= 8; $x++) {
	push @newAoA, [ $AoA[$x]->@[ 7..12 ] ];
    }

如果您进入 Schwartzian 变换,您可能会选择 map

@newAoA = map { [ $AoA[$_]->@[ 7..12 ] ] } 4 .. 8;

虽然如果您的经理指责您通过难以理解的代码寻求工作保障(或快速的不安全感),那么很难争辩。 :-) 如果我是您,我会把它放在一个函数中

    @newAoA = splice_2D( \@AoA, 4 => 8, 7 => 12 );
    sub splice_2D {
	my $lrr = shift; 	# ref to array of array refs!
	my ($x_lo, $x_hi,
	    $y_lo, $y_hi) = @_;

	return map {
	    [ $lrr->[$_]->@[ $y_lo .. $y_hi ] ]
	} $x_lo .. $x_hi;
    }

另请参阅

perldata, perlref, perldsc

作者

Tom Christiansen <[email protected]>

最后更新:2011 年 4 月 26 日星期二下午 6:30:55 MDT