« 5. 雜湊(Hash) | Perl 學習手札目录 | 7. 正規表示式 »

6. 副常式

6. 副常式
當你的程式在很多時候總是不斷在進行類似的工作時,而且你總是為了這些工作在寫相同的重複程式碼時,你應該考慮替你自己把這些段落用更簡潔的方式來萃取出一個可以重複使用的區塊。這時候,你就可以考慮使用副常式,不過如果要更清楚的了解副常式的意義,也許我們應該先來看看這樣的例子:


my @array = qw/6 8 -4 18 -9 -22 36 48/; my @new_array = map { ($_**2) } grep { $_ > 0 } @array; print @new_array; print "\n"; my @array2 = qw/16 8 -24 8 -12 20 16 28/; my @new_array2 = map { ($_**2) } grep { $_ > 0 } @array2; print @new_array2;

從這幾行程式,你應該很容易看出一些端倪,因為我們發現在這個小小的程式中,幾乎除了陣列中的元素不同之外,其他的幾行程式顯然都在進行相同的工作。也許你覺得只有兩個陣列需要運算還算容易,不過當你需要進行相同運算的陣列達到十個,或二十個的時候,你總不希望又把相同的程式碼進行大量的複製吧?
也許你會認為複製,貼上這樣的動作總比弄懂副常式來得簡單許多,不過你也許需要考慮,當你的程式需要進行修改怎麼辦?讓我們來舉一個例子吧,如果你的程式在取平方值的地方現在希望可以取立方值,或甚至進行更複雜的運算,而你現在總共複製了一百份的程式碼在做同樣的運算。沒錯,恭喜你,你現在就因為這樣小小的改變而必須修改這一百個地方,而且這對程式的除錯會變成非常大的負擔。除非你認為你的工作時間就應該花在這樣的工人智慧上,否則你確實要考慮來使用副常式了。當然,如果你還是堅持不用副常式,千萬別找別人去維護你的程式。
使用副常式不但可以增加程式的可重用性,降低維護成本,當然還可以提昇程式的可讀性。就像剛剛的例子,你對於每次需要進行某個運算時,只需要使用不同的參數,而如果運算部份的程式需要修改時,也只需要修改一次。對於想要看程式碼的人來說,他也可以清楚的看懂這一部份到底進行甚麼樣的工作。因此適當的在自己的程式中使用副常式確實是非常必要而且提高效率的方式。

6.1 關於Perl的副常式
在Perl中,使用副常式並不困難,尤其當你曾經使用其他程式語言寫過副常式,那麼Perl的副常式對你來說更是容易上手。不過我們假設你從來沒寫過任何程式語言,那麼我們準備從Perl來學習副常式了。
在Perl中,我們可以用&來表明副常式,而識別字的命名方式也是和其他變數的命名方式相同。也就是可以用數字,底線和字母組成,但是不能以數字開始。關於副常式,大概有兩個部份你必須特別注意的,也就是副常式的定義跟叫用。也就是副常式本身的區塊以及使用副常式。就像我們剛剛說到的,副常式的本身是以&符號作為辨識,所以如果你有一個叫做DoSub的副常式,那麼你就可以利用&DoSub的方式來叫用。當然,利用&符號叫用副常式本身,並不是絕對必要的,除非你的副常式名稱和Perl內建的函式名稱有所重疊,否則你其實可以省略&,也就是說,你可以直接使用DoSub來叫用你自己寫的DoSub副常式。當然,有時候我們會建議你在叫用副常式時盡量加上&符號,除非你能夠非常確定你使用的副常式名稱和Perl並不重複。但是當你看到許多程式設計師都省略&符號時,可別以為他們寫錯了,他們也許都是經驗老練的高手,已經能輕易確定自己所使用的函式名稱不會發生衝突。如果你還是不太了解我們解釋的意思,那還是讓我們來看看這樣的寫法吧:


my $num = 12; print hex($num),"\n";   # 這是Perl提供的hex函式 print &hex($num),"\n";   # 我們自己寫的hex副常式 sub hex { my $param = shift; $num*2; }

