程式扎記: [Perl 學習手冊] CH10 : 其它控制結構

標籤

2010年9月6日 星期一

[Perl 學習手冊] CH10 : 其它控制結構


前言 :
這一章我們將看到許多用來撰寫 Perl 程式的技巧. 這些技巧不會提升 Perl 語言本身的威力, 但卻能讓工作更容易完成.

unless 控制結構 :
if 控制結構裡的程式區塊, 只有當條件運算式為真才會執行. 如果你想讓程式區塊在條件為 "假" 時執行, 請將 if 換成 unless :
  1. $kw = "test";  
  2. unless ($kw =~ /^[A-Z_]\w*$/) {  
  3.     print "$kw doesn't seems as Perl's identified word!\n";  
  4. }  
使用 unless, 表示除非這個條件式為真, 否則就執行這段程式碼. 它和 if 測試一模一樣, 只是條件相反. 另一種幫助記憶的方法就是將它想成獨立的 else 子句. 也就是 if 條件不執行的時候, 就執行 unless 子句.
- 伴隨著 unless 的 else 子句
你甚至可以在 unless 之後加上一個 else 子句. 雖然這是在語法允許的, 但卻有潛在的語意混淆 :
  1. $mon = "Feb";  
  2. unless ($mon =~ /^Feb/) {  
  3.     print "This month has at least 30 days.\n";  
  4. else {  
  5.     print "Can you tell why?\n";  
  6. }  
其實這用 if/else 來寫會直觀點. 通常使用 unless 就是用來確認條件為否的狀態. 因此當你想要條件為真/否都要處理, 建議使用 if/else 以方便維護.

until 控制結構 :
有時候你會想要顛倒 while 迴圈的條件式, 那麼請使用 until :
  1. $i = 50;  
  2. $j = 1;  
  3. until ($j > $i) {  
  4.     $j *= 2;  
  5. }  
  6. print "Rst = $j\n";  
這個迴圈會一直執行, 直到條件式為 "true". 它只不過是個改裝過的 while 迴圈罷了 ; 兩者之間唯一的差別在於 , until 會在條件為 "假" 時反覆執行. 就像 if 和 unless 的例子, 你可以用否定條件的方式將任何 until 迴圈改寫成 while 迴圈.

運算式修飾符 :
為了簡化表達的方式, 運算式後面可以接著一個用來控制它的修飾符. 舉例來說, if 修飾符的作用跟 if 區塊很類似 :
print "$n is a minus number.\n" if $n < 0;

去掉圓括號與大括號, 除了節省了一些打字時間外, 它的執行結果和底下程式完全相同 :
  1. if($n<0) {  
  2.     print "$n is a minus number\n";  
  3. }  
注意到即使條件式寫在後面, 它仍然會先執行. 因此在解讀 Perl 程式碼時, 我們必須跟 Perl 內部編譯器一樣, 把陳敘句全部讀完, 才有辦法判斷它的真意. 還有其他修飾符, 範例如下 :
  1. &error("Wrong input!") unless &valid($input);  
  2. $i += 2 until $i > $j;  
  3. print " ", ($n += 2while $n < 10;  
  4. &greet($_) foreach @person;  
用這種"倒裝句" 撰寫程式, 可以把陳敘句中重要的部分放在前面. 還有運算修飾符的兩邊只能各放一個運算式, 所以不能寫成 "某式 if 某式 while 某式 until 某式 unless 某式" , 這樣容易造成混淆. 此外修飾符的左邊也不能放多個陳敘句. 如果你兩邊都需要一個以上的運算式, 就只能使用傳統的做法.

未修飾的區塊控制結構 :
所謂的 "未修飾" (naked) 區塊就是沒有關鍵字或條件的區塊, 假設你有一個 while 迴圈如下 :
  1. while(條件式) {  
  2.     程式主體;  
  3.     程式主體;  
  4.     程式主體;  
  5. }  
然後拿走關鍵字 while 還有條件式, 就會得到一個未修飾區塊 :
  1. {  
  2.     程式主體;  
  3.     程式主體;  
  4.     程式主體;  
  5. }  
未修飾區塊就像一個 while/foreach 迴圈, 可是它不支援循環執行功能 ; 區塊的內容只會執行一次, 它是個不循環的迴圈!
我們稍後會看到未修飾區塊的其他用法. 為臨時語彙變數提供一個有效範圍, 是它其中一項特色 :
  1. {  
  2.     print "Please give a number: ";  
  3.     chomp(my $n = );  
  4.     my $root = sqrt $n;   
  5.     print "$n root to be $root\n";  
  6. }  
在這個區塊裡, $n 與 $root 都是以此區塊為有效範圍的臨時變數. 如果某個變數只會在某幾列程式使用, 你可以把這幾列放到一個未修飾區塊內, 並且在區塊內宣告變數. 當然如果我們稍後還要用到 $n 或是 $root 的值, 那我們就必須在較大的有效範圍內宣告它們.

elsif 子句 :
許多情況下, 你得逐項檢查一系列的條件式, 看看其中哪個為真. 你可以透過 if 控制結構的 elsif 字句來完此事 :
  1. if(! defined $dino) {  
  2.     print "This value is undef.\n";  
  3. } elsif ($dino =~ /^-?\d+\.?$/) {  
  4.     print "This is integer.\n"  
  5. } elsif ($dino =~ /^-?\d+\.?\d+$/) {  
  6.     print "This is a simple float number.\n";  
  7. else {  
  8.     print "Others\n";  
  9. }  
自動遞增與自動遞減 :
讓純量變數往上或往下數一, 是很常見的用途. 因為它們經常出現, 所以就像其它很常用的運算式一樣, 有特別的簡寫.
自動遞增運算符 (++) 會將純量變數加一, 這跟 C 與其他相似程式語言中同名的算符一樣 :
  1. my $bedrock = 42;  
  2. $bedrock++ # 將 $bedrock 加一, 現在是 43  
跟其他將變數加一的方法一樣, 它在有需要時會建立新的純量 :
  1. my @people = qw{ fred barney fred wilma dino barney fred pebbles };  
  2. my %count;  
  3. $count{$_}++ foreach @people;  # 必要時建立新的鍵與值  
第一次處理 foreach 迴圈時. $count{$_} 會自動遞增. 先是 $count{"fred"}, 因此它會從 undef (因為該雜湊裡原先沒有 fred) 變成1; 迴圈下一次執行時, $count{"barney"} 會變成一. 每次迴圈處理完後, %count 中的某個元素就會自動遞增, 當然也有可能被建立. 這是個快速而簡易的方法, 用來檢查串列中有哪些項目, 並自動計算每個項目的出現次數. 同樣的, 自動遞減運算符 (--) 會將純量變數減一.

- 自動遞增的值
你可以在從變數取的值之際, 同時變更變數的值. 在變數名稱之前置放 ++ 運算符, 代表你要先遞增變數的值, 再取得它的新值. 這是一個前置遞增 (preincrement) :
my $m = 5;
my $n = ++$m;
 # $m 遞增至6, 然後將值放入 $n

相同的道理也可以用在 前置遞減 (predecrement). 如果將變數名稱放在前面, 之後在接遞增或遞減運算符. 這種做法稱為後置遞增/後置遞減 :
my $d = $m++; #$d 取得舊的值 (6), 然後$m 遞增至 7
my $e = $m--; #$e 取得舊的值(7), 然後$m遞減至6

for/foreach 控制結構 :
在 Perl 得剖析器裡, foreach 和 for這兩個關鍵字是等價的. 也就是說當 Perl 看到其中之一時, 換成另外一個也不會有差別. Perl 可以從圓括號的內容判斷出你的意圖. 如果裡面有兩個分號, 它會被當成 for 迴圈. 否則其實它是個 foreach 迴圈 :
  1. for(1..10) { # 從 1 到 10 的 foreach 迴圈  
  2.     print "Count $_!\n";  
  3. }  
這其實是個 foreach 迴圈, 只是寫成 for 的形式. 除了初學者的程式碼, 它通常會被寫成 for 以節省多打四個字母的時間.

迴圈控制 :
看到這裡, 也許你有注意到, Perl 是一個 "結構化" 的程式語言. 尤其是 Perl 程式的任何區塊都只有一個入口, 也就是區塊的頂端. 不過跟前面介紹過的結構, 也時後會需要更多樣式的控制方式. 舉例來說, 你也許需要建立一個跟 while 迴圈相似, 但至少要求執行一次. 要不然你或許偶而需要提早離開某個區塊程式. Perl 有三個迴圈控制運算符, 你可以在迴圈裡使用它們, 好滿足你的需求.

- last 運算符
last 運算符會立即終止迴圈的執行. 它是回圈的緊急出口. 當你碰上 last, 迴圈就會因此而結束 :
  1. while() {  
  2.     if(/__END__/) {  
  3.         # 立即停止迴圈  
  4.         last;  
  5.     } elsif(/john/) {  
  6.         print;  
  7.     }  
  8. }  
  9. ## 碰上 last 之後跳到這裡  
只要輸入 __END__ 記號, 這個迴圈就會結束. 在 Perl 中有五種迴圈區塊 : for, foreach, while, until 以及未修飾區塊. if 區塊或副常式的大括號不算數. last 運算符會對整個迴圈區塊都起作用.

- next 運算符
有時候你並不想離開迴圈, 但是卻想立刻結束目前這次操作. 這就是 next 運算符的用處 ; 它會跳到目前迴圈區塊的底端. 接下來程式將會繼續執行迴圈的下次操作 :
  1. while(<>) {  
  2.     foreach(split) {  
  3.         $total++;  
  4.         next if /\W/;  
  5.         $valid++;  
  6.         $count{$_}++;  
  7.         ## 碰到 next 之後會跳到這裡 ##  
  8.     }  
  9. }  
跟 last 一樣, next 也可以用在這五種迴圈區域 : for, foreach, while, until 或是未修飾區塊, 同樣地如果有許多層的迴圈區塊, next 只會對最內層起作用.

- redo 運算符
迴圈控制運算符的第三個成員是 redo. 它會跳回目前的迴圈區塊的頂端 (不會前進到迴圈的下一次操作). 範例如下 :
  1. my @words = qw{ fred barney peddles dino wilma betty };  
  2. my $errors = 0;  
  3. foreach(@words) {  
  4.     ## 碰到 redo 後會到這裡 ##  
  5.     print "Please key in:";  
  6.     chomp(my $try=);  
  7.     if($try ne $_) {  
  8.         print "Sorry - wrong word.\n\n";  
  9.         $errors++;  
  10.         redo; # 跳到迴圈頂  
  11.     }  
  12. }  
  13. print "Over. You made $errors mistakes.\n";   
和另外兩個運算符一樣, redo 在這五種迴圈都可以作用, 並且在迴圈區塊是巢狀的情況下, 只會對最內層的迴圈起作用. next 和 redo 兩者之間最大的不同在於 next 會正常的繼續下一次操作, 但是 redo 則會重新執行該次操作.

- 帶有標籤的區塊
當你需要對外層的迴圈進行操作時, 請使用標籤 (Label). 在 Perl 裡標籤和其他識別字一樣, 由字母, 數字及底線符號組成, 但不能以數字開頭 - 不過由於它們並沒有前置字符, 標籤可能會和內建函式名稱, 甚至是與你自己的函式名稱互相混淆. 所以將標籤命名為 print 或是 if 是很糟糕的選擇. 正因為如此, Larry 建議將標籤都取名為大寫字母. 這樣不僅能確保它不會跟其他識別字相牴觸, 也使得它在程式中的位置更容易被找到.
要對某個迴圈加上標籤, 只要將標籤及一個冒號放在迴圈前面就行了. 之後再迴圈裡若有需要, 你可以在 last, next 或 redo 的後面加上標籤名稱 :
  1. LINE: while(<>) {  
  2.     foreach(split) {  
  3.         last LINE if /__END__/; # 跳到 LINE 迴圈  
  4.         ...;  
  5.     }  
  6. }  
為了增進可讀性, 通常建議的方法是把標籤靠左對齊, 即使目前的程式碼已經縮排過. 這裡注意到, 標籤是對整個區塊命名, 而不是用來在程式中建立一個目標點. 另外通常應該以名詞來為迴圈命名. 也就是說我們可能叫它 word. 因為它每次會處理一個單字. 如此一來要描述 "next WORD" 或者 "redo LINE" 之類的事就很直覺.

三元運算符 ?:
三元運算符就像是將一個 if-then-else 測試全部放在一個運算式裡. 由於使用時需要三個運算元, 所以稱為 "三元" (ternary) 運算符. 它看起來像底下 :
條件運算式 ? 若條件為真的運算式 : 若為假所執行的運算式

首先執行 "條件運算式", 看看究竟是真還是假. 如果為真則執行第二個運算式 ; 否則就執行第三個運算式. 每次使用都會執行右邊兩個運算式中的一個, 另一個則會被略過. 在下面的例子裡, 副常式 &is_weekend 的執行結果, 決定了哪一個字串運算式會被賦值給變數 :
  1. my $location = &is_weekend($day) ? "home" : "work";  
下面的例子, 我們會計算並印出一個平均值 - 或是在求不出平均值時, 用一列連字符來表示 :
  1. my $average = $n ? ($total/$n) : "--------";  
  2. print "平均值 : $average\n";  
底下是一個常見的技巧, 用來撰寫一個俐落的多重分岔程式碼 : (也可以使用 if/else 結構)
  1. my $size =   
  2.     ($width < 10) ?  "小" :  
  3.     ($width < 20) ?  "中" :  
  4.     ($width < 50) ?  "大" :  
  5.                      "超大"; # 預設值  

邏輯運算符 :
Perl 擁有處理布林 (真/假) 值所有必要的邏輯算符. 比方說以邏輯 AND 運算符 (&&) 與 邏輯 OR 運算符 (||) 來連結多項邏輯測試 :
  1. if($dessert{'cake'}  && $dessert{'ice cream'}) {  
  2.     # Both true  
  3.     print "Have cake and ice cream!!\n";  
  4. }   elsif ($dessert{'cake'} || $dessert{'ice cream'}) {  
  5.     print "Not bad...\n";  
  6. else {  
  7.     # No one true.  
  8. }  
- 短路運算符
在 Perl 裡, 短路運算符所求得的值不僅是布林值, 也是它上一次執行的值. 它們的效果是一樣的, 因為當整個測試應該為真的時, 上一次執行的部分一定為真 ; 而且當整個測試應該為假時, 上一次測試結果一定為假. 不過當它做為傳回值時, 會更有用處. 例如選擇預設值, 就是邏輯 OR 運算符的粽多用途之一 :
my $last_name = $last_name{$someone} || '(No last name)'; # 當沒有某人的 lase name, 則放入 '(No last name)'

上面的用法即當 $lase_name{$someone} 的值為 "假" (空字串 or undef), 則使用預設值 '(No last name)'. 但有時你只希望當遇到 undef 才使用預設值, 這時你可以使用三元運算符來修正此問題 :
  1. my $last_name = defined $last_name{$someone} ?  
  2.     $lase_name{$someone} : '(No last name)';  
- defined-or 運算符
上面你看到了我們使用 || 運算符來提供預設值. 忽略特殊的情況 ; 即使所定義的值為假值, 仍舊是一個值, 接著你看到了三元運算符的版本, 為了讓你少打幾個字, Perl 5.10 提供了 defined-or 運算符 // , 當它發現左邊的值有定義便會執行短路的版本, 而不管左邊的值為真或假 (即使某人 $someone 的 last name 為 0) :
use 5.010;
my $last_name = $last_name{$someon} // '(No last name)';

而另一個 defined-or 的運用就是當你想在變數的值尚未定義時, 才提供一個預設值, 但如果值已經定義, 則不用理會它. 這裡我們用環境變數 VERBOSE 為範例 :
  1. use 5.010;  
  2. my $Verbose = $ENV{VERBOSE} // 1;  
  3. print "I can talk to you!\n" if Verbose;  
下面使用 // 的範例中. 你可以看到當 $try 的值為何時, 會得到 default 的值 :
  1. use 5.010;  
  2.   
  3. foreach my $try (0, undef, '0'125) {  
  4.     print "Trying [$try] ---> ";  
  5.     my $value = $try // 'default';  
  6.     say "\tgot [$value]";  
  7. }  
輸出結果如下, 當 $try 為 undef 你會得到 default 字串 :
Trying [0] ---> got [0]
Trying [] ---> got [default] # undef 轉為空字串不會顯示
Trying [0] ---> got [0]
Trying [1] ---> got [1]
Trying [25] ---> got [25]
This message was edited 8 times. Last update was at 30/08/2010 10:21:23

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!