目錄

名稱

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 轉換,您可能會選擇用於此的映射

@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;
    }

另請參閱

perldataperlrefperldsc

作者

Tom Christiansen <tchrist@perl.com>

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