9. 再談控制結構
程式中的控制結構是用來控制程式進行方向的重要依據,所以有足夠靈活的程式控制結構能節省程式設計師的大量時間。可是也必須謹慎的使用,否則如果一個程式裡面到處充滿了迴圈控制,然後中斷,轉向,反而讓程式變得非常沒有結構,在程式的結構遭到破壞之後,一旦程式出了問題而需要要開始追蹤整個程式的行進就變成要花大量的時間。很顯然的,如果沒有保持程式良好的結構性,對於日後的維護將會是一大負擔。
不過如果能夠小心使用,這些控制程式流程的工具會是程式設計師重要的工具。所以我們就來看看除了之前提過的for,while,until,if等等各式各樣的流程控制之外,我們還能有什麼其他的方式可以方便的操控Perl吧。
9.1 迴圈操作
既然是流程控制,我們有時候會因為程式的狀況而希望離開迴圈或某些條件敘述的判斷區塊,也可能需要省略迴圈中的某次運算,或進行其他的跳躍。聽起來這些方式好像很容易讓人搞的眼花繚亂,現在我們就要來看看這些功能到底有什麼幫助。
9.1.1 last
顧名思義,這也就是最後的意思,因此Perl看到這個關鍵字就會相當高興,表示他距離休息又靠近了一步。不過這樣可以提前結束迴圈的函數到底扮演著甚麼樣的角色呢?讓我們來試試:
for (1...10) {
last if ($_ == 8);
print; # 這樣會印出 1...7
}
好玩吧,當你在迴圈內加上了另外一個判斷式,並且允許迴圈在某個條件下跳出迴圈的執行,這時候,Perl就提早下班,回家休息了。當然,last的使用是有限制的,也就是他只允許在可以執行結束的區塊內,其中最常被應用的是在迴圈內,而這裡的迴圈指的是for,foreach,while,until,另外也可以在單獨的區塊中使用,就像這樣:
print "start\n";
{
print "last 前執行\n"; # 會順利印出
last;
print "這裡就不執行了\n"; # 所以這一行永遠不會被執行
}
print "然後就結束了\n";
沒錯,這個例子的程式確實非常無趣,因為我們寫了一行永遠不會執行的程式,不過卻讓我們透過這個小程式清楚的看到last的執行過程。所以我們可以輕易的讓Perl跳出某個區塊,而既然可以提前結束區塊的執行範圍,我們有時候也需要Perl可以重複執行迴圈內的某些條件這時候redo這個函式就派上用場了。
9.1.2 redo
雖然我們可以用last讓Perl提早下班,同樣的,我們也可以要求Perl加班,也就是利用redo這個函式讓Perl重新執行迴圈中的某些條件。例如我們在迴圈中可以利用另一個判斷敘述來決定目前的狀況,並且利用這個額外的敘述判斷來決定是否要讓迴圈中的某個條件重複執行。例如我們有一個迴圈,我們可以很容易的要求perl在迴圈中的某個階段重複執行一次,就像下面的例子:
for (1...10) {
$_++; # redo 其實會來這裡
redo if ($_ == 8); # 我們希望 redo 的條件
print; # 會印出 2, 3, 4, 5, 6, 7, 9, 9, 10, 11
}
我們可以研究一下redo的過程,在上面那個程式的第三行,我們要求Perl在迴圈的變數等於8的時候就執行redo。所以當我們在迴圈內的條件符合redo的要求時,Perl就會跳到迴圈的第一行,也就是說當迴圈的值進行到7的時候,經過$_++的運算就會讓$_變成8,這時候就符合了redo的條件,因此還來不及印出來變數$_,Perl就被要求回到迴圈中的第一行,於是$_變成了9,這就是第一次9的出現。接下來迴圈恢復正常,就接連印出9...11。也就是我們看到的結果了。
不過這裡要注意的是我們迴圈的使用方式,我們使用了for(1...10),而不是使用for ($_ = 1; $_ <= 10); $_++)這樣的敘述,而這兩者有著相當的差異。如果各位使用了後者的迴圈表示,結果就會有所不同。
我們還是實際上來看看使用for (;;)來檢查redo的效果時,也可以藉此看看兩者的差異了:
for ($_ = 1; $_ <= 10; $_++) {
$_++; # 我們還是先把得到的元素進行累加
redo if ($_ == 8); # 遇到8的時候就重複一次
print $_; # 印出目前的 $_,我們得到2, 4, 6, 9, 11
}
很有趣吧,我們來看看這兩者有什麼不同,首先我們看到第一個例子中,Perl是拿出串列1...10的元素,並且把得到的元素放進變數$_中。接下來就像我們在迴圈中所看到的樣子了,所以迴圈並不是以$_作為計數的依據。這樣的方式就像這樣的寫法:
for (my @array = (1...10))
可是當我們看到第二個例子的時候,我們卻是指定了$_作為迴圈的計數標準。所以我們在迴圈中對$_進行累加,就完全影響到迴圈的執行。因此我們一開始拿到$_等於1,可是一進迴圈就馬上又被累加了一次,我們就印出了2,接著Perl又執行迴圈的遞增,讓我們取得3,我們自己又累加了一次,也就印出4,等我們累加到8的時候,迴圈被要求執行redo,因此我們又累加一次,$_變成9,緊接著最後一次的迴圈,經過累加之後,我們印出了11。看起來好像非常複雜,不過你只要實際跟著迴圈跑一次應該就可以看出其中的變化了。
不過在使用redo的時候必須非常小心,因為你很可能因為設定了redo的條件而產生無窮迴圈。就像剛剛的例子,如果我們改寫成:
for (1...10) {
redo if ($_ == 8);
print $_;
}
在這個迴圈裡,我們希望迴圈的控制變數在8的時候可以進行redo,於是它就一直卡在8而跳不出來了。就像我們說的,這裡變成了無窮迴圈,你的程式也就有加不完的班了。
9.1.3 next
我們剛剛使用了last來結束某個區塊,也透過redo來重複執行迴圈中的某個條件敘述,那麼既然可以在迴圈內重複執行某個條件的敘述,那麼略過某個條件下的敘述也應該不是太難的問題。是的,其實只要利用next,那麼我們可以在某些情況下直接結束這次的執行,也就是省略迴圈中某一些狀況的執行。當然,描述還是不如直接看看實例,我們還是利用簡單的例子來了解next的作用:
for (1...10) {
next if ($_%2); # 以串列值除以2的餘數判斷
print $_;
}
這個例子裡面,我們會印出1...10之間的所有偶數。首先,這是一個從1到10的迴圈,主要的工作在於印出目前迴圈進行到的值,不過就在列印之前,我們使用了一個next函式,而決定是否執行next的判斷是以串列值除以2的餘數來作為條件
。如果餘數為真(在這裡的解釋就是:如果餘數為1),就直接結束這次的執行,當然,如果餘數為0(在這個程式中,我們可以解釋為「遇到偶數時」),就會印出串列的值,所以程式會印從1到10的所有偶數值。
雖然有些時候,你會發現next的好用之處,可是如果你會因為next而造成追蹤程式的困擾時,那就可能要修改一下你的使用方式了。例如改變迴圈的判斷條件或是索引的遞增方式等等,就像上面的例子,我們也許可以改用while來判斷,或者使用for(;;),而不是使用foreach加上next來增加程式的複雜性,不過這些都必須依賴經驗來達成。
9.1.4 標籤
標籤的作用主要就是讓Perl知道他該跳到哪裡去,這樣的寫法並不太常被使用,主要是因為對於程式的結構會有一定程度的破壞,因為你可以任意的設置一個標籤位置,然後要求Perl跳到標籤的位置,當然,他確實有一些使用上的要求,而不是完全漫不限制的隨便下一個標籤就讓Perl轉換執行的位置,至少這並不是goto在做的工作。不過撇開這個暫且不談,我們先來看看怎麼使用標籤。下面的例子應該可以讓大家能夠看出輪廓:
LABEL: for my $outter (1...5) {
for (1...10) {
if ($_ > 2) { next LABEL; } else { print "inner $_ \n"; }
}
next LABEL if ($outter%2);
print $_;
}
當我們有時候單單利用next或last無法逃離迴圈到正確的地方時時,使用標籤就能夠幫助我們找到出路。就像我們的例子中,我們一共有兩個for迴圈,兩個if判斷,我們要怎麼讓Perl不會在裡面迷路呢?這時候標籤的使用就很方便了,就像我們在內部的for迴圈中根據得到的值來決定是否要跳出上一層的for迴圈。
可是使用標籤時有一個特別需要注意的部份,就是標籤的使用並非針對程式中的某一個點,而必須是一個迴圈或是區塊。否則整個標籤的使用就會太過混亂,你會發現要檢查程式的錯誤變成了「不可能的任務」。當然,如果你在你的迴圈中插了大量的標籤也會讓其他人非常困擾,因為就算是Perl可以處理這樣的標籤,只怕你自己也會搞的頭暈。這又是寫程式時的風格問題了。
標籤可以配合我們之前所提的幾種控制指令來運用,因此你可以要求使用next,redo,last加上標籤來標明迴圈的方向。就像上面的例子,我們先在第一行的地方加上標籤'LABEL',表明接下來如果需要,要求Perl直接來這裡。接下來我們用了一個foreach迴圈,其中的值是從1到10。可是在這個迴圈中,我們又使用了next,要求如果變數$_大於2就執行next,而且是跳到標籤LABEL的位置。也就是說,他除了跳過裡面的迴圈之外,也會跳出外層迴圈的其他敘述。所以當內層迴圈的$_變數大於2的時候,程式中最後面的兩行敘述都不會被執行。當然,大家應該還是想要知道這樣的程式會產生出甚麼樣的結果:
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2
inner 1
inner 2
你應該發現了,程式一直都只執行了一部份,因為當內圈的變數$_大於2的時候,Perl就急著要回去LABEL的地方,所以就連裡面的迴圈都沒辦法完整執行,外面的迴圈更是被直接略過,這樣應該就很容易理解了。
9.2 switch
如果你用過其他程式語言,例如C或Java,你現在也許會很好奇,為甚麼我們到目前為止還沒有提到Switch這個重要的流程控制函式,主要是因為Perl在最初的設計是沒有放入Switch的。其實很多人對於Perl沒有提供switch都覺得非常不可思議,不過Larry Wall顯然有他的理由,至於這些歷史原因,我們也沒必要在這裡討論。
好吧,我聽到一陣嘩然,為甚麼Perl沒有這個可以為程式畫上彩妝的工具呢?其實我個人也覺得Switch用來進行各種條件判斷的流程控制確實是非常方便,而且會讓程式看起來相當整齊,不過大部份的時候,你有甚麼流程控制非得需要Switch才能完成呢?因為我們在進行Switch的時候,其實也就是希望表達出許多層的if {} elsif {} elsif {} ..... 。也就是說,if敘述其實已經可以滿足我們的需求了,那麼Switch就真的是幫助我們取得比較整齊,易讀的程式碼。不過在大部份的情況下,你想要用漂亮的程式碼來吸引Perl的黑客(註一)們協助完成一項工作,倒不如告訴他們怎麼樣可以少打一些字。
9.2.1 如果你有複雜的 if 敘述
Switch之所以受到歡迎,當然有過人之處,雖然我們也可以用其他方式達到同樣的目的,可是至少對我來說,程式的易讀性似乎還是以Switch來得好些,不過這部份可就是見仁見智了。就像我們說的,如果你有一大堆if {} elsif {} elsif {} .... 的敘述時,你的程式看起來也許看起來會像這樣:
my $day = <STDIN>;
chomp($day);
if ($day eq 'mon') {
...
} elsif ($day eq 'tue') {
...
} elsif ($day eq 'wed') {
...
} elsif ($day eq 'thu') {
...
} elsif ($day eq 'fri') {
...
}
其實這樣的程式碼也沒甚麼不妥,可是你也許會覺得這樣的寫法有點麻煩。當然,對這些人來說,如果可以把上面這段程式碼利用Switch寫成這樣,那好像看起來更讓人感覺神清氣爽:
my $day = <STDIN>;
chomp($day);
swich ($day) {
case ('mon') { ... }
case ('tue') { ... }
case ('wed') { ... }
case ('thu') { ... }
case ('fri') { ... }
}
以可讀性來講,使用Switch確實比用了一大堆的 if {...} elsif {...} elsif {...} 要好的多,那麼我們要怎麼樣可以使用Switch來寫我們的程式呢?
9.2.2 利用模組來進行
很顯然的,還是有許多Perl的程式設計師對於switch的乾淨俐落難以忘懷。因此有人寫了perl模組,我們就可以利用這個模組來讓我們的程式認識switch。
利用Switch模組,我們就可以寫出像上面一樣的語法,讓你的程式看起來更簡潔有力。而且switch的使用上,不單可以比對某個數字或字串,你還可以使用正規表示式進行複雜的比對來決定程式的進行方向。我們在這裡只是告訴大家一些目前已經存在的解決方案,而不應該在這裡講太多關於模組的使用,以免造成大家的負擔。
另外,還有部份程式設計師不太喜歡目前Switch的運作,認為破壞了原來Perl在流程控制的結構而也會因此而破壞原來Perl程式的穩定性。因為不管如何,這些意見都是僅供參考。不過既然「辦法不只一種」,那麼就看個人的接受度如何了。
9.3 三元運算符
另外也有一種非常類似 if {...} else {...} 的運算符,我們稱為三元運算符。他的寫法也就是像這樣:
my ($a, $b) = (42, 22);
my $max = ($a > $b) ? $a : $b;
print "$max\n";
首先我們把串列 (42, 22) 指定給變數 $a 跟 $b,接著我們要找到兩個值中較大的一個,於是利用判斷式 ($a > $b) 來檢查兩個數字之間的關係。如果 $a > $b 成立,那麼 $max 就是 $a,否則就是 $b。所以很明顯的,上面的三元運算符也可以改寫成這樣:
my ($a, $b) = (44. 22);
if ($a > $b) { $max = $a } else { $max = $b }
print "$max\n";
以上面兩個例子來看,相較之下,三元運算符的方式應該簡單許多,只是這樣的方式並不夠直覺,對於剛開始寫Perl的人而言可能會有點障礙。不過我們還是必須提醒,這樣的寫法很可能常常出現在其他的程式裡,所以即使你只想依賴 if {...} else {...} 來完成同樣的工作,至少你也要知道別人的程式碼中表達的是甚麼。
而且,其實利用三元運算符也可以完成不少複雜的工作。例如你可以在判斷式的地方用一個副常式,並且根據回傳的結果來決定你要的值等等。因此一但有機會,也許你也可以試試。在這裡,我們可以再舉一個怎麼增加便利性的寫法的例子:
my $return = cal(5);
print "$return\n";
sub cal {
my $param = shift;
($param > 4) ? $param*2 : $param**2; # 利用參數來判斷回傳值的運算方式
}
9.4 另一個小訣竅
接下來我們來點飯後甜點,也就是 || 算符。其實不只 || 算符,其他的邏輯算符也可以拿來作流程控制的小小螺絲釘。不過首先我自己偏愛使用 || (也是使用機會比較高的),而且我們只打算來個甜點,這時候顯然不適合大餐了。
我們有時候會希望某些變數可以有預設值,例如副常式的參數,或是希望使用者輸入的變數等等。所以你當然可以這樣寫:
sub input {
my $key = shift;
$key = "預設值" unless ($key);
print "$key\n";
}
這個副常式甚麼也沒作,就只拿了使用者傳來的參數,然後印出來。可是我們還可以讓他更簡單一些,我們把他改成這樣:
sub input {
my $key = shift || "預設值";
print "$key\n";
}
這時候,|| 算符被我們拿來當一個判斷的工具。我們先確定使用者有沒有傳入參數,也就是平常我們所使用的shift,如果@_中是空陣列,那麼 $key = shift 就會得到偽值,這時候 || 就會啟動,讓我們的預設值產生效果。因此我們就得到 $key = "預設值"。
另外,|| 還常常被用來進行意外處理。因為我們必須知道,如果某個運算式失敗,那麼我們就可以讓程式傳回錯誤訊息。就像這樣:
output() || die "沒有回傳值";
sub output {
return 0;
}
我們在程式裡面呼叫 output 這個副常式,不過因為回傳值是 0,於是 || 也發生效用,就讓程式中斷在這裡,並且印出錯誤訊息。
習題:
1. 陸續算出 (1...1) 的總和,(1...2) 的總和,...到 (1...10) 的總和。但是當得到總和大於50時就結束。
2. 把下面的程式轉為三元運算符形式:
#!/usr/bin/perl -w
use strict;
chomp(my $input = <STDIN>);
if ($input < 60) {
print "不及格";
} else {
print "及格";
}
註一:其實我們指的就是hacker,不過現今大多數人都誤用cracker(指潛入或破壞其他人系統者)為hacker(指對某些領域有特別研究的人)