8 補充知識

8.1 局部變量和代碼塊

Lua 語言中的變量在默認情況下是全局變量,所有的局部變量在使用前必須聲明。與全局變量不同,局部變量的生效范圍僅限于聲明它的代碼塊。一個代碼塊是一個控制結構的主體,或是一個函數(shù)的主體,或是一個代碼段(即變量被聲明時所在的文件或字符串):

x = 10
local i = 1

while i <= x do
  local x = i * 2
  print(x)
  i = i + 1
end

if i > 20 then
  local x
  x = 20
  print(x + 2)
else
  print(x)
end

print(x)

請注意,上述示例在交互模式中不能正常運行。因為在交互模式中,每一行代碼就是一個代碼段。一旦輸入示例的第二行,Lua 語言的解釋器就會直接運行它并在下一行開始一個新的代碼段。這樣,局部的聲明就超出了原來的作用范圍。解決這個問題的一種方式就是顯式地聲明整個代碼塊,即將它放入一對 do - end 中。一旦輸入了 do,命令就會只在遇到匹配的 end 時才結束,這樣 Lua 語言解釋器就不會單獨執(zhí)行每一行的命令。
當需要更好地控制某些局部變量的生效范圍時,do 程序塊也同樣有用:

local x1, x2
do
  local a2 = 2 * a
  local d = (b ^ 2 - 4 * a * c) ^ (1 / 2)
  x1 = (-b + d) / a2
  x2 = (-b - d) / a2
end
print(x1, x2)

盡可能地使用局部變量是一種良好的編程風格。首先,局部變量可以避免由于不必要的命名而造成全局變量的混亂;其次,全局變量還能避免同一程序中不同代碼部分中的命名沖突;再次,訪問局部變量比訪問全局變量更快;最后,局部變量會隨著其作用域的結束而消失,從而使得垃圾收集器能夠將其釋放。
鑒于局部變量優(yōu)于全局變量,有些人就認為 Lua 語言應該把變量默認視為局部的。然而,把變量默認視為局部的也有一系列問題(例如非全局變量的訪問問題)。一個更好的解決辦法并不是把變量默認視為局部變量,而是在使用變量前必須聲明。Lua 語言的發(fā)行版中有一個用于全局變量檢查的模塊 strict.lua,如果試圖在一個函數(shù)中對不存在的全局變量賦值或者使用不存在的全局變量,將會拋出異常。這在開發(fā) Lua 語言代碼時是一個良好的習慣。
局部變量的聲明可以包含初始值,其賦值規(guī)則與常見的多重賦值一樣:多余的值被丟棄,多余的變量被賦值為 nil。如果一個聲明中沒有賦初值,則變量會被初始化為 nil:

local a, b = 1, 10
if a < b then
  print(a)
  local a
  print(a)
end
print(a, b)

Lua 語言中有一種常見的用法:

local foo = foo

這段代碼聲明了一個局部變量 foo,然后用全局變量 foo 對其賦初值(局部變量 foo 只有在聲明之后才能被訪問)。這個用法在需要提高對 foo 的訪問速度時很有用。當其他函數(shù)改變了全局變量 foo 的值,而這段代碼又需要保留原始值時,這個用法很有用,尤其是在進行運行時動態(tài)替換時。即使其他代碼把 print 動態(tài)替換成了其他函數(shù),在local print = print語句之前的所有代碼使用的都還是原先的print函數(shù)。
有些人認為在代碼塊的中間位置聲明變量是一個不好的習慣,實際上恰恰相反:我們很少會在不賦初值的情況下聲明變量,在需要時才聲明變量可以避免漏掉初始化這個變量。此外,通過縮小變量的作用域還有助于提高代碼的可讀性。


8.2 控制結構

Lua 語言提供了一組精簡且常用的控制結構,包括用于條件執(zhí)行的 if 以及用于循環(huán)的 while、repeat 和 for。所有的控制結構語法上都有一個顯式的終結符:end 用于終結 if、for 及 while 結構,until 用于 repeat 結構。
控制結構的條件表達式的結果可以是任何值。請記住,Lua 語言將所有不是 false 和 nil 的值當作真(特別地,Lua 語言將 0 和空字符串也當作真)。

8.2.1 if then else

if 語句先測試其條件,并根據條件是否滿足執(zhí)行相應的 then 部分或 else 部分。else 部分是可選的。

if a < 0 then a = 0 end

if a < b then return a else return b end

if line > MAXLINES then
  showpage()
  line = 0
end

