程式扎記: [Perl 學習手冊] CH04 : 副常式

標籤

2010年8月21日 星期六

[Perl 學習手冊] CH04 : 副常式

前言 : 
我們已經見過一些內建的系統函式, 像是 chomp, reverse 和 print 等等. 但是如同其他程式語言一樣, Perl 也可以讓你定義 副常式 (subroutine), 也就是由使用者定義的函式. 副常式可以讓我們在程式中重複利用一塊程式碼. 而在Perl 中使用副常式可以使用識別字 & 符號. Perl 有一到準則規定哪些時候可以省略 & 符號, 哪些時候不行. 目前除了禁止使用情況下, 我們一律使用 & 符號來使用副常式 ; 這是萬無一失的做法. 

定義副常式 : 
定義你自己的副常式, 將會使用到關鍵字 sub, 副常式的名稱 (不含 &) 以及 (大括號內) 經縮排的程式碼塊, 此區塊構成副常式主體, 範例如下 : 

  1. sub marine {  
  2.     $n += 1;  
  3.     print "Marine $n!!\n";  
  4. }  
副常式的定義可以放在程式裡的任何地方. 與 C 不一樣的 Perl 的副常式不需要前置宣告. 而副常式的定義具有全域性 ; 除非使用某些強制技巧, 否則不會有私有的副常式. 假如你有兩個同名的副常式, 後者將覆蓋前者, 但這樣會被認為是差勁的寫法. 如同前面範例所示, 副常式裡可以使用任何的全域變數. 事實上目前為止, 我們所看到的變數都是全域變數 ; 也就是說你可以在程式的任何地方存取它們. 稍後我們會介紹如何定義私用變數. 

調用副常式 : 
任何運算式中只要有使用副常式的名稱 (加上 & 符號), 就會調用該副常式 : 
  1. &marine;   # Show : Marine 1!!  
  2. &marine;   # Show : Marine 1!!  
  3. &marine;   # Show : Marine 1!!  
呼叫副常式並實際處以其結果, 而在 Perl 中, 任何的副常式都有傳回值. 然而並不是任何的 Perl 副常式都會提供有用的傳回值. 
既然任何 Perl 副常式都有傳回值, 所以 Larry 將之簡化成在副常式的執行過程中, 它會不斷的進行運算 ; 其中最後一次運算結果不管是多少, 都會自動被當作副常式的傳回值. 舉例來說, 我們定義了一個副常式如下 : 
  1. sub sum_of_fred_and_barney {  
  2.     print "He, you call sum_of_fred_and_barney subroutine!!\n";  
  3.     $fred + $barney;  # Return value of this subroutine.  
  4. }  
上面的副常式的回傳值就是 $fred 與 $barney 的總合 ; 以下是比較完整的範例代碼 : 
  1. $fred = 3;  
  2. $barney = 4;  
  3. $wilma = ∑_of_fred_and_barney;  
  4. print "\$wilma is $wilma.\n";  
  5. $betty = 3 * ∑_of_fred_and_barney;  
  6. print "\$betty is $betty.\n";  
  7. sub sum_of_fred_and_barney {  
  8.     print "He, you call sum_of_fred_and_barney subroutine!!\n";  
  9.     $fred + $barney;  # Return value of this subroutine.  
  10. }  

引數 : 
Perl 副常式可以有引數 (argument). 要將引數串列傳給副常式, 只要在副常式調用後面加上被圓括號括住的串列運算式就行了, 作法如下 : 
$n = &max(10, 15); # 此副常式的呼叫包含兩個參數 

引數串列會被傳遞給副常式, 當然會有個變數來儲存這個串列, 所以在參數串列會自動賦值給名為 @_ 的特殊陣列變數, 在副常式執行其間都有效. 所以這表示副常式的第一個變數存放於 $_[0], 第二個變數存放在 $_[1] 依此類推 - 請特別注意這些變數與 $_ 變數毫無關聯, 就像 $name[0](陣列變數) 與 $name (純量變數) 能彼此共存一樣, 而 @_ 就是給副常式使用的參數陣列而以. 接著我們來完成上面範例的 max 副函式 : 
  1. sub max {  
  2.     if($_[0] > $_[1]) {  
  3.         $_[0];  
  4.     } else {  
  5.         $_[1];  
  6.     }  
  7. }  
上面的副函式雖然工作正常, 但是一堆索引讓程式變的不雅觀而難以閱讀與維護. 另一個問題就是名稱 &max 雖然易懂但是卻不能提醒我們這個副常是只需要兩個參數. 而實際狀況是多餘的參數將被省略, 不足的參數則會的到 undef 的值. 另外一點要注意的是 @_ 是支援巢式呼叫的, 每次呼叫副函式都會有自己當下的 @_ 參數陣列, 不用擔心在遞迴呼叫同一個副函式時會發生 @_ 參數陣列被誤用. 

副常式裡的私用變數 : 
在 Perl 裡所有的變數都是全域變數, 也就是說在程式中任何地方你都可以存取它們. 但是你隨時可以使用 my 關鍵字建立稱為 語彙變數 (lexical variable) 的私有變數 : 
  1. sub max {  
  2.     my($m, $n) = @_; # 將參數賦值給私用變數  
  3.     if($m > $n) {$m} else {$n}  
  4. }  
