程式扎記: [Perl 學習手冊] CH15 : 智慧型比對與 given-when

標籤

2010年9月24日 星期五

[Perl 學習手冊] CH15 : 智慧型比對與 given-when


前言 :
Perl 做到了當你想要數值時就使用數值, 當你想要字串時就使用字串, 當你意指單一值時它就使用單一值, 以及當你意指使用串列它就使用串列. 如果使用 Perl 5.10 的智慧比對運算符 以及 given-when 控制結構, 它更可以大量減少你的Code.

智慧型比對運算符 :
智慧型比對運算符 ~~ 會檢視它兩邊的運算元, 並自行決定應該如何比對它們. 如果兩邊的運算元看起來比較像數值, 它就會進行數值比較. 如果他們看起來像字串, 它就會進行字串比較. 如果有一個運算元看起來像是正規表示式, 它就會進行樣式比對, 因此它可以讓你少打很多字.
越複雜的運算越能展現出智慧比對運算符的能耐. 假設你想在雜湊 %name 中的某個鍵與字串 "John" 相符時印出一道訊息. 你無法使用 exists 函式, 因為它只會檢查鍵是否存在. 你可以使用 foreach 迴圈並以正規表示式測試每一個鍵, 並跳過那些不相符的部分, 範例如下 :
  1. my $flag = 0;  
  2. %names = ("JohnLee" => 30"PeterWang" => 29);  
  3. foreach my $key (keys %names) {  
  4.     next unless $key =~ /John/;  
  5.     $flag = $key;  
  6.     last;  
  7. }  
  8. print "I found a key matching 'John'. It was $flag\n" if $flag;  
挖勒, 光是解釋就不簡單, 但是它可以在任何 Perl 5 任何版本上跑. 如果使用智慧比對運算符, 你只需要把雜湊放在它個左邊以及把正規表示式擺在它的右邊就行了 :
  1. use 5.010;  
  2. %names = ("JohnLee" => 30"PeterWang" => 29);  
  3. say "I found a key matching 'John'" if %names ~~ /John/;  
對智慧型比對運算符來說, 當它看到一個雜湊與一個正規表示式, 它便會知道將正規表示式應用在 %names 的每個鍵上, 如果有發現某個鍵比對成功, 它就會停止比對並傳回真值. 而實際上它會根據實際的情況做對的事. 因此一個運算符能讓你進行不同的運算.
如果你想要比對兩個陣列 (為了簡單起見, 假設這兩個陣列具有相同大小), 你可能會以其中一個陣列的每個元素來比對另一個陣列相應的元素. 每當相應的元素相同就遞增 $equal 計數器. 迴圈執行完畢, 如果 $equal 等於 @names1 之元素的個數, 則這兩個陣列必定是相同的 :
  1. my $equal = 0;  
  2. @names1 = qw/ 1 2 3 4 /;  
  3. @names2 = qw/ 1 2 3 4 /;  
  4. foreach my $index(0 .. $#names1) {  
  5.     last unless $names1[$index] == $names2[$index];  
  6.     $equal++;  
  7. }  
  8. print "The arrays have the same elements!\n" if $equal == @names1;  
如果使用智慧比對運算符, 只需要把這兩個陣列分別放在 ~~ 的兩邊就可以完成前面代碼所做的事 :
  1. use 5.010;  
  2. my $equal = 0;  
  3. @names1 = qw/ 1 2 3 4 /;  
  4. @names2 = qw/ 1 2 3 4 /;  
  5.   
  6. say "The arrays have the same elements!" if @names1 ~~ @names2;   
再舉一個例子, 假設你呼叫一個函式 max() 並且想要知道回傳的值是否屬於它的引數串列以便檢查, 你可能會這麼做 :
  1. my @nums = qw/ 1 2 3 27 42 /;  
  2. my $result = max(@nums);  
  3.   
  4. my $flag = 0;  
  5. foreach my $num (@nums) {  
  6.     next unless $result == $num;  
  7.     $flag = 1;  
  8.     last;  
  9. }  
  10. print "The result is one of the input values\n" if $flag;  
但透過智慧比較運算符, 你就可以很輕鬆完成上面代碼的目的 :
  1. my @nums = qw/ 1 2 3 27 42 /;  
  2. my $result = max(@nums);  
  3. print "The result is one of the input values\n" if $result ~~ @nums;  
最後智慧比對運算符具交換性, 也就是說運算元的順序不會影響結果.

智慧比對運算符的優先順序 :
了解智慧比對運算符可以省下大量工作後, 現在你必須知道如何判斷它所進行的是何種比對. 你可以參考 perlsyn 線上文件. 底下列出了智慧型比對運算符所能進行的各種比對 :
範例 | 比對類型
%a ~~ %b | 是否雜湊完全相同
%a ~~ @b | 是否 %a 中至少有一個鍵為 @b 的一個元素
%a ~~ /Fred/ | 是否至少有一個鍵與樣式相符
%a ~~ 'Fred' | 是否存在 $a{Fred} 雜湊鍵
@a ~~ @b | 是否兩個陣列完全相同
@a ~~ /Fred/ | 是否至少有一個元素與樣式相符
@a ~~ 123 | 是否至少有一個元素為 123(數值)
@a ~~ 'Fred' | 是否至少有一個元素為 'Fred' (字串)
$name ~~ undef | 是否 $name 尚未定義
$name ~~ /Fred/ | 樣式比對
123 ~~ '123.0' | 以數值形式的字串比較數值是否相等
'Fred' ~~ 'Fred' | 比較字串是否相同
123 ~~ 456 | 比較數值是否相同