如果要編寫嵌套的 if 語句,可以使用 elseif。它類似于在 else 后面緊跟一個 if,但可以避免重復使用 end:

if op == "+" then
  r = a + b
elseif op == "-" then
  r = a - b
elseif op == "*" then
  r = a * b
elseif op == "/" then
  r = a / b
else
  error("invalid operation")
end

由于 Lua 語言不支持 switch 語句,所以這種一連串的 else-if 語句比較常見。

8.2.2 while

顧名思義,當條件為真時 while 循環(huán)會重復執(zhí)行其循環(huán)體。Lua 語言先測試 while 語句的條件,若條件為假則循環(huán)結束;否則,Lua會執(zhí)行循環(huán)體并不斷地重復這個過程。

local i = 1
while a[i] do
 print(a[i])
 i = i + 1
end

8.2.3 repeat

顧名思義,repeat-until 語句會重復執(zhí)行其循環(huán)體直到條件為真時才結束。由于條件測試在循環(huán)體之后執(zhí)行,所以循環(huán)體至少會執(zhí)行一次。

local line
repeat
  line = io.read()
until line ~= ""
print(line)

和大多數(shù)其他編程語言不同,在 Lua 語言中,循環(huán)體內聲明地局部變量的作用域包括測試條件:

-- 使用 Newton-Raphson法計算 'x' 的平方根
local sqr = x / 2
repeat
  sqr = (sqr + x / sqr) / 2
  local error = math.abs(sqr^2 - x)
until error < x /10000

8.2.4 數(shù)值型 for

for 語句有兩種形式:數(shù)值型 for 和泛型 for。
數(shù)值型 for 的語法如下:

for var = exp1, exp2, exp3 do
  --somthing
end

在這種循環(huán)中,var 的值從 exp1 變化到 exp2 之前的每次循環(huán)會執(zhí)行 something,并在每次循環(huán)結束后將步長 exp3 增加到 var 上。第三個表達式 exp3 是可選的,若不存在,Lua 語言會默認步長值為 1.如果不想給循環(huán)設置上限,可以使用常量 math.huge:

for i = 1, math.huge do
  if(0.3 8 i ^ 3 - 20 * i ^ 2 - 500 >= 0) then
    print(i)
    break
  end
end

為了更好地使用 for 循環(huán),還需要了解一些細節(jié)。首先,在循環(huán)之前,三個表達式都會運行一次;其次,控制變量是被 for 語句自動聲明的局部變量,且其作用范圍僅限于循環(huán)體內。一種典型的錯誤是認為控制變量在循環(huán)結束后仍然存在:

for i = 1, 10 do print(i) end
max = i

如果需要在循環(huán)結束后使用控制變量的值(通常在中斷循環(huán)時),則必須將控制變量的值保存在另一個變量中:

local found = nil
for i = 1, #a do
  if a[i] < 0 then
    found = i
    break
  end
end
print(found)

最后,不要改變控制變量的值,隨意改變控制變量的值可能產生不可預知的結果。如果要在循環(huán)正常結束前停止 for 循環(huán),那么可以參考上面的例子,使用 break 語句。

8.2.5 泛型 for

泛型 for 遍歷迭代函數(shù)返回的所有值,例如我們已經在很多示例中看到過的 pairs、ipairs 和 io.lines 等。雖然泛型 for 看似簡單,但它功能非常強大。使用恰當?shù)牡骺梢栽诒WC代碼可讀性的情況下遍歷幾乎所有的數(shù)據結構。
當然我們也可以自己編寫迭代器。盡管泛型 for 的使用很簡單,但編寫迭代函數(shù)卻有不少細節(jié)需要注意。我們會在后續(xù)的第 18 章中繼續(xù)討論該問題。
與數(shù)值型 for 不同,泛型 for 可以使用多個變量,這些控制變量是循環(huán)體中的局部變量,我們也不應該在循環(huán)中改變其值。


8.3 break、return 和 goto

break 和 return 語句從當前的循環(huán)結構中跳出,goto 語句則允許跳轉到函數(shù)中的幾乎任何地方。
我們可以使用 break 語句結束循環(huán),該語句會中斷包含它的內層循環(huán);該語句不能在循環(huán)外使用。break 中斷后,程序會緊跟著被中斷的循環(huán)繼續(xù)執(zhí)行。
return 語句用于返回函數(shù)的執(zhí)行結果或簡單地結束函數(shù)的運行。所有函數(shù)的最后都有一個隱含的 return,因此我們不需要在每一個沒有返回值的函數(shù)最后書寫 return 語句。
按照語法,return 只能是代碼塊中的最后一句:換句話說,它只能是代碼塊的最后一句,或者是 end、else 和 until 之前的最后一句。例如,在下面的例子中,return 是 then 代碼塊的最后一句:

local i = 1
while a[i] do
  if a[i] == v then return i end
  i = i + 1
end

通常,這些地方正是使用 return 的典型位置,return 之后的語句不會被執(zhí)行。不過,有時在代碼塊中間使用 return 也是很有用的。例如,在調試時我們可能不想讓某個函數(shù)執(zhí)行。在這種情況下,可以顯式地使用一個包含 return 的 do:

function foo()
  return
  do return end
  -- other statements
end

goto 語句用于將當前程序跳轉到相應的標簽處繼續(xù)執(zhí)行。goto 語句一直以來備受爭議,至今仍有許多人認為它們不利于程序開發(fā)且應該在編程語言中禁止。不過盡管如此,仍有很多語言處于很多原因保留了 goto 語句。goto 語句有很強大的功能,只要足夠細心,我們就能夠利用它來提高代碼質量。
在 Lua 語言中,goto 語句的語法非常傳統(tǒng),即保留字 goto 后面緊跟著標簽名,標簽名可以是任意有效的標識符。標簽名可以是任意有效的標識符。標簽額語法稍微有點復雜:標簽名稱前后緊跟兩個冒號(形如 ::name:: )。這個復雜的語法是有意而為的,主要是為了在程序中醒目地突出這些標簽。
在使用 goto 跳轉時,Lua 語言設置了一些限制條件。首先,標簽遵循常見的可見性規(guī)則,因此不能直接跳轉到一個代碼塊中的標簽(因為代碼塊中的標簽對外不可見)。其次,goto 不能跳轉到函數(shù)外。最后,goto 不能跳轉到局部變量作用域。
關于 goto 語句經典且正確的使用方式請參考其他一些編程語言中存在但 Lua 語言中不存在的代碼結構,例如 continue、多級 break、多級 continue、redo和局部錯誤處理等。continue 語句僅僅相當于一個跳轉到位于循環(huán)體最后位置處標簽的goto語句,而 redo 語句則相當于跳轉到代碼塊開始位置的 goto 語句:

while some_condition do
  ::redo::
  if some_other_condition then goto continue
  elseif yet_ another_condition then goto redo
  end
  -- some code
  ::continue::
end

Lua 語言規(guī)范中一個很有用的細節(jié)是,局部變量的作用域終止于聲明變量的代碼塊中的最后一個有效語句處,標簽被認為是無效語句。下列展示了這個實用的細節(jié):

while some_condition do
  if some_other_condition then goto continue end
  local var = something
  -- somecode
  ::continue::
end

讀者可能認為,這個 goto 語句跳轉到了變量 var 的作用域內。但實際上這個 continue 標簽出現(xiàn)在該代碼塊的最后一個有效語句后,因此 goto 并未跳轉進入變量 var 的作用域內。
goto 語句在編寫狀態(tài)機時也很有用。示例 8.1 給出了一個用于檢驗輸入是否包含偶數(shù)個 0 的程序。

示例 8.1 一個使用 goto 語句的狀態(tài)機的示例

::s1:: do
  local c = io.read(1)
  if c == '0' then goto s2
  elseif c == nil then print 'ok'; return
  else goto s1
  end
end

::s2:: do
  local c = io.read(1)
  if c == '0' then goto s1
  elseif c == nil then print 'not ok'; return
  else goto s2
  end
end

goto s1

雖然可以使用更好的方式來編寫這段代碼,但上例中的方法有助于將一個有限自動機自動地轉化為 Lua 語言代碼。
再舉一個簡單的迷宮游戲的例子。迷宮中有幾個房間,每個房間的東南西北方向各有一扇門。玩家每次可以輸入移動的方向,如果在這個方向上有一扇門,則玩家可以進入相應的房間,否則程序輸出一個警告,玩家的最終目的是從第一個房間走到最后一個房間。
這個游戲是一個典型的狀態(tài)機,當前玩家所在房間就是一個狀態(tài)。為實現(xiàn)這個迷宮游戲,我們可以為每個房間對應的邏輯編寫一段代碼,然后用 goto 語句表示從一個房間移動到另一個房間。示例 8.2 展示了如何編寫一個由 4 個房間組成的小迷宮。

示例 8.2 一個迷宮游戲

