« 3. 串列與陣列 | Perl 學習手札目录 | 5. 雜湊(Hash) »

4. 基本的控制結構

4. 基本的控制結構
4.1 概念
大部份的時候,程式總不會跟著你寫程式的順序,一行一行乖乖的往下走。尤其是當你的程式由平鋪直敘漸漸變成有些起伏,這時候,怎麼確定你的程式到底應該往那裡走,或者他們現在到底到了那裡。如果你無法掌握程式的流程,只怕他們很快就會離你而去。你從此再也無法想像你的程式會怎麼運作,當然也很有可能你就寫出了會產生無窮迴圈的程式了。
4.1.1 關於程式的流程
在程式的進行當中,你經常會因為過程發生了不同的事件,因為結果的不同而必須進行不同的運算,這個時候,你就必須進行程式中的流程控制。或者,你會需要對某些工作進行重複性的運算,這時候,重複性的流程控制就可以大大的幫助你減輕工作負擔。因此,你常常會發現,流程的控制在你的程式之中確實是非常重要而且經常被使用的。雖然Perl的流程控制跟其他程式語言並沒有太大的差異,不過我們還是假設大家並沒有這方面的基礎。所以還是從頭來看看最基本的流程控制應該怎麼作呢!
4.1.2 真,偽的判斷
流程的基本控制主要在於判斷某個敘述句是否成立,並藉以判斷在不同情況下該怎麼進行程式的流程。比如我們可以用簡單的例子來認識一下流程控制的進行:


my $num = <STDIN>; chomp($num); if ($num<5) { print "small"; } else { print "big";

這樣看起來是不是非常簡單呢?
不過Perl比較特殊的部份在於他並不存在一種獨立的布林資料型態,而是有他獨特對於真,偽值的判定方式。所以我們應該先要知道,在Perl中,那些值屬於真,那些值屬於偽,這樣一來,我們才能知道判斷句是否成立。


* 0 屬於偽值 * 空字串屬於偽值 * 如果一個字串的內容是"0",也會被視為偽值。 * 一個undef的值也屬於偽值。

當然,有些運算式也是透過這些方式來判斷,我們可以輕易的找到例子來觀察Perl的處理方式。例如你可以看看Perl對這樣的判斷式怎麼處理:


my $true = (1 < 2); print $true;

沒錯,回傳值是1,表示這是個真值,因此如果你在流程控制中用這樣的判斷式,很清楚可以知道流程的方向。

4.1.3 區塊
在開始進入正式的判斷式之前,我們應該先來說說Perl程式中的區塊。在Perl中,你可以用一對大括號{}來區分出一個Perl區塊,這樣的方式在程式的流程控制中其實非常常見。
在Perl的語法中,區塊中的最後一個敘述不必然要加上分號的,比如你可以這麼寫:


my $num = 3; { my $max = $num; print $num }

不過如果你未必覺得自己足夠細心的話,也許你該考慮留下這個分號,因為一但你的程式略有修改,你也許會忘了加上該有的分號。那你花在加這些分號的時間可能會讓你覺得應該隨時記得替你的敘述句加上分號才是。另外,區塊本身是不需要以分號作為結束的,不過你可別把區塊所使用的大括號跟雜湊所使用的混在一起。

4.1.4 變數的生命週期
既然提到區塊,我們似乎應該在這裡稍微提起Perl裡面關於變數的生命週期。一般來說,Perl的生命週期都是以區塊來作為區別的。這和有些程式語言的定義方式似乎有些差距,當然,Perl的區分方式應該是屬於比較簡單的一種,所以一般而言,你只需要找到相對應的位置,就很容易可以知道某個變數現在是否還存在他的生命周期中。我們可以看個範例:


my $num = 3; { my $max = $num; print $max; } print $max;

在程式裡,我們在區塊中宣告了變數$max的存在,並且把變數$num的值給了他。就在這一切的運算結束之後,我們進行了兩次的列印動作。而兩次列印分別在大括號的結束符號前後,表示一個列印是在區塊中進行,另一個則是在區塊結束後才列印。不過當我們試著執行這支程式時,發生了一個錯誤:


Global symbol "$max" requires explicit package name at ch2.pl line 12. Execution of ch2.pl aborted due to compilation errors.

沒錯,我們在區塊內定義了變數$max,也因此,變數$max的生命週期也就僅止於區塊內,一但區塊結束之後,變數$max也就隨之消失了。另外,我們也可以看看這個類似的例子:


my $num = 3; { my $max = $num; print "$max\n"; } { my $max = $num*3; print "$max\n"; }

這個例子中,我們看到了變數$max被定義了兩次,可是這兩次卻因為分屬於不同的區塊,因此Perl會把他們視為是完全獨立的個體。也不會警告我們有個叫做$max的變數被重複定義了。這看起來非常簡單吧?!這讓你可以在你需要的區塊裡,定義屬於那個區塊自己的同名變數,可是有時候其實你會把自己搞的頭暈,不信的話,你可以看看接下來的寫法:


my $a = 3; my $b = 9; { print "$a\n"; # 屬於外層的區塊,所以你會看到 3 my $b = 6; # 定義了這區塊內自己的變數 print "$b\n"; # 於是你看到的這個$b的值其實是6 } { print "$a\n"; # 這個區塊沒有自己的$a print "$b\n"; # 也沒有自己的$b    # 所以你在這裡看到的值其實是上一層的變數值 } print "$a\n"; # 這裡似乎毫無疑問 print "$b\n";   # Perl還是印出期待中的3跟9

4.2 簡單判斷
好極了,現在我們已經知道甚麼是真值,甚麼是偽值。這樣就可以運用在程式的流程判斷了。
4.2.1 if
if的判斷非常的直覺,也就是說,只要判斷式傳回真值,程式就會執行條件狀況下的內容。這是一個非常簡單的例子:


my $num = 3; if ($num < 5) { print "這是真的"; }

沒錯,這個程式雖然簡單,但卻很清楚的表達出if判斷式的精神。在($num < 5)裡,Perl傳回一個真值,於是我們就可以執行接下來的區塊,也就是列印出字串"這是真的"。


提示: 由於這些判斷式會用到大量的二元運算符,為了避免執行上產生難以除錯的問題,我們在這裡提醒各位一些容 易忽略的部份。 "<",">",">=","<=","==","!=":這些算符都是在針對數字時用到的比較算符。 "eq","lt","gt","le","ge","ne":如果你是對字串進行比對,請記得使用這些比較算符。

4.2.2 unless
和if相對應的,就是unless了。其實在其他程式語言,很少使用unless的方式來進行判斷。因為我們可以使用if的否定來進行同樣的工作例如你可以用
if (!($a < 3))
這樣的方式來描述一個否定的判斷句。可是利用否定的運算符"!"來進行判斷顯然不夠直覺,也因此比較容易出錯。這個時候unless就顯得方便多了。從口語來看,if敘述就是我們所說的「假如...就...」,而unless就變成了「除非...就...」。這樣在運算式看起來,就顯得清楚,也清爽多了。所以你可以寫成:
unless ($a <3)
這樣的寫法跟上面的那個例子是一樣的效果,不過在易讀性上明顯好了許多。尤其當你的判斷式稍微複雜一些,你就更可以感受到unless的好用之處了。
4.2.3 一行的判斷
在許多時候,我們會用非常簡單的判斷來決定程式的走向。這時候,我們便希望能以最簡單的方式來處理這個敘述句。尤其當我們進行了判斷之後,只需要根據判斷的結果來執行一行敘述時,使用區塊的方式就顯得有點冗長了,比方你有一個像這樣的需求:


if ($num < 5) { $num++; }

這樣的寫法確實非常工整,可是對於惜字如金的Perl程式員來說,這樣的寫法似乎非常不經濟。於是一種簡單的模式被大量使用:


$num++ if ($num < 5);

你沒看錯,確實就是如此,把判斷句跟後續的運算句合併為一個運算式。而且這種用法不僅止於if/unless判斷式,而是被大量使用在許多Perl的運算式中。我們以後還會有很多機會遇到。不過先讓我們繼續往下看。
4.3.4 else/elsif
你總是有很多機會使用到if/unless判斷式,而且常常必須搭配著其他的判斷才能完整的讓你的程式知道他該做甚麼事。這個時候,比如你也許會想這麼寫:


if ($num == 1) { ... } if ($num != 1) { ... }

這樣的程式雖然也對,不過總覺得那裡不太對勁,畢竟這兩個判斷式顯然正在對同一件事進行判斷,不過卻必須分好幾個敘述句。當然,如果你的判斷還是簡單(像我們的例子所寫的)也就還能手工進行。可是如果你的判斷式長的像這樣呢?


if ($num == 1) { ... } if ($num == 2) { ... } if ($num == 3) { ... } ...... if (($num != 1) && ($num != 2) && ($num != 3) && ...) { ... }

沒錯,你現在可以想像人工進行這件事情的複雜度了吧!所以如果有簡單的方式來進行,同時還能增加程式的易讀性,似乎是非常好的主意,而else/elsif就是這個問題的解答。
如果我們在一個,或一大堆if判斷式的最後希望能有一個總結,表示除了這些條件之外,其他所有狀況下,我們都要用某個方式來處理,那麼else就是非常好的助手。最簡單的形式大概就會像這樣:


if ($num == 1) { ... } else { # 其實這裡就是 if ($num != 1) 的意思了 ... }

不過如果我們有超過一個判斷式的時候,就像之前的例子,我希望$num在1..3的時候,能有不同的處理方式,甚至我如果進行一個禮拜七天的工作,我希望每天都能有不同的狀況,那只有else顯然不夠。我總是不希望每次都來個if,到然後還要判斷使用者打錯的情況。這時候,elsif就派上用場了,你每次在其他條件下,如果還要訂下其他的條件,那麼你就可以寫成像這樣:


if ($date eq '星期一') { .... } elsif ($date eq '星期二') { .... } elsif ($date eq '星期三') { .... .... } else { print "你怎麼會有$date\n"; }

其實,利用if/else/elsif已經可以處理相當多的問題,可是在許多程式語言中還可以利用switch/case來進行類似的工作。在Perl中,也有類似的方式,這是由Damian Conway寫的一個模組,目前已經放進Perl的預設套件裡了。不過我們並不打算在這裡增加所有人的負擔。

4.3 重複執行
我們剛剛所提到的只是對於某個條件進行判斷,並藉由判斷的結果來決定程式的流程,因此條件的不同會讓程式往不同的地方繼續前進。不過很多時候我們需要在某些條件成立的時候進行某些重複的運算,比如我們希望算出10!,也就是10的階乘。這就表示只要我們指定的數字不超過10,就讓這個運算持續進行,這時候,我們顯然需要進行重複的運算。
4.3.1 while
while就是一個很好的例子,讓我們來看看怎麼利用while來完成階乘的例子:


my $num = 1; my $result = 1; # 小心,這裡一定要指定$result為1 while ($num <= 10) { # 確定你是否超過範圍 $result*=$num; $num = $num + 1; }

看起來不難吧!你只要掌握幾個原則,理論上就可以很容易讓while迴圈輕鬆上手。
首先,你總得讓你的迴圈有正常運作的機會。當然你如果不希望這個迴圈有任何機會執行,Perl也不會在你的耳邊大叫,不過維護你的程式的人大概會很難理解這一段不可能執行到的程式碼有甚麼功用吧。
其次,別忘了讓你的程式有機會離開他的迴圈,除非你知道自己在作甚麼否則你的程式會不斷的持續進行,當然,那就是所謂的無限迴圈。例如你寫了一個非常簡單的程式:


while (1) { # 在這裡,程式會得到永遠的真值 print "這是無限迴圈"; }

第三,在這裡,你還是可以讓只有單一敘述的while迴圈利用倒裝句達成:
我們假設你完全知道剛剛的程式會發生甚麼事,而那正是你所希望達成的,那麼我們就可以來改寫一下,讓他變得更簡潔:


print "這是無限迴圈" while (1);

別怪我們太囉唆,不過這樣的寫法確實讓程式乾淨許多,而且許多Perl程式設計師(也包括我自己在內)非常喜歡這樣的用法,如果你有機會讀到別人的程式,還是先在這裡熟悉一下吧。

4.3.2 until
類似if/unless的相對性,你也可以用until來取代while的反面意義,例如你可以用until來作剛剛階乘的同樣程式。語法其實跟while一樣:


until (判斷式) { .... }

雖然語法看起來完全一樣,不過如果是剛剛的階乘,判斷式就會變成這樣:


my $num = 1; my $result = 1; until ($num > 10) { $result*=$num; $num = $num + 1; }

4.4 for
for迴圈也是非常有用的迴圈,尤其在你使用陣列時,你可以很方便的取出所有陣列中的元素。而且你幾乎不需要知道現在陣列中有多少元素,聽起來非常神奇不是嗎?不過先讓我們來看看for到底是怎麼用的呢?
4.4.1 像 C 的寫法
如果你寫過C語言,你應該對這樣的寫法非常熟悉,所以你應該可以直接跳過這一小段。當然,我們假設大部份的人都不熟C,那麼我以為,如果你覺得太累,也可以晚一點再回來看這一段。因為作者個人的偏見,以為這雖然是Perl的基本語法,可是在實際程式寫作時用的機會卻比其他方式少了一些。不過我想大家都是好學生,還是讓我們來看看基本的for迴圈應該怎麼寫呢?還是維持我們的傳統,來看看這個例子吧:


my $result = 1; for (my $num = 1; $num <= 10; $num = $num + 1) { $result *= $num; }

跟剛剛的while/until終於有些不太一樣,而且主要的差別似乎在於這一行:
for (my $num = 1; $num <= 10; $num = $num + 1)
沒錯,這一行確實就是for迴圈的奧秘所在。首先我們看到小括號裡面的三個敘述,這三個分別代表迴圈的初值,迴圈的條件,以及每次迴圈進行後所作的改變。從這個例子來看,我們先定義了一個變數$num,初始值是1。接下來要求迴圈執行,只要$num在不超過10的狀況下就不斷的執行,最後一個則表示,當迴圈每做一次,$num的值就被加1。
當然,這三個部份都是獨立的,所以如果你有比較特殊的判斷方式,也可以隨意修改。例如你可以用總和超過/不超過多少來作為判斷的依據。或者每次在執行完一次迴圈就把$num的值加3,這樣的寫法對於應付比較複雜的狀況顯然非常好用。
不過如果我們大部份的時候都只是非常有規律的遞增,或遞減,並且以此為判斷迴圈是否應該結束的依據。既然如此,也許我們還有更清楚,而且簡單的方式嗎?
4.4.2 其實可以用 ...
大家對於Perl程式設計師喜歡簡單的體會應該已經非常深刻了,因此我們就來看看怎麼樣可以讓非常具有規則性的for迴圈可以用更簡單的方式來表達吧!例如像我們前面提到的例子:
for (my $num = 1; $num <= 10; $num = $num + 1)
這麼簡單的for迴圈還要寫這麼長一串真是太累人了,記得有一種非常簡單的語法嗎?
for $num = 1 to 10
沒錯,如果Perl也可以這樣寫那就非常口語化了。而且再也不用那麼長的敘述句只為了告訴Perl:「給我一個1到10的迴圈吧!」不過很顯然,Perl的程式設計師找到了更簡便的方法,你只要用...就可以表示你需要的迴圈範圍了。


for my $num (1...10) { # 這就是表示$num從1到10 print $num; } 這其實可以寫成: for (1...10) { # $_ 經常被拿來作為迴圈的預設變數 print $_; } 更簡化的寫法: print for (1...10);

覺得很神奇嗎?其實一點也不會,因為這正是Perl程式設計師經常在使用的方式。而你需要更熟悉的也許就是要習慣於這些人對於習慣寫作的方式。

4.4.3 有趣的遞增/遞減算符
如果你寫過C語言程式,或是你看過其他人寫Perl的迴圈,應該會常常看到這樣的敘述句:

print for ($i = 1; $i <= 10; $i++);   # 印出 1...10
可是對於遞增(++)與遞減(--)運算子卻又是似懂非懂。那麼這兩個運算子到底在說些甚麼呢?不過顧名思義,他們主要的工作就是對數字變數進行遞增或遞減的運算。例如你也許會這麼用:


my $i = 1; while ($i <= 10) { print $i; $i++;   # 把 $i 加上 1 }

沒錯,於是在 while 迴圈中,$i就會由1到10,靠的正是這個遞增運算子。當然,在這裡你也可以把這個式子替換為:

$i = $i + 1; # 或是 $i += 1

不過看起來總是沒有$i++簡潔吧!同樣的,遞減運算子(--)也是進行類似的工作,也就是每次把你的數值減1。不過遞增或遞減運算子總有時候會讓人感覺困擾,讓我們來看看以下的例子:


my $i = 1; while ($i <= 10) { print ++$i."\n";   # 印出 2...11 } my $j = 1; while ($j <= 10) { print $j++."\n";   # 印出 1...10 }

從這個例子來看,應該比較清楚,在第一個迴圈中,Perl會先幫$i加1之後印出,也就是根據遞增(遞減)運算子的位置來決定如何運算。當然,我們就可以確定在第二個迴圈中,遞增運算子是怎麼運作的。當遞增運算子在前時,Perl會先對運算元進行運算,就像第一個迴圈的狀況,反之,則是在進行完原來的運算式之後,才進行遞增(或遞減)的運算,也就是像第二個迴圈中所看到的結果。

4.4.4 對於陣列內的元素
我們當然可以用 for (;;) 這樣的方式來取出陣列中的所有元素來運算。不過這時候你只是依照陣列的索引順序,因此你還需要根據索引的值來取得陣列中的元素值。就像這樣子:


my @array = qw/1 2 3 4 5/; for (my $i = 0; $i < $#array; $i++) { print $array[$i]; }

顯然這樣的寫法太過繁瑣,我們其實可以利用foreach來進行更簡單的取值動作。那麼剛剛的迴圈部份可以寫成:


print foreach (@array);

不要懷疑,就是這麼簡單,不過讓我們先來解釋一下整個迴圈的運作。
當你對一個陣列使用foreach迴圈時,Perl就會自動取出這個迴圈每一個值。接下來,你可以指定Perl把取得的值放到某個變數中,例如你可以寫成:


foreach my $element (@array)

不過這時候我們就用到了Perl最常用到的預設變數$_,當我們在迴圈中沒有指定任何變數時,Perl就會把取出來的值放入預設變數$_中。緊接著我們希望把迴圈取得的值列印出來,也就是執行
print $_;
這樣的式子,當然,這樣的式子在Perl出現的機率真是太少,因為大部份的時候,如果你只要列印單一的$_,Perl程式員也就會省略$_這個變數。而因為我們在迴圈中只打算執行print這個指令,所以倒裝句也就順勢產生。看起來應該顯得非常簡潔吧!現在你應該還不習慣這樣的寫法,不過如果你有機會接觸其他Perl程式員寫的程式,那千萬要慢慢接受這樣的寫法。

那麼還有一個問題,那就是foreach是否只能用在迴圈的取值,或是foreach跟for該怎麼區別他們的用法呢?
這個問題倒很容易,因為在Perl之中,foreach跟for所進行的工作基本上是一模一樣,或說他們之中的任何一個都只是另一個的別名。因此只要你可以使用for的地方,即使你將他替換為froeach也完全可以被Perl接受,不過有時候也許你會希望以foreach來表達你的算式能夠讓可能維護你的程式的人比較容易接受,當然,很多時候Perl程式員確實不願意多打四個字母,所以你被需按照當時的語境確定實際執行的是for或foreach的語意了。


還記得我們在上一章提出的問題嗎?如果我們有一個含有整數串列的陣列,而我們想取出其中大於零的整數然後取他們的平方值,那麼我們應該怎麼作呢?現在我們可以嘗試來玩玩這個問題。
我們先用迴圈的方式來解決這個問題:


my @array = qw/6 -4 8 12 -22 19 -8 42/; my @positive; for (@array) {   # 針對陣列的每個元素檢察 push @positive, ($_**2) if ($_ > 0);  # 如果大於零就取平方值 } print for (@positive);

那如果使用我們在上一章介紹的函數呢?


my @array = qw/6 -4 8 12 -22 19 -8 42/; my @positive = map { $_**2 } grep { $_ > 0 } @array;   # 倒裝,把@array的元素先放進 grep 檢查,再把通過檢查的結果利用map取得平方值放進新的陣列 print for (@positive);

相當有趣吧,這也就是Perl的名言:「解決事情的方法不只一種。」

習題:
1. 算出1+3+5+...+99的值
2. 如果我們從1加到n,那麼在累加結果不超過100,n的最大值應該是多少?
3. 讓使用者輸入一個數字,如果輸入的數字小於50,則算出他的階乘,否則就印出數字太大的警告。