當你使用智慧型比對運算符, Perl 會從此表的頂端開始尋找它的兩個運算元相應於何種比對類型. 運算元的順序無關緊要.

given 陳敘句 :
使用 given-when 控制結構, 我們可以在所給訂的引數滿足某個條件時執行某個程式碼區塊. Perl 的這個控制結構相當於 C 的 switch 陳敘句. 下面程式碼會先從命令列取得第一個引數, 然後檢查每個 when 條件式 (從最寬鬆到最嚴苛), 看看是否能夠找到 John. 每個 when 區塊會在相應條件式找到 John 時做出不同回應 :
  1. use 5.010;  
  2.   
  3. given ($ARGV[0]) {  
  4.     when(/John/i) {say 'Name has John in it'}  
  5.     when(/^John/) {say 'Name starts with John'}  
  6.     when('John') {say 'Name is John'}  
  7.     default {say "I don't see a John"}    
  8. }  
given 會替它的引數取 $_ 這個別名, 而且每個 when 條件式會自動替 $_ 進行智慧型比對. 你可以改寫前面並使用 $_ ~~ /John/i 明確的進行智慧型比對. 如果 $_ 並不滿足任何的 when 條件式, 則 Perl 會執行 default 區塊. 你可能也能夠使用 if-elsif-else 來撰寫. 如果given-when 跟 if-elsif-else 一模一樣, 那就沒有什麼意義了. 與 if-elsif-else 結構不同的是, given-when 在滿足某個條件之後還可以檢查是否滿足其他條件. 接著我們先澄清幾件事. 除非你有明確指定, 否則每個 when 區塊的結尾處都會自動執行 break, 這會使得 Perl 停止執行 given-when 結構, 並且移往程式的其餘部分. 前面的例子在 when 區塊的結尾處事實上都會有 break, 只是你不必親自鍵入它們. 然而這個程式運作的布是很好. 因為我們希望程式能夠逐一檢查 when 條件式, 而不是滿足一個就終止整個控制結構.
因此有了 continue 可以在 when 區塊的結尾處使用, 則 Perl 還會嘗試後續的 when 陳敘句. 而這是 if-elsif-else 無法做到的. 而當有一個 when 的條件在之後被滿足後, Perl 會執行相應的區塊, 同樣預設的 break 也會被執行. 參考修改代碼如下 :
  1. use 5.010;  
  2. $name = "AlJohn";  
  3. given ($name) {  
  4.     when(/John/i) {say 'Name has John in it'continue;}  
  5.     when(/^John/) {say 'Name starts with John'continue;}  
  6.     when('John') {say 'Name is John'continue;}  
  7.     default {say "I don't see a John"}    
  8. }  
但這個條件有個問題, 當我們執行程式時, 我們看到 default 區塊也會被執行 :
Name has John in it
I don't see a John <預設區塊訊息>

default 區塊實際上是一個永遠為真的 when 條件. default 之前 when 區塊具有一個 continue. 因此 Perl 會繼續執行 default 區塊. 因此我們只要把最後一個 when 的 continue 拿掉就可以解決問題了.
測試多個項目時, 可以參考下面範例代碼 :
  1. use 5.010;  
  2. @names = qw/ John PeterWang/;    
  3. foreach(@names) {    
  4.     say "\nProcessing $_";    
  5.     when(/John/i) {say "Match(0) > Name has John in it"continue;}  
  6.     when(/^John/i) {say "Match(1) > Name starts with John"continue;}    
  7.     when('John')  {say "Match(2) > Name is John";}    
  8.     say "Moving on to default...";    
  9.     default       {say "I don't see a John"}    
  10. }  
This message was edited 11 times. Last update was at 21/03/2011 17:15:17

4 則留言:

  1. 請問 我執行最後一個範例時,JohnLee的名字還是會執行default的script.
    是否是因為這個名字跳過
    when('John') (say...} 的檢查
    開始執行 say "Moving on to default..."之後的部分

    請問有好的方法可以解決?

    回覆刪除
  2. 感謝你的意見, 範例我有修改過. 當$_ = JohnLee
    則最後一個 when 就會被執行到而導致預設的 break 也被執行因而 default 的部分就不會被跑到. 不過你提到的部分確實是個問題, 最快的解法就是在第一個 when 滿足後用一個 flag 決定 default 的東西要不要印/執行. 如果第一個 match 有跑到, 說明 default 內的東西就可以忽略...但實際還是要看你的程式邏輯拉. ^^

    回覆刪除
  3. 謝謝你的回答,不過你的意思是要把 $_=JohnLee改成John?
    如果是這樣的話我就理解了。

    回覆刪除
  4. 拍謝, 原本是 $_=JohnLee, 後來我想跟原本程式邏輯不搭, 所以又改回 :
    @names = qw/ John PeterWang/
    ...
    when('John') {say "Match(2) > Name is John";}

    回覆刪除

網誌存檔

關於我自己

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