goto room1    --起始房間

::room1:: do
  local move = io.read()
  if move == "south" then goto room3
  elseif move == "east" then goto room2
  else 
    print("invalid move")
    goto room1    --待在同一個房間
  end
end

::room2:: do
  local move = io.read()
  if move == "south" then goto room4
  elseif move == "west" then goto room1
  else
    print("invalid move")
    goto room2
  end
end

::room3:: do
  local move = io.read()
  if move == "north" then goto room1
  elseif move == "east" then goto room4
  else
    print("invalid move")
    goto room3
  end
end

::room4:: do
  print("Congratulations, you win!")
end

對于這個簡單的游戲,讀者可能會發(fā)現(xiàn),使用數(shù)據驅動編程是一種更好的設計方法。不過,如果游戲中的每間房都不同,那么就非常適合使用這種狀態(tài)機的實現(xiàn)方法。


8.4 練習

  • 練習 8.1:大多數(shù) C 語法風格的編程語言都不支持 elseif 結構,為什么 Lua 語言比這些語言更需要這種結構?

首先,Lua 語言沒有 switch,所以需要一個結構來處理多重條件判斷的問題,其次,使用 elseif 可以少寫很多 end 避免出現(xiàn)因 end太多導致編寫代碼出錯的問題。

  • 練習 8.2:描述 Lua 語言中實現(xiàn)無條件循環(huán)的 4 種不同方法,你更喜歡哪一種?
while true do end
repeat until false
for i = 0, 1 , -1 do end
::continue:: do goto continue end

一般見得多的都是 while true,所以我也喜歡用 while true。

  • 練習 8.3:很多人認為,由于 repeat-until 很少使用,因此像 Lua 語言這樣的簡單的編程語言中最好不要出現(xiàn),你怎么看?

repeat-until 對標類 C 中的 do-while,有時候使用這種結構邏輯上更加通順,可以減少開發(fā)的難度,減少出錯的概率,所以還有保留的必要。

  • 練習 8.4:正如在 6.4 節(jié)中我們所見到的,尾部調用偽裝成了 goto 語句。請用這種方法重寫 8.2.5 節(jié)的迷宮游戲。每個房間此時應該是一個新函數(shù),而每個 goto 語句變成了一個尾調用。
---@return function
function room1()
    print("enter room1")
    local move = io.read()

    if move == "south" then
        return room3()
    elseif move == "east" then
        return room2()
    else
        print("invalid move")
        return room1()
    end
end

---@return function
function room2()
    print("enter room2")
    local move = io.read()

    if move == "south" then
        return room4()
    elseif move == "west" then
        return room1()
    else
        print("invalid move")
        return room2()
    end
end

---@return function
function room3()
    print("enter room3")
    local move = io.read()

    if move == "north" then
        return room1()
    elseif move == "east" then
        return room4()
    else
        print("invalid move")
        return room3()
    end
end

function room4()
    print("Congratulations, you win!")
end

room1()
  • 練習 8.5:請解釋下為什么 Lua 語言會限制 goto 語句不能跳出一個函數(shù)。(提示:你要如何實現(xiàn)這個功能)

因為在進入一個函數(shù)的時候,編譯器需要保存現(xiàn)場,將上下文進行壓棧,如果 goto 語句可以跳出一個函數(shù)的話,必須要保存現(xiàn)場以便返回,但是這樣就沒有使用 goto 的必要了。

  • 練習 8.6:假設 goto 語句可以跳轉出函數(shù),請說明示例 8.3 中的程序將會如何執(zhí)行。

示例 8.3 一種詭異且不正確的 goto 語句的使用

function getlabel()
  return function() goto L1 end
::L1::
  return 0
end

function f(n)
  if n == 0 then return getlabel()
  else
    local res = f(n - 1)
    print(n)
    return res
  end
end

x = f(10)
x()

請試著解釋為什么標簽要使用局部變量相同的作用范圍規(guī)則。

當 n == 0 的時候 getlabel 已經 return 了,當goto以后就不知道會跳轉到哪里去了,如果直接使用棧中的上下文數(shù)據,會直接從f(1) 返回到 f(2) 但值不能保證傳給了 f(2) 的 res,從編譯原理的角度說,return 值是通過公共的內存區(qū)域來共享的,但 f(0) 并不知道 f(2) 的 公共區(qū)域地址在哪里,所以會導致很多奇怪的錯誤。
正因為有程序棧的存在,所以不能隨意的在不同的塊中隨意跳轉。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容