這個例子應該就可以非常簡單的看出&帶來的不同,我們第一次使用了hex來呼叫函式,因為Perl內建了hex這個函式,所以Perl會直接使用內建的hex函式,而第二次我們使用&hex呼叫時,才真正叫用了我們自己定義的副常式hex。
不過說了這麼多,我們還是必須在開始叫用副常式之前,先嘗試寫出你自己的副常式。也就是主要運作的那一個部份,你可以使用sub這個Perl的關鍵字來定義一個副常式,而且既然我們已經使用了sub這個關鍵字,你總不會還以為我們會喜歡多打一個&字元吧,所以最典型的副常式大多會長的像這樣子:


sub subroutine { ... ... }

當然,副常式內的縮排並非絕對必要的,不過為了保持程式的可讀性跟維持好的程式寫作習慣,我們還是極力建議各位在進行程式寫作時,能夠養成區塊內的縮排。在Perl中,一般的使用情況下(註一),你可以把副常式放在程式中的任何位置,只要你叫用的時候,能夠讓程式本身不至於找不到副常式而發生錯誤就可以。雖然筆者自己習慣把副常式放在最後,不過對於已經有其他程式語言寫作習慣的人來說,也許有規定副常式的位置,而養成在程式的一開始就定義出程式中用了那些副常式的習慣。不過不管如何,Perl對於這些情況都是允許的,所以你可以試著找到自己習慣的方式。
另外,在Perl中使用副常式還有一個特點,也就是對於程式中全域變數的存取。由於副常式也是屬於程式的一部份(對Perl來說,那就是另一個程式中的區塊),因此在Perl的設計中,你可以任意的存取程式中的全域變數,就像我們之前使用的那些變數。對很多使用其他程式語言的人來說,這實在非常不可想像,當然,也因此持反對意見的人應該也不在少數。不過保留這樣的功能卻未必就是鼓勵使用者以這樣的方式來寫程式,而只是保留某種彈性的空間。筆者還是建議各位能盡量使用參數的方式,並且使用副常式中的私有變數,這樣的建議當然是有一些理由的,因為你很可能在程式的發展過程中寫了一些副常式,並且把他們放在程式之中,等到程式慢慢成熟之後,你也許就可以把這些副常式放進模組裡,以方便建立程式的可重用性(註二),以及屬於自己的函式庫。這時候,如果你的副常式足夠獨立的話,那麼搬移的工作就可以輕鬆許多,也不容易產生一些難以除錯的狀況。相反的,如果你在開始使用副常式的時候就大量使用全域變數時,你可能會發現要把這些副常式放入模組中就顯得特別困難。不過有時候能夠在副常式中使用全域變數也是非常方便的,例如有些模組中,你可能會有一些不提供給外部使用的副常式,這時候你也許會直接叫用程式的全域變數。
現在,我們來寫我們的第一個副常式:


sub hello { print "hello\n"; }

沒錯,這個副常式雖然簡單,但是卻能夠讓我們一窺副常式的奧秘,所以當你叫用這個副常式時,他只會印出"hello"這個字串。那我們就來試試:


&hello;   # 印出 hello &hello;   # 再印一次 sub hello { print "hello\n"; }

你應該發現了,副常式就是這麼簡單。

6.2 參數
沒錯,副常式的用法其實並不太困難,不過要能發揮副常式更重要的功能,可就還要下些功夫了,也就是讓副常式能根據我們的需求進行不同的回應。所以我們應該想辦法讓副常式能根據我們的需求來進行一些調整,進行不同的運算。首先,我們需要的是參數,所謂的參數也就是需求不同的那一個部份,利用參數來告訴副常式我們所需要的調整。使用參數,當然會有傳入跟接收的部份。發送端也就是叫用的部份,也就是我們要告訴副常式,我們需要進行調整的內容,我們只要直接把所要傳送的值放進小括號內,就像這樣:

&hello('world');

不過只有傳送當然是不夠的,我們的副常式也需要知道外面的世界發生了什麼事,它需要接收一些資訊。那我們來看看傳送的資訊去哪裡了,我們先來做個實驗:


&hello("world");   # 我們傳了參數 "world" sub hello { print @_;   # 原來參數傳到這裡了 }

我們可以看到,當我們呼叫副常式,並且把參數傳給副常式時,參數會被放到預設的陣列變數@_裡。這樣我們就可以叫用參數來進行操作了。既然如此,我們來改寫hello這個副常式吧!


