[Lua] 敘述-Lua 新手村 (4)

還記得我們曾經在新手村 (1) 提到 chunk(組塊)這個詞嗎?當時還提到,所謂 chunk 是由很多 statement(敘述)組合在一起的,而那時我對敘述的解釋是,一行程式碼就是一個敘述,但其實,這個說法不太精確,而我們這一篇文章將會來談談 Lua 中有哪些敘述。
  • 敘述 Statement
 敘述又稱作陳述式,所有敘述都會造成一個效果,而這個效果要視我們使用了哪一種敘述而定。在 Lua 中,敘述分為:指定、local 宣告、block、控制結構、函式宣告及呼叫,還有 return、break 敘述1
 而控制結構裡包含了 if、while、repeat、for 等敘述,是我們這篇的主角。
 註1:Lua 5.2 裡還多了 goto 敘述。
  • 指定 Assignment
 指定又稱指派,是改變變數值最基本的方法。普通的指定我們已經看過很多了,接著來看看 Lua 裡所謂的「多重指定」:
a, b = 12, 78
a
12
b
78
 我們可以一次把多個值指定(分配)給多個變數,像上例這樣。我們可以利用這個特性,寫出簡潔有力的兩變數交換:
a, b = 12, 78
a, b = b, a
print(a, b)
78    12
 當然,也可以對 table 的欄位(field)進行這樣的操作:
ary = { hi=19, sky=36 }
ary['hi'], ary['sky'] = ary['sky'], ary['hi']
ary['hi']
36
ary['sky']
19
 不過,如果左方變數跟右邊值的數量不一樣的話呢?
a, b = 1, 2, 3, 4  -- 3、4 被丟棄 (discard)
print(a, b)
1    2
a, b, c, d = 7, 8  -- c、d 補 nil
print(a, b, c, d)
7    8    nil    nil
 多出來的值被丟棄,多出來的變數被補 nil,請牢牢記住這個規則,這在我們之後講到函式時很重要。
  • Local 宣告與區塊(Block)
 我們之前所使用的變數都是屬於全域(global)變數,也就是說,無論在哪裡我們都可以存取他們,而他們也不會主動消失。這有什麼不好嗎?全域的變數對我們來說很難掌控,因為我們很難去找出所有使用到他們的地方,進而很難追蹤某個變數的使用記錄,也無法確定某個變數目前儲存的值是不是我們所期望的,甚至,還會有名稱衝突的問題。
 為了解決這類問題,Lua 提供了區域(local)級的變數,使用方法很簡單,第一次使用某個變數時在最前面加上 local,而只要他所屬的區塊結束後,這個變數就會不見了。
do
    local i = 100
    print(i)  -- 100
end
print(i)  -- nil
 我們可以使用 do ... end 來刻意製造一個區塊,並且看到了在這裡的 i 只有生存在 do ... end 裡面,在外面存取 i 的話會得到 nil。此外,這個範例是開一個檔案來寫的。 但是如果在區塊外面原本就有一個同名的變數呢?
local i = 9  -- 他的區塊是這整個 Chunk
do
    print(i)  -- 9 (外面的 i)
    local i = 100
    print(i)  -- 100
end
print(i)  -- 9
 應該不難理解,不過,第一行也用了一個 local 是什麼意思呢?他好像沒有在區塊裡啊。其實,一個 chunk 就是一個區塊,所以其實就算沒有用 do ... end,原本就會有一個由 chunk 所造成的區塊了。

 接著注意一個地方,如果你在互動模式裡這麼寫:
local k = 9
print(k)
nil
 什麼?為什麼 k 會是 nil 呢?其實是因為互動模式中,每行就是一個獨立的區塊,所以一行結束相當一個區塊的結束。 另外,我們可以預先宣告 local 變數而不給他值,這告訴 Lua 說這個變數是區域級的,也就是屬於目前這個區塊,這也有助於避免誤用到區塊外的同名變數或不小心宣告成全域變數。
i = 9
do
    local i  -- 預先宣告,其值為 nil
    print(i)  -- nil
    local i = 100
    print(i)  -- 100