這些變數的 "作用範圍" 被限制在所屬的區塊中 (也就是該區塊的私用變數) ; 其他地方的 $m, $n 完全不受這兩個私用變數 ($m, $n) 的影響. 反之亦然. 另外值得一提的是在上面範例代碼中 if 區塊裡, 做為傳回值的運算式沒有分號. 雖然 Perl 允許你省略區塊中的最後一個分號, 但通常只有像前面的範例那樣, 在程式碼簡單到整個區塊濃縮成一列時, 才會省略分號. 
而前面範例副函式的第一列會建立並設定私用變數的值. 幾乎所有的副常式都會以類似的程式碼做為開頭. 但看到這一列你就會知道這個副常式具有兩個純量參數. 而在副常式中它們分別被成為 $m 與 $n. 

不定長度的參數串列 : 
Perl 哲理 "去除不必要的限制" 讓我們可以在副常式傳入任意數目的參數, 不過當傳入不如預期參數個數時也許會造成問題. 因此我們可以檢查 @_ 陣列以確定引數個數是否正確, 簡單範例如下 : 
  1. sub max {  
  2.     if(@_ != 2) {print "Warning! Only two arguments required!\n";}  
  3.     print "Dealing as usual...\n";  
  4. }  
但其實最好的辦法是讓 &max 可以接受任意數目的參數 : 
  1. sub max {  
  2.     my($max_so_far) = shift @_;  
  3.     foreach(@_) {  
  4.         if($_ > $max_so_far){$max_so_far = $_;}  
  5.     }  
  6.     $max_so_far;  
  7. }  
  8. print &max(124108);  
透過上面代碼, 不管傳入幾個參數給 &max 副函式, 它都會求出在參數裡面最大的值並將之回傳 (10). 那如果我們不傳入任何參數給 &max 副函式呢? 結果可以預期的是 undef 將會被回傳. 因此呼叫 &max 的人必須留意回傳值可能是 undef. 

return 運算符 : 
在副常式裡, return 運算符會立即傳回某個值 : 
  1. my @names = qw/ fred barney betty john ken /;  
  2. foreach(@names) {  
  3.     print "$_ ";  
  4. }  
  5. print "\n";  
  6. print &which_element_is("john"@names);  
  7. sub which_element_is {  
  8.     my($what, @array) = @_;  
  9.     foreach(0..$#array) {  
  10.         if($what eq $array[$_]) {  
  11.             return $_;  
  12.         }  
  13.     }  
  14.     -1;  
  15. }  
上面範例代碼的副函式用來找出 "john" 在 陣列 @names 的索引值. 透過 return 我們可以再一經比對成功就將索引值回傳而不必等到整個 foreach 迴圈跑完. 

傳回非純量值 : 
考慮你想取出某段範圍的數字 (如同範圍運算符 ..), 而你不但想往取, 也想往下取. 所以在不透過範圍運算符你可以定義副函式如下 : 
  1. $a = 11;  
  2. $b = 6;  
  3. sub list_from_a_to_b {  
  4.     if($a < $b) {  
  5.         return $a..$b;  
  6.     } else {  
  7.         return reverse($b..$a);  
  8.     }  
  9. }  
  10. my @result = list_from_a_to_b;  # 得到 (111098 ,76)  
  11. foreach(@result) {  
  12.     print "$_ ";  
  13. }  
其實你也可以甚麼都不回傳. 單寫一個 return 不給任何引數時, 在純量語境就是回傳 undef, 在串列語境就成了空串列. 

持續的私用變數 : 
使用 my 關鍵字, 我們可以定義副常式使用私用的變數, 但是副常式的每次呼叫都必須重新定義這些私用變數. 使用 state 關鍵字, 我們仍然可以定義副常式的私用變數, 不同的是 Perl 會在副常式的兩次呼叫之間保存這些變數. 考慮之前的 &marine 副函式改寫如下 : 
  1. use strict;  
  2. use 5.010;  
  3.   
  4. sub marine {  
  5.     state $n = 0;  
  6.     $n += 1;  
  7.     print "Marine $n!!\n";  
  8. }  
  9. marine;  
  10. marine;  
  11. marine;  
這裡即使我們不使用全域變數, 但如純量變數 $n 依然可以累加. 是因為副常式在第一次呼叫中便宣告初始化 $n. 後續的所有呼叫中, Perl 會忽略該陳序句 (即 state $n = 0), 且每次呼叫之間, Perl 會替副函式的下一次呼叫保存 $n 的值. 不只是純量變數, 我們可以將任何型態的變數宣告為 state 變數. 參考下面範例透過一個 state 陣列來提供一個連續總合 : 
  1. use strict;  
  2. use 5.010;  
  3.   
  4. sub running_sum{  
  5.     state @numbers;  
  6.     state $sum=0;  
  7.     foreach my $number (@_) {  
  8.         push (@numbers, $number);  
  9.         $sum += $number;  
  10.     }  
  11.     print "Total Sum(@numbers) is $sum!\n";  
  12. }  
  13.   
  14. running_sum(56);  
  15. running_sum(1..3);  
  16. running_sum(4);  
但是將陣列或是雜湊宣告為 state 變數還是存在些微的限制 ; 無法使用串列語境來初始化它們 : 
state @array = qw( a b c ) # 錯誤!!!! 

這個問題可能在未來新版本的Perl 獲得解決. 

補充說明 : 
* Tutorialspoint > Perl Tutorial > Perl Subroutines 
* Perl 學習手扎 > 副常式

沒有留言:

張貼留言

網誌存檔

關於我自己

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