&hello("world");   # 傳參數"world" sub hello { my $name = shift @_;  # 把參數從預設陣列拿出來 print "hello $name\n";  # 根據參數不同印出不同的招呼 }

6.3 傳回值
大多數的時候,我們除了參數,還會希望副常式可以有回傳值,也就是讓副常式利用我們的參數運算之後,也能夠傳回運算結果給我們,比如我們想要寫一個找階乘(註四)的副常式,因此我們告訴副常式,我們希望找到某個數的階乘,而當然也期望從副常式得到運算的結果,也就是我們需要副常式的回傳值。最簡單的方式,就是在副常式中使用return這個指令來要求副常式回傳某個值。我們可以試著把階乘的副常式寫出來:


my $return = &times(4);   # 把回傳值放到變數$return print $return; sub times { my $max = shift;   # 把參數指定為變數$max my $total = 1;   # 如果不指定,預設會是0,那乘法會產生錯誤 for (1...$max) {   # 從 1 到 $max   $total *= $_; # 進行階乘的動作 } return $total   # 傳回總數 }

在這裡,又有一些簡便的使用方式來處理Perl的傳回值,因為Perl會把副常式中最後一個運算的值當成預設的回傳值,所以你可以省略在進行運算後還必須再進行一次return的動作。就像這樣的寫法:


my $return = &square(4); print $return; sub square { my $base = shift; $base**2; }

這時候,我們看到副常式的最後一次運算是把參數進行了一次取平方的動作,而這個運算結果就會直接被Perl當為回傳值,所以你就不需要再另外進行回傳的動作。這樣確實可以簡化寫副常式時的手續,繼續維持了Perl的簡樸風格。當然,如果你還是不太熟悉這種回傳的方式,你還是可以加上return的敘述,不過當你在看其他Perl程式設計師的程式時,可別被這樣的寫法搞混了。

6.4 再談參數
我們已經知道了在副常式中怎麼使用參數及回傳值,而且我們還看到了Perl在處理參數時所使用的預設陣列。聰明的讀者應該早就猜到,當我們使用超過一個的參數時,應該就是依照陣列的規則一個一個被填入預設的陣列中,因此我們也可以按照這樣的原則來取出使用。我們可以用剛剛的概念,很容易的理解多個參數時的運用:


my $return = &div(4, 2);   # 這時候有兩個參數 print $return; sub div { $_[0]/$_[1];   # 只是進行除法 }

這樣的寫法其實非常粗糙,不過我們只是舉例來說明副常式的參數運用。這次我們直接取出預設陣列中的元素來進行預算,因為只有一個運算式,所以運算結果也自然的被當成回傳值了。這樣的運用方式非常的簡明,所以當你在寫副常式的時候,你便可以使用許多組的參數。不過如果我們在叫用副常式的時候傳了三個參數,就像:

&div(4, 2, 6);

那會產生什麼結果呢?其實回傳值就跟原來的一樣,因為Perl並不會去在意參數的個數問題。不過如果你的程式有需要,應該去確認參數的個數,避免參數個數無法應付需要,以確保程式能正常而順利的進行。
既然Perl的參數是以陣列的方式儲存,而我們也知道,Perl的陣列並沒有大小的限制,也就是以系統的限制為準。那麼我們很容易的可以傳入多個參數,而且還可以正確的運算並且回傳運算的結果。就像這樣:


my $return = &adv(4, 2, 6, 4, 9);  # 我們一次傳入五個參數 print $return; sub adv { my $total; for (@_) {   # 針對預設陣列進行運算   $total += $_; # 加總 } $total/($#_+1);   # 除以總數 (取平均) }

這時候,不論你的參數個數多少,Perl都可以輕鬆的應付,然後算出所有參數的平均值而且這時候。而且我們所需要的就是不管使用者有多少參數,都可以正確的算出他們的平均值。不過使用不定個數的時機或許不像固定參數個數來得頻繁,很多時候,我們都會使用固定的參數個數,然後確定每個參數的用途。當然這樣的用法有時候會讓人產生一些困擾,尤其在你的程式會被大量重用時(註五),不過要考慮這個問題還需要對Perl有更深入的了解,所以暫時我們就先不討論這種深入的用法。