end
print(i)  -- 9
 最後,有一個 Lua 慣用用法:
local data = data  -- 左邊是 local 的變數,右邊是區塊外的變數
 這可以把區域變數初始化成外部同名變數的值,不過在使用 table 時要小心,畢竟傳遞 table 就只是傳遞參考。
 接著我們要來看看所謂的流程控制結構,而他們本身也都各自形成一個區塊。
  • 控制結構(Control Structure)
 Lua 提供了這幾種控制結構:if / else、while、repeat、for。第一個是條件判斷用的,其他三個是迴圈,而其中 for 還有迭代的功能。
  • 要走哪條路:if / else
 if 的語法如下:
if condition then
    -- 做一些事 (1)
end

if condition then
    -- 做一些事 (1)
else
    -- 做另一些事 (2)
end
 如果 condition 是條件真,則會進到 then 部分,也就是上標註 (1) 的地方;如果是條件假,就會進入 else 部分,也就是 (2),若是沒有 else 部分,像上面的第一種型式,則會直接跳過他。
 如果想要用不只一個條件來決定程式的走向,可以使用 elseif:
if condition1 then
    -- 做一些事 (1)
elseif condition2 then
    -- 做另一些事 (2)
else
    -- 做其他事情 (3)
end
 跟上面很類似,if 會從第一個條件開始檢查,一路找到條件真,再進入相對應的 then 部分,或沒有一個條件為真,就會進入 else 部分。與上面相似的,else 部分也可以省略。
 來看個例子:
score = io.read('*n')
if score >= 90 then
    print('A')
elseif score >= 80 then
    print('B')
elseif score >= 70 then
    print('C')
elseif score >= 60 then
    print('D')
else
    print('F')
end
  • 重複做對的事:while
 while 的語法如下:
while condition do
    -- 做一些事
end
 while 會檢查 condition 是不是條件真,如果是,則進入 do 區塊,do 區塊做完之後,while 會再一次檢查 condition,如果一樣是條件真,那就再做一次 do 區塊裡的事,一直反覆這樣的流程,直到 condition 變為條件假才結束這個 while。
  • repeat ... until
 repeat 的語法如下:
repeat
    -- 做一些事
until condition
 這個結構的意思是,先做 repeat ... until 區塊的事,再檢查 condition 是否為條件真,如果是,則離開迴圈,如果不是,就再進入一次 repeat ... until 區塊,這在進行輸入的判斷很有用,像這個程式:
repeat
    input = io.read()
until input ~= ""
print(input)
 這可以反覆讀取輸入,直到輸入不是空行為止。另外請注意,until 後的 condition 也在 repeat ... until 區塊內,也就是說,如果有一個在 repeat 和 until 間的 local 變數,則這個變數也可以在 condition 使用。
  • for 的第一型式:數值 for
 先來看看 for 的語法:
for var = start, stop, step do
    -- 做一些事
end
 var 是驅動 for 迴圈的變數,首先 Lua 會把 start、stop、step 求出值來,再將 start 設為 var 的初始值,並開始 for 迴圈本體。進行方法是每次進入迴圈本體時先檢查 var 是否大於 stop,若是,則結束 for;若小於等於 stop,則進入本體,當本體的程式執行完後,把 var 加上 step,再回到 for 開頭檢查是否大於 stop,如此反覆地進行下去。另外,如果沒有提供 step,預設值會是 1。
 來看一些例子:
for i = 1, 10 do print(i) end  -- 從 1 數到 10
for i = 10, 1, -1 do print(i) end  -- 從 10 倒數回 1
 從這個例子也可以看到其實區塊是可以縮成一行的,只要加上適當的空格即可。 接著談談較為細節的部分:start、stop、step 是我們前一篇文章講過的運算式,因此可以放入任何合法的運算式,如函式的呼叫,且就像我上面提到的,這三個運算式會在本體開始前就算好,因此只會執行一次運算,若是函式也只會執行一次;第二,var 的前面雖然沒有 local,但是 Lua 會自動把他宣告成 local,所以在 for 結束後就不能存取他了;最後,請不要在本體中改變 var 的值,這會造成不可預測的結果。
  • for 的第二型式:通用(Generic)for
 這種 for 跟第一型的很類似,只是通用 for 不是在數值間變化,而是在由一個迭代器(iterator)函式所提供的結果之間變化,這又稱做「迭代(iterate)」或「走訪(traverse)」,看起來好像很複雜,不過使用起來簡潔又有力。
 那什麼是迭代器函式(以下簡稱迭代器呢?Lua 本身就提供了一些好用的迭代器,如 ipairs()、pairs()、string.gmatch()、io.lines() 等,不過我們只會先使用前兩個。
    • ipairs():陣列的走訪
 我們來看看他的用法:
ary = { 'a', 'b', 'c', 'd', 'e' }
for i, v in ipairs(ary) do
    print(i, v)
end
-- 1    a
-- 2    b
-- 3    c
-- 4    d
-- 5    e
 可以看到,我們用了兩個變數,i 和 v,並透過關鍵字 in 來接收 ipairs() 所回傳的值,而我們也不難看出來,ipairs() 會回傳一個 table 的數值索引和其中的值。接著看看他的好朋友,pairs()。
    • pairs():關聯陣列的走訪
 pairs() 會回傳一個 table 的 key 和其中的值:
ary = { 'a', 'b', moon='lua', sun='sol' }
for k, v in pairs(ary) do
    print(k, v)
end
-- 1      a
-- 2      b
-- moon   lua
-- sun    sol
 可以看到,除了數字索引,非數值的 key 也可以透過 pairs() 取得。
 另外,通用 for 的 in 就好像前面提到的多重指定,所以也可以這麼寫:
ary = { 'a', 'b', moon='lua', sun='sol' }
for k in pairs(ary) do  -- 把值拋棄
    print(k)
end
-- 1
-- 2
-- moon
-- sun
 還有一個小技巧是使用 pairs() 來建構反向 table:
ary = { moon='lua', sun='sol' }
rev = {}
for k, v in pairs(ary) do
    rev[v] = k
end
-- 現在 rev 是 { lua="moon", sol="sun" }
 最後請注意,與數值 for 相同,for 宣告的變數是 local 的,而且不應在本體內改變 for 所宣告的變數。關於迭代器和通用 for,之後會有更深入的探討。
  • break 和 return
 break 和 return 可以用來跳出特定區塊。break 可以提早結束一個迴圈,例如:
for i = 1, 10 do
    print(i)
    if i == 3 then
        break
    end
end
-- 1
-- 2
-- 3
 這會使得 for 提前在 i == 3 時就結束。當然,break也可以用在 while 迴圈、repeat ... until 上。
 而 return 是用來結束一個函式,並將回傳值傳出函式的語法,如:
function double(x)
    return x * 2
end
print(double(12))
-- 24
 我們會在下一篇文章了解函式的用法。要特別注意的是,break 和 return 只能是所在區塊的最後一個敘述2,像這樣就不行:
for i = 1, 10 do
    print(i)
    if i == 3 then
        break
        print('end')
    end
end
-- [string "<eval>"]:5: 'end' expected (to close 'if' at line 3) near 'print'
 解決方法是將 break 用 do ... end 包起來,這樣 break 就是 do ... end 區塊的最後一個敘述了:
for i = 1, 10 do
    print(i)
    if i == 3 then
        do break end
        print('end')
    end
end
 註2:Lua 5.2 開始,這個限制只有 return 有,且在 Zerobrane 中的 Lua 5.1 (似乎)也只有 return 有限制。

 這篇文章把重要的流程控制結構完整地介紹過了,其中通用型 for 之後還會再遇到,不過了解這些結構後對小型程式就已經很足夠了;而在下一章我們會看到讓程式條理更清晰、更簡潔的技術:函式。


後記:這次 大 拖 稿。我想之後的情況可能也會差不多XD

留言

這個網誌中的熱門文章

[C] 每天來點字串用法 (2) - strcpy()、strncpy()

[Python] *args 和 **kwargs 是什麼?一次搞懂它們!

[C] 每天來點字串用法 (5) - strcat()、strncat()