詳談變數


請見 範例 並參考 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。

Perl 語言

  1. 新手上路
  2. 基本要素
  3. 餵資料
  4. 常用句型
  5. regexp
  6. 詳談變數
  7. 一語中的
  8. 副程式
  9. 模組
  10. 外界對話

附錄

  1. 參考資料
  2. scripting
  3. Windows
  4. 圖形介面
  5. big-5 碼