6.5 副常式中的變數使用
就像大部分人所想的,副常式也是一個區塊,所以有屬於這個區塊自己的變數,也就是副常式的私有變數。不過就如我們所說的,副常式是可以使用程式中的全域變數,就像程式中的其他區塊一般。因此我們只需要在副常式中宣告my變數,也就是定義了副常式的私有變數。那麼就像我們知道的,變數將會維持到這個區塊的結束,也就是你無法在程式的其他地方存取這個變數。
另外,在副常式中,還有一種相當特殊的變數,也就是利用local來定義變數。不過這個部份目前用的人已經非常的少,所以你可以記著副常式裡面有這樣的用法,然後跳過一個部份。而我們打算在這裡提出來的原因是因為各位也許會有機會在某些程式裡面看到這樣的用法,為了避免大家看到這種用法卻又不知道它的作用,我們就在這裡簡單的介紹local的用法,讓大家未來有機會看到時可以能有一些參考的資料。
其實local的用途在於確認某些變數是在副常式中私用的,可是因為副常式會有機會被其他程式引用,所以你無法預期在某個引用的程式之中是否也有名稱相同的變數。因此使用local來確立這是副常式中的私有變數,而如果原來的程式中有相同的變數名稱時,就把主程式的變數放入堆疊,也就是先暫時儲存了主程式的這個變數,然後把相同的變數名稱清空以提供副常式使用。一但離開了副常式之後,Perl就會復原原來被儲存,並且清空的變數了。這樣子看起來,local和my的用法看起來似乎非常接近。
當然,你會發現這跟my之間會有什麼差異呢?我們先來看看這個程式:


$var1 = "global"; &sub1;   # 印出 sub1 print "$var1\n";   # 印出 global &sub2;   # 現在變成 sub2 print "$var1\n";   # 又回到 global sub sub1 { my $var1 = "sub1"; print "$var1\n"; } sub sub2 { local $var1 = "sub2"; print "$var1\n"; }

看起來沒什麼不同,好像兩者之間沒有太大的差別,可是如果我們改寫一下程式︰


$var1 = "global"; $var2 = "for local"; &sub1;   # 印出 local, for local &sub2;   # 印出 global, for local sub sub1 { local $var1 = "local"; my $var2 = "my"; &sub2; } sub sub2 { print "var1=$var1\tvar2=$var2\n"; }

從這裡,我們好像可以發現一些不同。差別就在於當我們先呼叫sub1的時候,sub1會把原來的變數$var1放進堆疊,清空後把新的值"local"放入。而在呼叫sub2的時候,因為還在sub1的區塊內,因此local還佔用著$var1這個變數。所以印出"local"的值,可是使用my就有所不同的。雖然我們在sub1使用了my來定義區域變數$var2,可是my卻不會把佔用原來$var2變數的空間。所以當我們呼叫sub2時,會使用sub2裡的$var2變數。而在sub2裡面因為沒有定義$var2,所以Perl直接叫用全域變數,也就印出了"for local"的字串。

習題:
1. 下面有一段程式,包含了一個陣列,以及一個副常式diff。其中diff這個副常式的功能在於算出陣列中最大與最小數值之間的差距。請試著將這個副常式補上。

#!/usr/bin/perl -w use strict; my @array = (23, 54, 12, 64, 23); my $ret = diff(@array); print "$ret\n";   # 印出 52 (64 - 12) my @array2 = (42, 33, 71, 19, 52, 3); my $ret2 = diff(@array2); print "$ret2\n";  # 印出 68 (71 - 3)
2. 把第四章計算階乘的程式改寫為副常式型態,利用參數傳入所要求得的階乘數。

註一:如果你想要了也更複雜的副常式使用方式,可以參考perldoc perlsub。
註二:就像你弄出了小螺絲釘,你總不希望每次遇到一樣的需要就重作一次螺絲釘。
註三:筆著第一次學Perl的時候就是被預設變數$_打敗的。(XXX 正文中沒有出現)
註四:階乘就是從一乘到某個數,比如4的階乘就是1x2x3x4。
註五:也就是你的副常式被放入模組中,而會不斷被重用時。那麼你固定的參數的個數及順序,一但將來副常式要改寫時,很容易影響過去使用的程式碼,而產生無法正確執行的問題。不過這屬於進階的問題,我們並不在這裡討論。