詳談變數


請見 範例 並參考 perldata(1)。 也請先了解有關 l-value 與 r-value 的觀念。

List

凡是用逗點分開的一串純量, 就叫做一個 list。 整個 list 外面可以包上一對小括弧; 也可以省略。 它不是另外一種新的資料形態 (沒有一種變數叫做 list), 只是用手寫出來的資料。 list 可以說是 array 版的 literal。 凡是手冊上提到可以使用 list 的地方, 其實都可以放陣列。 例如 sort LIST 可以對一串純量 (或一個陣列) 排序, 傳回排序的結果; 又如 reverse LIST 可以把一串純量 (或一個陣列) 顛倒過來。 這類函數只要求他們的參數扮演 r-value 的角色, 運算結果以傳回值的方式留下, 並沒有去修改參數本身。 所以如果 @data = (1, 2, 3);@x = (@data, 4, 5, (reverse @data)) 會將 @x 修改成 (1, 2, 3, 4, 5, 3, 2, 1); 但是 @data 還是 (1,2,3)。

反過來說, 可以用陣列的地方, 是否一定也可以用 list 呢? 未必見得。 有些函數要求參數既要扮演 r-value 又要扮演 l-value 的角色, 這種情況手冊就會寫清楚只能用陣列, 例如 pop 函數。 用大家熟悉的觀念作一個比較:

$x = 5; --$x; --5;
$x = "xyz\n"; chomp $x; chomp "xyz\n";
@x = (7,3); pop @x; pop (7,3);

上例中 pop (7,3); 是錯誤的, 錯誤的原因和 --5;chomp "xyz\n"; 一樣。 Literal 只能當做 r-value (提供值) 而不能當做 l-value (提供空間來裝值); 但是 --, chomp, pop 等等 operators/functions 要求其 operands/arguments 扮演 l-value 的角色。 用白話文來說, 這些 operators/functions 具有破壞性 (destructive), 必須把算完的結果存入一個空間, 而 literals 無法提供一個可以裝值的空間。

List 沒有層次: 一個 list 外面不論加了多少對小括弧 (或是完全沒有小括弧), 都還是同樣的東西; 兩個 lists 併在一起寫, 會構成一個更長的 list, 而不是一個多層次的 list。 也就是說 @data = ((("Slow"), "and", ("sure")), "wins", ("the", "race"));@data = ("Slow", "and", "sure", "wins", "the", "race") 是一樣的。

所以 perlfunc(1) 當中解釋每個函數語法的地方, "LIST" 其實可以不只是一個參數, 而是「一串由逗點分開的許多純量及陣列」。 這些內建函數需要 LIST 的地方, 小括弧可有可無。 例如 printf "%d + %d = %d", 2 , 3, 5;printf("%d + %d = %d", 2, 3, 5); 效果一樣。 當然, 語法上不該逗點的地方, 使用時就不可以多給逗點, 否則會被 perl 誤以為是後面 LIST 的一部分, 例如 printf STDOUT, "%d + %d = %d", 2, 3, 5; 一句當中, STDOUT 後面誤加的逗點, 會讓 perl 誤以為 STDOUT 是 format 字串 (而不是 file handle)。

最簡單的 array 與 hash 初始值設定的語法, 右邊就是一個 list。 在一個 list 當中, => 和逗點的效果一樣 (小小差別: => 會自動 quote 左邊的字串)。 所以 @weekdays = ("Mon" => "Tue", "Wed" => "Thu");%days = ("Jan", 31, "Feb", 28); 雖然看起來很奇怪, 但都是合法的句子。 Q: %h = reverse @a; 這句話有什麼效果呢?

range operator: @t = 5..8;@t = (5, 6, 7, 8) 的簡寫。

repetition operator: @t = (9,8,7,6) x 3; 會使 @t 變成 (9,8,7,6,9,8,7,6,9,8,7,6)

