3. 串列與陣列
在我們已經知道怎麼使用純量變數之後,我們就可以處理非常多的工作。可是有些時候,當我們要使用純量變數來儲存許多性質相近的變數時,卻很容易遭遇瓶頸。例如我希望儲存某個班級四十位學生的數學期末成績,這時候如果每個學生的成績都需要用單獨的一個變數來儲存的話,那會讓資料難以處理,也許你從此再也不想寫程式了,而且你的程式大概會長的像這樣子:
my $first = '40';
my $second = '80';
my $third = '82';
...
...
沒錯,這樣的寫法雖然可能可以讓我們比過去使用紙張的方式正確率高一些,可是卻未必會省事。另外,如果我希望從資料庫找出今天總共有多少人在我的網路留言板留言,那這時候的留言個數是未知的,要怎麼批次處理這些資料就很花腦筋了,所以要有適當的資料結構可以作這樣的處理。
很顯然的,陣列的運用非常的廣泛,幾乎大部分撰寫程式的時候都會使用陣列來進行資料的存取,在許多程式語言中,陣列的結構相當的複雜,這確實是必要的。因為陣列的使用要必須足夠靈活,才能夠發揮它的功能,可是如果太過複雜卻也是造成入門者的進入門檻。Perl對於這方面卻有一些不同的做法,它提供的陣列結構非常簡單,如果你用最入門的方式去看它,很多第一次接觸的人甚至也可以輕易上手。可是Perl的陣列卻也可以利用非常強大的方式擴展開來,讓許多第一次看到Perl陣列結構卻非常失望的人也能重拾對Perl的信心。當然,使用這些技巧來進行Perl陣列的擴充,不但可以像其他程式語言一般,可以進行多維陣列之外,還可以能精準的結合某些資料結構,當然,這部份我們不會在一開始介紹陣列時就把大家嚇走,不過如果你已經對陣列的方式有些熟悉,可以在後面的章節慢慢看出Perl在這方面設計的巧妙。
3.1 何謂陣列
對於我們剛剛提出來的資料結構需求,希望能把相同的東西簡單的存取,並且讓它們能被歸納在一起。陣列正是解決這個問題的方案,也就是把一堆性質接近的變數放在同一個資料結構裡,這樣可以很方便的處理跟存取。就像一疊盤子一樣,他們都是性質相接近的東西,於是我們就把盤子碟子一疊,而屬於不同性質的東西就分放在其他地方,比如我們就不太應該把碗跟盤子放在同一疊裡面。在Perl裡面,你可以定義一個陣列,而陣列裡面存放的就是純量,當然存放的個數可以由零個到許多個,至於實際可以儲存的個數則依據每部電腦不同而有所差別,因為Perl依然依循它自己的個性,並不對程式設計師進行太多的限制,因此它可以允許你使用系統上所有的資源,換句話說,你可能會因為一個陣列過大而佔用系統的所有資源。
3.2 Perl 的陣列結構
我們先來看看怎麼在Perl裡面定義一個陣列。在Perl中,陣列變數是以@符號開頭,例如你可以定義一個變數名稱叫做@array。然後利用$array[0],$array[1]...的方式來存取陣列裡的元素。也就是說,你在定義了陣列@array之後,你可以指定陣列裡面的值,就像這樣的方式:
my @array;
$array[0] = 'first';
$array[1] = 'second';
$array[2] = 'third';
....
這樣比起剛剛我們一個一個變數慢慢的指定雖然方便了不少,至少我們可以很清楚的了解這些數值都是屬於同一個群組的,因為它們被放在同一個陣列中(註一)。不過這樣的寫法實在太辛苦了,尤其當你已經知道你陣列中的元素個數,以及他們個別的值,你就可以用簡單一點的方式來把陣列的值指定給你的陣列,就像這樣:
my ($array[0], $array[1], $array[2]) = qw/first second third..../;
其中,qw/first second third.../這一串東西就被稱為串列,例如:
my ($one, $two, $three) = (1, 2, 3);
也就是把一個串列一次指定給三個變數。利用qw也是同樣的方式,因此剛剛那一行程式其實也可以寫成:
my ($first, $second, $third) = qw/first second third/;
這樣的方式,就是我們把串列的值指定給變數,所以當然這些變數也可以是陣列的元素。不過既然我們確定要把串列的值指定給某個陣列,我們顯然可以更簡單的這麼作:
my @array = qw/first second third/;
這樣的方式就是直接利用串列賦值給陣列的方式,而類似的方式還可以寫成這樣:
my @array = (1...10);
my @array = (0, 1, 2, 4...8, 10);
my @array2 = (3, -1, @array, 13);
my @array2 = qw/3, -1, @array, 13/; # 這應該不是你想要的東西
當然,如果你定義了一個陣列,但是卻沒有賦值給他,那麼這個陣列就會是一個空陣列。相同的狀況,你也可以指定任意的陣列大小給Perl,當然前提是你的電腦有足夠的能力承受。這當然也是Perl的傳統之一。
Perl從來就不是一個嚴謹的程式語言,因此對於陣列的部份也採取同樣的規定。你不需要在程式的一開始就規定你的陣列長度,因此你可以在程式裡面隨時新增元素到你的陣列中。例如你的程式也許會寫的像這樣子:
my @array = qw/第零 第一 第二/;
$array[3] = '第三';
$array[4] = '第四';
沒錯,你可以使用串列形式來指定陣列的值,也可以直接把值指定給陣列的某個索引值,就像我們剛剛所使用的方式。另外,你也會發現,如果你這麼寫的話,Perl也不會阻止你:
$array[15] = '一下子就到 15 了';
那麼Perl會直接幫你的陣列程度擴充到15,也就是陣列的索引值會變成從0-14,而陣列大小變為16。至於陣列中間沒有被指定的值,Perl都會自動幫你設為undef,所以你的陣列中,有許多還沒定義的值。好吧,很多人也許對於這樣的設計不以為然,不過有時候這樣還是很方便的,不是嗎?想像你已經預測你的陣列會有20個元素,可是你現在只知道最後一個元素的值,你總不希望必須先把前面十九個元素值填滿之後才能開始使用你期待已久的那個元素值吧?
當然,對於那些認為應該嚴謹的定義程式語言語法,不能讓程式設計師為所欲為的人來說,Perl顯然不是他們會選擇的工具。而且這樣的戰爭已經持續了很長的一段時間,也不是我們可以在這裡解決的。讓我們暫且跳開風格爭議,繼續回來看Perl在陣列中的用法吧。
有時候我們需要知道陣列中的元素個數,比如我們希望在陣列中依序取出陣列中的元素並且進行運算,那麼我們就可以利用下面的方式來進行:
my @array = qw{first second third};
# 記得利用qq賦值給字串的作法嗎?用qw賦值給陣列也是類似
$array[4] = 'fifth'; # 我們跳過索引值3
print $#array; # 這裡取得的是最後一個索引值
print $array[3]; # 這裡應該不會有任何結果
既然$#array是陣列中最後一個索引值,所以我們可以利用($#array + 1)得到目前陣列中的元素個數(註二)。不過如果你打算利用這個索引值來確定目前陣列的長度,並且加入新的元素,就像這樣:
my @array = qw/first second third/;
$array[$#array+1] = 'forth'; # 把新的值放到現在最大索引值的下一個
當然,如果你這樣寫也是可以被接受的:
my @array = qw/first second third/; # 一開始,你還是有三個元素值
$array[$#array+1] = 'forth'; # 這時候的 $#array 其實是 2
$array[$#array+1] = 'fifth'; # 可是這時候 $#array 已經變成 3 了
print @array;
3.3 push/pop
沒錯,我是說那樣的寫法可以被接受,可是好像非常辛苦,尤其當你已經被一大堆程式搞到焦頭爛耳,卻還要隨時注意現在的陣列到底發展到多大,接下來你應該把最新的值放到那裡,這樣顯然非常辛苦。你一定也猜到了,Perl不會讓這種事情發生的。所以Perl提供了push這個指令把你想要新增的值「推」入陣列中,同樣的,你也可以利用pop從陣列中取出最後一個元素。不過為甚麼要使用push/pop這樣的指令,這當然和整個陣列的資料結構是具有相關性的,如果你弄清楚了陣列的形式也許就很容易理解了。我們可以把陣列的儲存看成是一疊盤子,因此如果你要放新的盤子,或者是拿盤子,都必須從最上面動作。這也就是為甚麼我們可以利用push/pop來對陣列新增,或是取出元素的最重要原因。我們可以從下面的例子看到 push跟pop的運作:
my @array = qw{first second third};
push @array, 'fourth';
print $#array; # 這裡印出來的是3,表示'fortuh'已經被放入陣列
pop @array;
print $#array; # 至於pop,則是把元素從陣列中取出
而且利用pop取出元素一律是從陣列的最後一個元素取出,也就是「後進先出 (last in, first out)」的原則。當然,pop的回傳值也就是被取出的陣列元素,以上面的例子來看,取出的就是'fourth'這個元素。
另外,在使用push時,也不限定只能放入一個元素,你可以放入一整個陣列。那麼就像這樣的寫法:
my @array = qw{first second third};
my @array2 = qw/fourth fifth/;
push @array, @array2;
print @array; # 現在你有五個元素了
3.4 shift/unshift
沒錯,push/pop確實非常方便,他讓我們完全不需要考慮目前陣列的大小,只需要把東西堆到陣列的最後面,或者把陣列裡的最後一個元素拿掉。不過我們也發現了,這樣的操作只能針對陣列的最後一個元素,實在有點小小的遺憾。其實我們想想,如果我把陣列中非結尾的某個元素去掉,那會發生甚麼事呢?比如我現在有一個陣列,他目前總共有三個元素,因此索引值就是0..2。如果我想要把索引值為1的那個元素取消,那麼索引值是不是也就需要作大幅更動。尤其當陣列的元素相當多的時候,其實也會有一些困擾。
不過Perl還是允許我們從「頭」對陣列進行運算,也就是利用shift/unshift的指令。如果我們已經知道push/pop的運作,那麼我們可以從範例中輕鬆的了解shift/unshift對陣列的影響:
my @array = (1...10);
shift @array; # 我把1拿掉了
unshift @array, 0; # 現在補上0
print @array; # 現在陣列的值變成了(0, 2...10)
現在你的陣列進行了大幅度的改變,我們應該來檢查一下,當我們在進行shift運算過程中,陣列元素的變化。
我們還是用剛剛的陣列來看看完整的陣列內容:
my @array = (1...10); # 我們還是使用這個陣列
shift @array; # 我把1拿掉了
print "$_\t$array[$_]" for (0...9); # 現在陣列的值變成了(0, 2...10)
好極了,我們看到了輸出的結果:
0 2
1 3
2 4
3 5
4 6
5 7
6 8
7 9
8 10
Use of uninitialized value in concatenation (.) or string at ch3.pl line 7.
9
沒錯,我們看到了錯誤訊息。因為我們的陣列個數少了一個,因此索引值9目前並不存在,Perl也警告了我們。所以我們發現了,Perl在進行shift的時候,會把索引也重新排列過。不過你能不能從中間插入一個值,並且改變陣列的索引排列,或是攔腰砍斷,取走某些元素,然後希望Perl完全不介意這件事呢?目前看來似乎沒有辦法可以這麼作的。不過有些方式可以讓你單讀取出陣列中某些連續性的元素,也就是使用切片的方式。
3.5 切片
就如我們之前提到,我們總是把一堆串列放入陣列中,雖然放入的方式不盡相同,但是至少我們可以在陣列中找出0個以上的元素所組成的陣列。沒錯,如果我們知道一個陣列中的元素,而且我希望取出這個陣列中的某些連續性元素是不是可行呢?例如有一個陣列的元素是(2003...2008),那麼如果我希望取得的是這個陣列中2004-2006這三個元素,並且把這三個元素拿來進行其他運算或運用,我是不是應該這樣寫:
my @year = (2003...2008);
my ($range[0], $range[1], $range[2]) = ($year[1], $year[2], $year[3]);
其實如果你真的這麼寫了,也不會有人說你的程式有錯誤,雖然這樣的寫法總是很容易讓人產生錯誤。即使不是語法上的錯誤,也容易因為打字的原因而產生可能的邏輯錯誤。既然如此,我們顯然應該找出容易的方法來作這件事。我們用一個很容易看清楚的例子來說明吧:
my @array = (0...10);
my @array2 = @array[2...4];
print @array2; # 沒錯,你拿到了(2, 3, 4) 三個元素
這個方法,我們就稱為切片,就像我們把生魚片取出其中的一片。可是如果我要的範圍並不屬於連續性的話,還能切片嗎?其實就像你一個一個取出陣列中的元素,只是有些部份是連續的,你不希望把每個元素都打一次。所以如果你希望多切幾片,可以考慮這麼作:
my @array = (0...10);
my @array2 = @array[2...4, 6];
這時候,你拿到的不但是(2, 3, 4)三個元素,也包含了6這一個元素。這樣是不是非常方便呢?
3.6 陣列還是純量?
如果你已經開始自己試著寫一些Perl程式,不知道你有沒有遇到這個問題,你有一個陣列@array,你想新增一個陣列,元素跟原來的陣列@array相同,於是你想寫了這樣一個式子:
my @array2 = @array;
沒想到一時手誤,把這個式子打成這樣︰
my $array2 = @array;
這時候,Perl卻沒有傳回錯誤給你,可是程式會傳回什麼結果呢?我們可以來實驗看看,只要打這幾行:
my @array = (0...10);
my $array2 = @array;
print $array2; # 程式傳回 11
這個值恰好就是陣列@array的元素個數,所以我們似乎發現好方法來找到陣列的元數個數了。不過也許應該來研究一下,為什麼Perl對於資料型態能夠進行這樣的處理。這其實是非常重要的一個部份,也就是語境的轉換。這很像我們在之前曾經遇過的例子,當我有兩個變數,分別是:
my $a = 4;
my $b = 6;
可是當我使用 $a.$b 跟 $a+$b 兩個不同的運算子時,Perl也會自動去決定這時候該把兩個變數使用字串,或是變數進行處理。因為語境的不同,讓運算的方式也有所不同,這在Perl當中是非常重要的觀念。不過這個觀念絕非由Perl所獨創,相反的,這樣的用法在現實生活中是屢見不鮮。比如有人問你平常用甚麼寫程式,你也會依照當時聊天的情況回答你是用甚麼編輯器,或者是用甚麼程式語言。因此在語言的使用中,如何選對適當的語境確實相當重要,而既然Larry Wall就是研究語言的專家,把這種方法運用在Perl裡面也是再自然不過了。
我們再來看看剛剛的例子,我們指定一個陣列,並且指定這個陣列的元素包括一個從0到10的串列,而當我們把這個陣列賦值給一個純量變數時,Perl便會把串列元素個數指定為這個純量變數的值。這也就表示Perl正以純量變數的語境在處理你的運算,而對一個陣列以純量變數的語境進行運算時,Perl就如我們所看到的,以陣列中串列元素的個數表示。所以你可以寫出這樣的運算式:
my @array = (1...10); # 利用串列賦值給陣列
my $scalar = @array + 4; # 在純量語境中進行
my @scalar_array = @array + 4;
# 先以純量語境進行運算,然後以串列方式賦值給陣列
這樣看起來會不會有一點眼花繚亂?程式第一行的中,就像我們所熟知的狀況,我們把一個串列賦值給陣列。接下來,我們利用純量語境把陣列內串列元素的個數取出,並進行運算,然後把結果放到一個純量變數裡,這裡全部都是以純量變數的方式在進行。第三行就比較複雜一點了,我們先用純量語境,取出陣列的串列元素個數,以純量方式進行運算,接下來把這個得到的結果以串列的方式指定給陣列@scalar_array。所以最後一行其實也像是這樣:
my @array = (1...10);
my $scale = @array + 4; # 這裡是純量語境
my @scalar_array = ($scale); # 把得到的結果放進串列中,並且賦值給陣列 @scalar_array
其實就像這裡所看到的,如果你的需求是一個串列,而你卻只能得到一個純量,那麼Perl就會給你一個只有一個元素的串列。其實要訣就是仔細看看你希望得到甚麼樣的東西,而Perl可以給你甚麼東西。而有時候,當理想與現實有些落差的時候,也許就會有些undef產生。假如我們把剛剛的例子改成這樣:
my @array = (0...10);
my ($scalar1, $scalar2) = (@array + 4);
當我們要求的串列無法獲得滿足時,Perl就會幫忙補上undef。
3.7 一些常用的陣列運算
既然我們總是喜歡把性質類似的變數放在一起變成陣列,那麼很多時候我們就會希望對這一整個陣列進行某些運算。例如排序,過濾,一起帶入某個公式中進行運算等等。這時候我們經常利用迴圈來幫我們處理這一類的事情,不過有些常用的運算,Perl已經幫我們設想好了,我們只需要輕鬆的一個式子就可以進行一些繁複的工作。
3.7.1 sort
排序總是非常必要的,我們在舉陣列的時候有提到,如果我們要把某個班級學生的數學成績放入陣列,那麼我們也許會希望利用這些成績來排序。這時候,sort就非常有用了。我們可以這樣作:
my @array = qw/45 33 75 21 38 69 46/;
@array = sort { $a <=> $b } @array;
這樣Perl就會幫我們把陣列重新排列成為
21 33 38 45 46 69 75
其實,如果你這樣寫也是有相同的效果:
@array = sort @array;
當然,如果你需要比較複雜的排序方式,就要把包含排序的區塊加入,所以你也可以寫成:
@array = sort { $b <=> $a } @array;
其中$a跟$b是Perl的預設變數,在排序時被拿來作為兩兩取出的兩個數字。而<=>則是表示數字的比較,如果陣列中的元素是字串,則必須以cmp來進行排序。
我們可以用接下來的例子來說明怎麼樣進行更複雜的排序工作。
my @array = qw/-4 45 -33 8 75 21 -15 38 -69 46/;
@array = sort { ($a**2) <=> ($b**2) } @array; # 這次我們以平方進行排序
所以得到的結果會是:
-4 8 -15 21 -33 38 45 46 -69 75
3.7.2 join
有時候,你也許會希望把串列裡面的元素值用某種方式連接成一個字串。比如也許你想要把串列中的元素全部以','來隔開,然後連接成一個字串,那麼join就可以幫上忙了。你可以在串列中這麼用:
print join ',', qw/-4 45 -33 8 75 21 -15 38 -69 46/;
這一行顯然也可以寫成:
my @array = qw/-4 45 -33 8 75 21 -15 38 -69 46/;
print join ',', @array;
和join函數相對應的的則是split,他可以幫忙你把一個字串進行分隔,並且放進陣列中。
3.7.3 map
很多人會使用Excel的公式,而公式的作用就是針對某一行/列進行統一的運算。比如小時候在學校考試的時候,老師常常會因為全班成績普遍太差,而進行所謂「開平方乘以十」的計算。這時候,如果可以用map就顯得很方便了。
my @array = map { sqrt($_)*10 } qw/45 33 8 75 21 15 38 69 46/;
我們可以看到,串列裡面是學生的成績,所謂map就是把陣串列裡的元素一一提出,並進行運算,然後得到另外一個串列,我們就把所得到的串列放到陣列中。於是就可以得到這樣的一個陣列:
67.0820393249937
57.4456264653803
28.2842712474619
86.6025403784439
45.8257569495584
38.7298334620742
61.6441400296898
83.0662386291807
67.8232998312527
當然,map還有許多有趣的使用範例,而且如果能適時運用,確實能大幅降低你寫程式的時間,也可以讓你的程式更加乾淨俐落。
3.7.4 grep
我們既然可以針對串列中的每一個元素進行運算,並且傳回另一個串列,那麼是否可以針對串列進行篩選呢?例如我希望選出串列中大於零的元素,或者以字母開始的字串元素,那麼我可以怎麼作呢?這時候,grep就會是我們的好幫手。如果各位是Unix系統的使用者,應該大多用過系統的grep指令,而Perl的grep函數雖然不盡相同,不過精神卻是相近的。我們可以利用grep把串列中符合我們需求的元素保留下來。就像這樣:
my @array = qw/6 -4 8 12 -22 19 -8 42/; # 指定一個串列給陣列 @array
my @positive = grep {$_ > 0} @array; # 把@array裡大於零的數字取出
print "$_\n" for @positive; # 印出新的陣列 @positive
而且答案就正如我們所想像的,Perl能夠正確的找出這個陣列中大於零的數字。
也許你會有一些不錯的想法,如果我們想要把剛剛的陣列中所找出大於零的數字取得平方值之後印出,那麼我們應該怎麼做比較容易呢?當然,一般的情況下,我們就會想到迴圈,而這也正是我們接下來要說的部份。
習題:
1. 試著把串列 (24, 33, 65, 42, 58, 24, 87) 放入陣列中,並讓使用者輸入索引值 (0...6),然後印出陣列中相對應的值。
2. 把剛剛的陣列進行排序,並且印出排序後的結果。
3. 取出陣列中大於40的所有值。
4. 將所有陣列中的值除以 10 後印出。
註一:當然,你也可以把程式中的所有純量變數全部放在一個陣列中,不過很快的,你會發現連你自己都不想再看到這支程式了。
註二:別忘了,Perl的索引值是由零開始。