14 參照 (Reference)
對於一個剛開始使用Perl的使用者來說,要深切的了解參照確實是非常困難的一件事。可是如果不使用參照,你就會發現有很多時候,事情會變得相當複雜。因此大略的了解Perl參照的使用實在是相當重要。不過對於許多從事Perl教學的人來說,Perl的參照是既複雜又困難的事,因此在大約20小時的教學中,並不容易包含參照的使用。所以有些給Perl入門使用者的書籍也會避開這個題目,而在更進階的書籍再專文介紹。可是我以為如果失去了參照的使用,很多相當方便的使用方式都無法被實作,讓人無法領略Perl的方便性。可是要如何以簡單的方式概略的介紹Perl的參照也是另外一項複雜的議題。不過雖然如此,我們還是來嘗試以比較淺顯易懂的方式來介紹參照的常用方式。
14.1 何謂參照
就像C程式語言造成許多程式設計師的困擾一般,參照之於Perl也有類似的效果。當然造成這個狀況的原因大概也是因為參照的抽象觀念也足夠跟C的指標媲美。其實這也許只是危言聳聽,我們應該想辦法讓指標變得更容易使用,而且根據Perl的80/20定律,我們只需要學會其中的百分之二十,就可以應付百分之八十的狀況。
「參照」,其實跟所謂的指標在意義上是非常接近的,也就是某個變數指向另外一個變數。比如我們有一個陣列變數像是這樣:
my @array = (1...10);
那麼我希望使用一個純量變數$ref來表示@array這個陣列時跟怎麼辦呢?這時候,我們只要表明它是一個陣列,而且它被儲存的位置在那裡。我們就可以順著這個線索找到@array這個陣列,而且得到他的內容。所以我們可以用這樣子來表示$ref變數:
ARRAY(0x80a448)
這樣看起來就非常清楚,我們有一個陣列,位址在0x80a448。所以我們可以藉由這樣的資料取得@array這個陣列的詳細資料。這就跟我們在Unix系統下使用檔案連結的方式有點接近,我們透過某個連結資訊來找到被連結的檔案。
而在Perl裡面,所謂的參照,其實確實是建立了另外一個符號,就像儲存一般的變數一樣,不過這次我們得到的是一個純量變數。Perl確實可以使用純量變數來指向任何其他的資料結構,也就是另一個純量變數,陣列或雜湊,就像我們剛剛看到的樣子。
我想我們可以用一個比較簡單的方式來解釋參照,例如妳們家的成員可以組成一個資料結構的型態(你可以使用陣列或雜湊,看你想要儲存的資料內容而定),於是如果想要用一個純量的資料型態來取得家裡成員的內容,那麼我們也許可以用門牌號碼來代表(當然,如果是有戶政單位的作業疏失造成門牌號碼重複等各種錯誤可不在我們討論的範圍內)。
14.2 取得參照
當然,你其實大可不必擔心怎麼找出參照的位址,因為這個部份可以由Perl代勞。我們來看看怎麼取得參照:
my @array = (1...10);
my $ref = \@array; # 取得陣列@array的參照
print $ref;
當然,這個程式的執行結果就會看起來像是我們剛剛寫的樣子,只是位址會有所差異。接下來,我們也可以來看看怎麼利用一個參照來取得被參照的資料結構內容。就以剛剛的例子來看,我們定義了一個陣列@array,然後利用反斜線(\)來取得@array的參照。那麼我們要怎麼取得$ref所參照的@array內容呢?其實我們只需要這麼作就可以了:
print @$ref; # 利用參照找回陣列
很顯然的,我們可以利用純量變數來取得另一個純量變數,或是陣列,或是雜湊的參照。當然,取得的方式都是類似的,所以我們只要這樣:
$scalar = "1...10";
@array = (1...10);
%hash = (1...10);
$scalar_ref = \$scalar;
$array_ref = \@array;
$hash_ref = \%hash;
這樣看起來應該就清楚多了,不過很多人可能還是沒辦法想像這樣的參照能有甚麼很大的用途。其實參照很重要的用途之一,就是在增加資料結構的彈性,或者你也可以說它是增加資料結構的複雜性。我們先來看看一個實例:
@john = (86, 77, 82, 90);
@paul = (88, 70, 92, 65);
@may = (71, 64, 68, 78);
我們現在假設有一個考試,總共考了四個科目,上面的陣列是這三個學生每一科的成績。可是我們如果要針對某個科目,一次取得每個學生的成績就會顯得很麻煩,因為我們可能需要$john[0], $paul[0], $mat[0]這樣的方式。因此在其他程式語言的實作方式則是使用所謂的「多維陣列」,例如你可以定義一個陣列,那麼它的結構會看起來樣這樣子:
grades[0][0] = 86;
grades[0][1] = 77;
grades[0][2] = 82;
.....
.....
grades[2][2] = 68;
grades[2][3] = 78;
很可惜,Perl的陣列並不接受這種方式的定義,也就是並沒有提供所謂的多維陣列的概念。可是卻不能依此推論Perl的資料結構太過簡陋,因為透過參照的方式可以讓Perl的三種資料結構都獲得最充分的運用。所以我們來看看該怎麼使用Perl的參照方式來實作多維陣列。
首先我們還是有三個陣列,分別代表三個學生的各科成績,就像剛剛的例子一般。接下來的重點就是把這三個陣列的參照放進另外一個陣列中,就像這樣:
@grades = (\@john, \@paul, \@may);
所以現在看來,@grades這個陣列中其實已經包含了@john,@paul,@may三個陣列了。這樣就可以實作出一個多維的陣列。各位應該可以想像整個陣列中夾雜著陣列參照的狀況,其實我們可以用圖來觀察,也許會比較容易理解。
<<圖一>>
這個圖裡面,我們其實並不是忠實的表達出資料在電腦記憶體中儲存的形式,不過在概念上卻可以清楚的看出利用參照來表達複雜資料結構的方法。也就是達到過去我們曾經在其他個種程式語言中所使用的多維陣列的方式。當然,在實際的使用上,如果我們想要達到過去利用其他語言做出來的多維陣列的,還是需要一點點小小的轉換,因為我們必須不只一次的在陣列中使用陣列參照,如此一來,如果要取出最後一層的陣列值就會讓人有點頭痛。而不像過去我們使用多維陣列的方式,如果有多維陣列,我們幾乎就只要這麼寫:
array[3][2][4];
既然如此,那麼為甚麼Perl不直接給我們多維陣列就好了呢?首先,在實際運用上,我們最常用的還是以一維跟二維陣列為主,所以利用參照的方式就可以容易的達成這個需求。其次,一但利用參照的方式,我們就可以使用更有彈性的資料結構,而不單只是一個多維陣列。聽起來好像非常神秘,不過其實仔細想想,確實很有道理。還記得嗎?我們說過陣列的元素其實就是一堆的純量所組成的,而且參照本身就是一個純量值,只是利用這個純量值,我們可以取得被參照的資料儲存在記憶體的位置。然後還有一個提示,也就是參照可以用來取得各種在Perl中原來就有的資料結構型式(註一)。說到這裡,也許你已經看出一些端倪,也就是利用參照的方式,你可以把大多數的資料都以純量的方式來表示,因此就有各式各樣的運用方式。下面就是一些我們可能會運用到的方式:
my @array = ('john', 'paul', 'ken');
my %info = ( 'date' => '3/27',
'people' => \@array,
'place' => '台北車站' );
這也許是某一次活動的資料,我們先取得了一個陣列,其中是參加活動的人員。接下來,我們會有或動的其他資料,例如活動的日期,地點。然後我們還希望把參加的人員也一起放入活動資料中,所以我們就使用了雜湊來儲存這些資料,可是雜湊中關於人員這個部份,我們是以陣列參照來表示。這就是另外一個非常典型使用參照來活化資料結構的例子。當然,如果你還有力氣,可以看看更複雜的例子,就像這樣:
my @john_grades = (65, 87, 92, 77, 53);
my %john = ( id => '7821434',
birth => '1983/11/12',
grades => \@john_grades );
......
......
my %students = ( john => \%john,
......
...... );
這個例子顯然可以好好來解釋一下,就像魔術一般,我們用了雜湊跟陣列的多次排列,把學生的資料全部堆在一起了。首先我們先拿到學生的成績,這是一個陣列,而且是最簡單的陣列,所有的成績依序排列在陣列中。接下來,我們要取得某個學生的資料,其中包括了他的個人成績。因此我們使用了一個雜湊來儲存學生的個人資料,而在成績的部份,則是使用了陣列參照。這樣子,我們可以完整的描述一個學生的資料,接下來我們只需要另外一個雜湊來收集所有學生的資料就可以,而在這裡,我們這個雜湊的每個鍵是學生的名字(在假設學生不會同名的狀況下),然後把剛剛取得的單一學生資料取參照,作為這個整合參照的值。
這樣的寫法看起來確實複雜了許多,不過這是因為我們使用了逐步講解的方式,所以把整個過程的詳細的列出來。不過很多時候我們其實會使用匿名陣列或匿名雜湊的方式來表現。那麼就可以讓整個架構看起來容易,也清楚一些。我們把這種匿名陣列/雜湊寫法放在這裡,也許可以讓大家參考一下:
#!/usr/bin/perl
use strict;
my %students = ( john => { id => 'foo', # 這是雜湊的第一對鍵值
tel => '11223344',
grades => [34, 56, 78]},
paul => { id => 'bar', # 第二對鍵值從這裡開始
tel => '223344',
grades => [44, 55, 66]},
);
這樣的寫法顯然乾淨許多,雖然你可能還有點不習慣,不過可以確定的是,至少你不會再看到一大堆的變數名稱,而且每一個部份的相互關係也清楚了不少。當然,這時候你完全可以先忽略這個部份,不過為了讓你之後回過頭來看這一段程式碼時可以了解其中的奧妙之處,我們還是先說明一下這種方式的解讀方式。
首先,我們定義了一個學生的雜湊,這也就是我們最終想要的資料處理方式。接下來,我們看到雜湊%students中包含了兩對的鍵值。其中第一對的鍵就是john,而其相對應的值則是一個雜湊參照。這時候出現了第一個匿名的雜湊,他包含了三對的鍵值,其中的鍵分別是id,tel,grades。而grades對應的值則是一個匿名陣列的參照,這個陣列一共有三個值,分別代表三個科目的成績。因此在雜湊%students的第一對鍵值中,我們包含了兩個參照,分別為一個雜湊參照與另一個陣列參照。接下來的第二對鍵值則是以paul為對應的鍵,並且包含著一個結構相同的值。
好吧,你可以暫時先忘了這麼複雜的部份,至少你暫時應該可以使用最簡單的參照結構來實作一個二維陣列。
14.3 參照的內容
我們剛剛以經學到利用各種方式得到某個資料型態的參照,並且可以把取得的參照值放入其他的資料型態內,組成其他比較複雜的資料儲存形式。可是接下來我們總會在程式當中取出這些值,因此該怎麼解開參照,讓他們指向原來所代表的那一個資料內容呢?
我們先看看這樣一個簡單的參照:
my @aray = qw/John Paul May/; # 一個陣列
my $array_ref = \@array; # 取得這個陣列的參照
接下來,我們用大括號將參照包起來,並且恢復他應該有的資料型態代表符號,在這個例子中就是@號。所以看起來應該像是這樣子:
print @{$array_ref}; # 印出JohnPaulMary
當然,我們也可以利用陣列的方式來取得某個索引的值,也就是這樣:
print ${$array_ref}[1]; # 這樣就跟 $array[1] 一樣
看起來好像不太困難,那我們來依樣畫葫蘆,試試看雜湊參照的解法。當然,還是先建立一個雜湊吧,並且取得他的參照吧:
my %hash = qw/John 24 Paul 30 May 26/;
my $hash_ref = \%hash;
接下來,好像並不困難,我們只要把{$hash_ref}視為一個雜湊變數的名稱,所以要取得雜湊中,雜湊鍵為"John"的值就只需要這麼作:
print ${$hash_ref}{John}; # 果然印出 24
print ${$hash_ref}{Paul}; # 結果是 30
print ${$hash_ref}{may}; # 正如我們的期待,就是 26
當然,你也可以把%{$hash_ref}當成一個一般的雜湊來運作,所以你幾乎可以毫無疑問的這麼使用:
for (keys %{$hash_ref}) {
print ${$hash_ref}{$_}."\n"; # 印出 24, 26, 30
}
你是一個很簡單的例子,我們可以直接把%{$hash_ref}當成一般的雜湊來操作。所以一般使用於雜湊的函數也可以直接用於%{$hash_ref}上,相同的狀況,我們也可以在解開陣列參照之後,用相同的方式來操作。所以如果用剛剛的例子,我們也可以這麼寫:
my @aray = qw/John Paul May/;
my $array_ref = \@array;
for (@{$array_ref}) {
print "姓名:$_\n";
}
14.4 利用參照進行二維陣列
我們在前面已經提過了利用參照來實作二維陣列的方式,可是為甚麼這一小節還要再重新解釋一次呢?主要是因為我們剛剛可以利用參照建立一個簡單的二維陣列,可是我們卻還不知道怎麼能靈活的操作這個陣列。而且利用參照來營造一個二維陣列是非常常見的參照使用方式,所以我們必須再詳細的逐步解釋二維陣列的建構,以及解構。最後並且引申出利用雜湊的值包含陣列參照的運作與利用。
如果你還不熟悉,我們先來建立一個二維陣列。我們假設這是一個日期與氣溫的對照,每天定時量測當地氣溫三次,分別紀錄於陣列中。所以我們以比較繁雜的手續建立起這樣的一個二維陣列:
my @d1 = (24.2, 26.3, 23.4); # 每天的溫度
my @d2 = (23.5, 27.5, 22.6);
my @d3 = (25.2, 28.7, 24.8);
......
......
my @d30 = (19.8, 22.1, 19.2);
my @daily = (\@d1, \@d2, \@d3, ...... , \@d30); # 當月每天的溫度
雖然複雜,不過終於把整個陣列建立起來了。目前我們已經有了30個陣列,各代表了第一天到第三十天中每天的溫度紀錄,接下來就是定義一個陣列包含了這三十個陣列的參照值,而這個陣列也就包含了這個月每天三次溫度的紀錄。於是我們可以利用參照的方式取得某一天的溫度,例如${$daily[4]}[0]就代表了第五天的第一次測量。這次你一定受不了了,這麼複雜的結構並沒有為程式設計師帶來比較舒適的環境,反正讓人徒增困擾。因為我們必須為每一天先建立一個陣列,然後再將陣列參照放入另一個陣列中,接著解參照,取出第二層陣列中的值。
很顯然,如果我們只有這種方式可以使用,那麼負責Perl設計與維護的那些黑客們一定自己先受不了。所以我們的另一個方式就是「匿名陣列」,「匿名雜湊」,而且這個作法我們剛剛已經稍微看過了。現在我們再來了解一下它們的用法。首先在賦值上,陣列所使用的是中括號[],也就是當你在對陣列取值時的符號。而對於匿名雜湊,則是使用大括號{},同樣的,也是利用你對雜湊取值時所用的方式類似。所以剛剛的例子如果重新定義陣列@daily就應該要寫成:
@daily = ([24.2, 26.3, 23.4], [23.5, 27.5, 22.6], [25.2, 28.7, 24.8]...);
看起來好像跟其他程式語言的方式比較接近了,可以取值應該怎麼作呢?還是要先解參照,然後取出陣列的某個值,然後再來解參照嗎?很慶幸的,這種複雜的工作實在不適合用來放在這種可能在日常生活中會大量使用的二維陣列中,因此我們也可以用很方便的方式來取得其中的值。所以要取值的方法就像這樣:
print $daily[2][1];
這樣真的清爽多了,如果你用過其他程式語言的二維陣列,其實大概也都是這樣的寫法。當然,你可以作的絕對不只二維陣列,你可以用同樣的方式來實作多維陣列,就像你可以很容易的造出一個三維陣列。
@demo = ([[2, 4, 5], [3, 2], [2, 6, 7]], [4, 7, 2], [[1, 3, 5], [2, 4, 6]]);
現在回想起來,如果這個陣列要一個一個把名字定義出來,然後取它們的參照,放入其他陣列中......,這實在太辛苦了。於是匿名陣列節省了我們不少的時間,當然,想必也降低了很多錯誤的機會。
11.5 陣列中的參照,參照中的陣列,陣列中的陣列
這個標題實在太繞口令了,雖然我們應該直接取標題為:「匿名雜湊與匿名陣列」,不過這樣的標題好像非常不容易平易近人,所以還是維持這個冗長的標題吧。
在上一節其實已經利用匿名陣列了,也就是我們用來實作二維陣列的輕鬆愉快版本。另外,我們也嘗試過在雜湊裡面放入陣列,可是既然我們可以方便的利用匿名陣列來進行多維陣列的實作,那麼利用類似的方式,把匿名雜湊,匿名陣列的交互使用,顯然可以讓整個資料結構更具有彈性。
還記得我們怎麼整理學生的資料嗎?那時候我們已經用了這樣結構的處理方式。學生的個人資料項目是一個匿名雜湊,而每個學生的成績則是由匿名陣列來組成的。因此我們就可以用簡單的方式來取出需要的值,所以我們就可以這麼用:
print $students{john}{grades}[2];
這樣應該非常方便,你並不需要手動去解參照,或者進行甚麼繁雜的手續。而就像一般的陣列或參照的用法一樣,用中括號來取得陣列的值,或是用大括號才使用雜湊。而匿名雜湊也是常用的方式,它們可能被隱藏在陣列或雜湊中,就像我們剛剛看到學生資料的例子,就是一個「雜湊中的雜湊」實作的例子。
另外很常用的的一種匿名雜湊方式則是陣列中的雜湊,很好的一個例子就是從資料庫擷取出來的資料,這時候我們常常會把每一筆資料依據欄位的名稱,跟所得到值存放在雜湊中,然後將每筆這樣的雜湊存入陣列中所以一個陣列看起來會像是這個樣子:
@data = ( { 'column1' => 'data1',
'column2' => 'data2' },
{ 'column1' => 'data3',
'column2' => 'data4' } );
如果你的資料儲存形式像是這個樣子,在陣列中放入匿名雜湊,那麼你如果要取出某個值,就只需要這麼寫:
print $data[1]{column2}; # 這樣你就可以得到data4
其實你也許不太習慣,為甚麼在使用匿名陣列,或匿名雜湊時,總會有不同於正常指定陣列或雜湊的方式呢?不過我們可以來看看這樣的狀況:
my @array = ((3, 5, 7, 9), (1,4, 8, 6), (2, 5, 4, 2));
這時候,我們知道最外面一層是一個陣列,我們利用串列的方式指定了三個元素給這個陣列,而這三個元素卻都是串列,也就是說,我們希望把這三個串列放入陣列中。可是這時候問題就出現了,因為很明顯的,我們必須在最外層的陣列裡面定義三個變數,才能利用參照的方式把串列放入陣列裡,可是在一般使用的時候,不管陣列或參照,我們都可以使用串列的方式來賦值。像這樣的兩種形式其實都是可能的:
@temp = (3, 5, 7, 9);
%temp = (3, 5, 7, 9);
所以如果我們利用剛剛的方式,希望把三個串列利用匿名陣列或匿名雜湊放進陣列@array的話,就會造成Perl的錯亂,因為它無法清楚的明白你所需要的是匿名的陣列或是雜湊。這也就是你必須清楚的表示你的需求,因此你如果希望使用匿名陣列或雜湊,就必須適當的分別清楚,所以依據你自己的需求,你就必須作不同的定義,就像這樣:
my @array = ([3, 5, 7, 9], [1,4, 8, 6], [2, 5, 4, 2]);
my @array = ({3, 5, 7, 9}, {1,4, 8, 6}, {2, 5, 4, 2});
因為在Perl當中,你都是利用最簡單的陣列,雜湊的資料結構,配合上參照(當然還包括匿名陣列與雜湊)的方式,來組成更複雜的資料結構,例如多維陣列,或是陣列中的雜湊,雜湊中的陣列等等。也就因此,你可以有更大的彈性來玩弄各種結構的組成。比如你可以在陣列中的各個不同的元素裡,擺放不同資料結構的參照,所以你當然可以這麼作:
my @array = ({3, 5, 7, 9}, [1, 4, 8, 6], {2, 5, 4, 2});
所以,你對於這個變數的取值就有可能是:
print $array[0][2]; # 得到的結果是7
print $array[1]{8}; # 這裡會印出6
其實參照的用法並不僅只於這些資料結構上的變化,你還可以取得副常式的參照,當然也可以使用匿名副常式的方式。就像你在使用陣列或雜湊的參照一般。參照的用法非常的靈活,而且運用非常的廣泛,Perl的物件導向寫法也是參照的運用。不過我們不希望剛入門的使用者被大量的參照困擾,所以等各位寫過一陣子的Perl之後可以再去參考其他的Perl文件,了解更多關於Perl參照的用法。
習題:
1. 下面程式中,%hash是一個雜湊變數,$hash_ref則是這個雜湊變數的參照。試著利用$hash_ref找出參照的所有鍵值。
%hash = ( name => 'John',
age => 24,
cellphone => '0911111111' );
$hash_ref = \%hash;
2. 以下有一個雜湊,試著將第一題中的雜湊跟這個雜湊放入同一陣列@array_hash中。
%hash1 = ( name => 'Paul',
age => 21,
cellphone => '0922222222',
birthday => '1982/3/21' );
3. 承上一題,印出陣列$hash_array中每個雜湊鍵為'birthday'的值,如果雜湊鍵不存在,就印出「不存在」來提醒使用者。
註一:其實不單只是這些資料可以取得參照,還有其他部份也可以使用參照來操作,不過我們並不在此討論。