[list 對拷時, 元素對號入座] 圖案 利用 list 語法, 可以把一串元素拷貝給一串元素。 Perl 會按對應順序拷貝, 各元素對號入座。 但你可以想像所有元素的舊值會被事先備份出來, 所以不會有孰先孰後, 舊值不見的問題。 例如要交換兩個元素可以用: ($x, $y) = ($y, $x); 又, 左邊 list 當中, 第一個出現的 array 變數, 會把右邊剩下的所有值 "吃掉", 所以通常左邊最多只有一個 array 變數, 且通常都出現在最後。 例如 ($x, $y, @z) = (@p, @q); 的效果相當於 $x=$p[0]; $y=$p[1]; @z = (@p[2..$#p], @q);

用 regexp 搜尋一整批字串時, 先前介紹的 $1, $2, ... 語法不太美觀:

       $str =~ m#(\w+)/(\d+)/(\d+);
        $mon = $1;
        $day = $2;
        $year = $3;

  
變數沒有名字, 程式不易閱讀; 且 $1, $2, ... 只能保留到下次再用 regexp 之前, 所以要趕快用有名字的變數把它們記起來。 現在既然學了 list, 就可以用一個比較簡潔的語法, 直接把比對結果指定給一個 list, 像這樣: ($mon, $day, $year) = $str =~ m#(\w+)/(\d+)/(\d+)#; 甚至可以把比對結果指定給一個陣列, 這樣就可以在不知道會比對到幾個 「我有興趣的字串」 的情況下, 取得所有字串。 這種用法通常與 {m,n} 計數樣版或 g 選項配合使用, 例如: perl -ne 'print "$.: ",join(",",@x),"\n" if (@x = m/(\d+)/g)' 檔名 注意 (1) 這裡的 = 並沒有打錯。 那麼比對字串用的 =~ 跑到那裡去了呢? 其實是連同 $_ 一起省略掉了。 (2) 這句 if 同時有 「查詢是否比對到」 及 「記下比對到的字串」 雙重效果, 也是常用句型。

Array slice (陣列切片?)

假設有 @data 這個 array , 則 ($data[3], $data[0], $data[4]) 可以簡寫為 @data[3, 0, 4] 語法解釋: 我要從名叫 data 的變數中拿元素出來, 所以用 ... data ... ; 因為 data 是一個 array, 所以後面用中括弧; 而拿出來的結果是好幾個元素, 構成一個 array, 所以前面用 @。

假設有 %data 這個 hash , 則 ($data{"Feb"}, $data{"May"}, $data{"Nov") 可以簡寫為 @data{"Feb", "May", "Nov"} 解釋: 我要從名叫 data 的變數中拿元素出來, 所以用 ... data ... ; 因為 data 是一個 hash, 所以後面用大括弧; 而拿出來的結果是好幾個元素, 構成一個 array, 所以前面用 @

Q: 以下各運算式之間有什麼關係? $x, @x, %x, $x[3], $x{"3"}, @x[0, 3], @x{"0", "3"}

Q: 假設有 %x = (Jan=>12, Feb=>-5, ..., Nov=>8, Dec=>37); 請用 array slice 的語法及 list 對拷的語法, 一句話 同時將 Jan 與 Dec 所對應的值對調, 並將 Feb 與 Nov 所對應的值對調。 答案在網頁原始碼當中。

Q: 我們想將 @x 的每個元素往前移兩位, 而最前面兩個元素則繞到最後面去。 請利用 array sliace 語法一句話就完成。

變數有沒有值? hash 有沒有這個 key?

[hash 內的元素是否存在/有值?] 圖案

在 perl 裡面, 可以用 if ($x) 去詢問某變數的值, 如果 $x 裡面是 0 或空字串 "" 或空的 list (), 那麼答案都是 false; 其他數字或字串都是 true。 查詢 if (@x) 時, 如果 @x 是空的 list, 或是裡面所有元素都是 0 或空字串, 那麼答案也是 false。

如果變數根本就沒有設定過初始值, 那麼 perl 就會稱它裡面含的是 undef, 意思就是沒有定義 (undefined)。 下 perl -we 'print $x+$x,"\n"' 所看到的錯誤訊息 "Use of uninitialized value ..." 指的就是這種狀況。 如何判斷一個變數 $x 裡面有沒有值呢? 可以用 if (defined($x)) 另外, 有時就是想把已有值的變數的內容完全清除乾淨 (不想存 0, 也不想存空字串, 什麼值都不想存), 這時可以下 undef($x);$x = undef;

一個 hash %x 裡面的元素, 我們除了可以對它問以上的問題 ("它的值是 true 還是 false?" if ($x{"dog"}) 或 "它是不是連個值都沒有?" if (defined($x{"dog"}))) 還可以問另外一個問題: "hash 裡面有這個 key 嗎?" if (exists($x{"dog"})) 如果連 key 都不存在, 就不必談有沒有傎, 更不必談它的值是 true 還是 false 了。 另外, 如果就是想把既有的 key 刪掉 (當然也就連同它的 value 一起刪掉) 可以用 delete($x{"dog"});

例: ++$freq{"this"} 第一次執行時, 歷經三種狀態: 一開始 exists $freq{"this"} 為 false; 然後 exists $freq{"this"} 為 true 而 defined $freq{"this"} 為 false; 最後兩者皆為 true。

Reference

用 "\" 可取得一個變數的位址。 例: perl -e 'print \$_, "\n"' 又例: perl -e 'print \@ARGV, "\n"' 又例: perl -e 'print \%ENV, "\n"'

不論是純量, 陣列或是 hash, 任何一個變數的位址只佔一個純量的大小。 把某個變數 (可以是純量, 陣列或是 hash) 的位址存到純量 $x 去, $x 就成了一個 reference variable。 也不是字串, 而是另外一個變數的位址。

reference 變數的內容不是字串也不是數字, 但一樣可以印出來。 Perl 會告訴你這個變數 "指到" 記憶體的什麼地方去, 還有被指到的這個地方存放的是什麼樣的變數 (純量, 陣列或是 hash)。

熟悉 c/c++ 的讀者請注意: perl 的 reference variable 相當於 c/c++ 當中的 pointer variable; 與 c++ 當中的 reference variable 不同。 圖示 reference 變數

深入探討 reference 之前, 再次提醒: 看到 $x 有兩種可能的解釋: 要取值出來, 或是要裝東西進去; 看到 \$x 則一律是要取位址。

簡單 reference 想法: 若 $abc = \$xyz, 則以下每當 $abc 出現時, 都把它想成是 xyz。 參考到 array 或 hash 的 reference 亦同。 差不多可以說 \ 和 $ 互相抵消, \ 和 @ 互相抵消, \ 和 % 互相抵消。 例如 $abc = \@xyz; 則 @xyz 陣列最後一個元素的註標可用 $#$abc 取得, 而 @xyz 的最前面元素可以用 $$abc[0] 取得。 如果沒有把握, 就寫 ${$abc}[0]

分析/表達複雜的 reference 時, 用大括號 { ... } 而不是用小括號 ( ... ) 來表示運算順序的先後。

"箭頭表示法": "... 所指到的 array 內的 ... 這個元素" 可用 ...->[...] 表示; "... 所指到的 hash 內的 ... 這個元素" 可用 ...->{...} 表示。 ("一次跳兩步" 的思考方式)

(無名陣列) 用方括號可直接產生一個 anonymous array, 這個 array 的 reference 就放在 [ ... ] 出現的地方; (無名 hash) 用大括號可直接產生一個 anonymous hash, 這個 hash 的 reference 就放在 { ... } 出現的地方 。 例如右圖可由下句產生: $x = { "Mon"=>[1,2,3,4], "Tue"=>[5,6], "Wed"=>[7,8,9] };

Q: 下圖中紅色框圈起來的四塊, 分別要如何稱呼? (請寫 perl 算式) 圖示 reference 變數 (問題) 答案在網頁原始碼當中。

複雜的資料結構可用層層疊疊的無名 array 或 hash 來表示。 一連串的 -> 可以只寫第一個, 其餘全部省略。 請參考 sched 範例。

perllol(1) 內有很多例子; 也請參考 perlref(1)。 遇到複製的變數, 一層又一層的 anonymous arrays 與 anonymous hashes, 建議務必畫圖!

複製 reference 變數時要注意: 如果沒有仔細思考, 可能會變成 shallow copy, 產生出 "雙頭怪"。 需要完全複製出一份獨立的資料 (稱為 deep copy, "深層拷貝") 時, 可以考慮用 CPAN 的 Storable 模組。

作業

  1. sched 的輸出改成一個 html table。
  2. 請回答 範例 最後面的問題。
  3. Q: 這句話會產生什麼資料結構? @x = ({Apple=>3, Banana=>6, Guava=>4}, ["Mango", "Orange", "Cherry"]); 請畫圖表示。 並請試著取出其中部分元素或部分陣列/hash。