Lua 風格指南

lua-users home
wiki

簡介

套用這些 Lua 風格準則之前,請考慮採用 [Python 風格指南] 的警語,這同樣適用於 Lua。閱讀程式碼的頻率遠高於撰寫,且本風格指南旨在透過一致性來改善程式碼的可讀性。一致性極為重要,且在其他專案中、專案內部,以及單一模組或函數內有不同的程度重要性。但最重要的是:知道何時應違反一致性,並運用最佳判斷力--有時候風格指南根本無法套用。當套用準則會降低程式碼可讀性時,最好打破規則。

Lua 有自己的語法和慣用語習慣,因此需要制定專屬的風格指南,我們在此嘗試擬定。然而,不需要重複其他更成熟語言的程式設計員數十年經驗所得的教訓,尤其是相關的腳本語言,因此我們可以從其他語言的風格指南中獲取一些啟發

程式設計風格是一門藝術。這些規則有一些專斷性,但背後都有合理的理論依據。不僅提供關於風格的合理建議,還能理解制定風格建議背後的理論依據和人為因素,才是有用的

現在,談談 Lua……在定義推薦風格時,我們將從一些具備準官方權威的 Lua 程式碼來源取得意見,因為這些來源來自官方文件、Lua 原作者或其他信譽良好的來源

格式設定

縮排(Indentation)- 縮排通常使用兩個空格。Programming in Lua、Lua Reference Manual、Beginning Lua Programming,和 Lua 使用者 wiki 都遵循這個原則。 (為什麼如此,我不知道,但或許是因為 Lua 陳述式往往會深度巢狀,甚至以 LISP 或函數的方式,或者也許受到這些範例中的程式碼既簡潔又有教育意義這項事實影響。) 你會看到其他通用的慣例 (例如 3-4 個空格或 tab 鍵)。

for i,v in ipairs(t) do
  if type(v) == "string" then
    print(v)
  end
end

命名

變數名稱長度 - 有一個通則(不一定限於 Lua)是範圍較大的變數名稱應該比範圍較小的變數名稱更具描述性。例如,i 對大型程式中的全域變數而言可能是一個很差的選擇,但作為小迴圈中的計數器卻非常適當。

值與物件變數命名 - 變數用於儲存值或物件時通常使用小寫並簡短(例如 color)。Beginning Lua Programming 書中對使用者變數使用 CamelCase,但這似乎在很大程度上是出於教學原因,為的是清楚區分使用者定義變數與內建變數。

函式命名 - 函式通常會遵循與值和物件變數命名的類似規則(函式是一等物件)。由多個字詞組成的函式名稱可以用連續的方式表示(如同 getmetatable),如同標準 Lua 函式所作,雖然你也可以選擇使用底線符號(print_table)。有些來自其他程式設計背景的程式設計師會使用 CamelCase,例如 obj::GetValue()

Lua 內部變數命名 - [Lua 5.1 參考手冊 - 詞彙慣例] 指出,「慣例上,以底線符號後接大寫字母開頭的名稱(例如 _VERSION)保留給 Lua 用的內部全域變數。」這些通常是常數,但並不一定,例如 _G

常數命名 - 常數,尤其是那些為簡單值的常數,通常會寫成 ALL_CAPS,各個字詞可以選擇使用底線符號分隔(例如 PiL2 中的 MAXLINES,4.3 以及 PiL2 中的馬可夫範例,清單 10.6)。

模組/套件命名 - 模組名稱通常是名詞,名稱簡短且使用小寫,各個字詞之間沒有分隔。至少,這是 Kepler 中常見的命名模式:luasql.postgres(而非 Lua-SQL.Postgres)。範例:「lxp」、「luasql」、「luasql.postgres」、「luasql.mysql」、「luasql.oci8」、「luasql.sqlite」、「luasql.odbc」、「socket」、「xmlrpc」、「xmlrpc.http」、「soap」、「lualdap」、「logging」、「md5」、「zip」、「stable」、「copas」、「lxp」、「lxp.lom」、「stable」、「lfs」、「htk」。不過,請參閱下方模組區段的註解,這是關於用作類別的模組。

唯獨包含底線符號「_」的變數通常用作你想要忽略此變數的佔位符。

for _,v in ipairs(t) do print(v) end

注意:這類似於在 Haskell、Erlang、Ocaml 和 Prolog 等語言中使用「_」的方式,其中「_」在模式比對中具有匿名的(忽略的)變數這個特殊含義。不過在 Lua 中,「_」只是一個慣例,本身沒有任何特殊含義。語意編譯器通常會標記出未使用的變數,但對命名為「_」的變數則可能不會這麼做(例如 LuaInspect 就是一個這種情況)。

ikvt 通常會這樣使用

for k,v in pairs(t) ... end
for i,v in ipairs(t) ... end
mt.__newindex = function(t, k, v) ... end

M 有時用作「目前模組表」(例如:參閱 PIL2,15.3)。

self 是方法在上面被引用的物件(比如 C++ 或 Java 中的 this)。事實上,這是由 : 語法糖來強制實施。

  function Car:move(distance)
    self.position = self.position + distance
  end

類別名稱(或至少代表類別的元表)可以混合大小寫(BankAccount),或可能並未混合。如此一來,縮寫(例如,XML)可能只將第一個字母變為大寫(XmlDocument)。

匈牙利標記法[匈牙利標記法])。對變數名稱進行語義資訊編碼有助於您程式碼的讀者,特別是如果該資訊無法以其他方式輕易推論,儘管過度這樣做是多餘的,且會降低程式碼的可讀性。在一些更為靜態的語言(例如,C)中,資料類型為編譯器所知,資料類型編碼到變數名稱是多餘的,但即使如此,資料類型既不是唯一,也不是必然為最實用的語義資訊形式。

local function paint(canvas, ntimes)
  for i=1,ntimes do
    local hello_str_asc_lc_en_const = "hello world"
    canvas:draw(hello_str_asc_lc_en_const:toupper())
  end
end

上述範例中的變數命名規則對讀者暗示了以下事項。 canvas 是個畫布物件,可能明確衍生自類似 Canvas 的項目,而該資訊可能無法輕易地從程式碼中推論。 ntimes 是用來繪製某個項目的整數次數。 i 傳統上為整數索引,儘管這很明顯,即使稱為其他名稱,也可以讓程式碼簡潔。 _str_asc_lc_en_const 是多餘的,甚至令人困惑,可以移除:很明顯此變數是個常數、英文、小寫的 ASCII 字串。

程式庫與功能

除非必要,否則避免使用 debug 程式庫,特別是在執行受信任程式碼時。(使用 debug 程式庫有時是被視為技巧:StringInterpolation。)

避免使用已過時的特性。在 5.1 中,這些特性包括 table.getntable.setntable.foreach[i]gcinfo。請參閱 [Lua 5.1 參考手冊 - 7 - 與先前版本的相容性] 了解替代方案。

範圍

儘可能使用區域變數,而非全域變數。

local x = 0
local function count()
  x = x + 1
  print(x)
end

全域變數具有較大的範圍和生命期,因此會增加 [耦合] 和複雜度。 [1] 不要污染環境。在 Lua 中,存取區域變數也比存取全域變數快 [PIL 4.2],因為全域變數需要在執行時期進行表查詢,而區域變數則存在為暫存器 [ ScopeTutorial ]。

DetectingUndefinedVariables 中給出了一項有用的方法來偵測無意中使用全域變數的情形。在 Lua 中,全域變數有時是拼寫錯誤或其他潛伏在您程式碼中的錯誤所造成的結果。

有時使用 do-區塊 [PIL 4.2] 進一步限制區域變數的範圍是有用的。

local v
do
  local x = u2*v3-u3*v2
  local y = u3*v1-u1*v3
  local z = u1*v2-u2*v1
  v = {x,y,z}
end

local count
do
  local x = 0
  count = function() x = x + 1; return x end
end

全域變數的範圍也可以透過 Lua 模組系統 [PIL2 15][setfenv] 加以縮小。

模組

Lua 5.1 的模組系統通常受到推薦。然而,對於 Lua 的 5.1 的模組系統已有一些批評。詳細資訊請參閱 LuaModuleFunctionCritiqued,但總而言之,您可能會想要照著這樣的範例撰寫模組

-- hello/mytest.lua
module(..., package.seeall)
local function test() print(123) end
function test1() test() end
function test2() test1(); test1() end

然後像這樣使用它

require "hello.mytest"
hello.mytest.test2()

批評的理由是這樣寫會在所有模組中建立一個名為「hello」的全域變數(這是一個副作用),而且全域環境會透過「hello」表格公開,例如:「hello.mytest.print == _G.print」(這在許多方面對於 sandboxing 不利,而且看起來很奇怪)。

可以透過不使用「module」函式而僅採用以下簡單的方式定義模組來避免這些問題

-- hello/mytest.lua
local M = {}

local function test() print(123) end
function M.test1() test() end
function M.test2() M.test1(); M.test1() end

return M

然後再以這種方式匯入模組

local MT = require "hello.mytest"
MT.test2()

封裝一個包含建構函式的類別(以物件導向的觀點來看)的模組,有許多方式可以在模組中實作。以下是一個相當不錯的方法。[*2]

-- file: finance/BankAccount.lua

local M = {}; M.__index = M

local function construct()
  local self = setmetatable({balance = 0}, M)
  return self
end
setmetatable(M, {__call = construct})

function M:add(value) self.balance = self.balance + value end

return M

使用這種方式定義的模組通常只包含一個類別(或至少只包含一個公開類別),也就是模組本身。

可以像這樣使用它

local BankAccount = require "finance.BankAccount"
local account = BankAccount()

甚至是這樣的方式

local new = require
local account = new "finance.BankAccount" ()

上述內容多少遵循 Java 的傳統,包括將「finance」套件全部寫成小寫,將「BankAccount」類別寫成 Pascal 形式,以及物件寫成小寫合併式(Camel)形式。請注意其優點。這些類別很容易被發現,並且可以與類別的實例(也就是物件)加以區分,因為物件使用小寫合併式。如果您看到類似「BankAccount:add(1)」的內容,那很可能是一個錯誤,因為「:」是用於對物件執行方法呼叫,但您會留意到「BankAccount」很顯然是很個類別,因為它使用了這種大小寫慣例。

上述內容並非唯一使用的方式。您會看到許多其他形式

account = finance.newBankAccount()
account = finance.create_bank_account()
account = finance.bankaccount.create()
account = finance.BankAccount.new()

可以主張,在沒有適當理由的情況下,變化並不是好事。

註解

在「--」之後加上空白。

return nil  -- not found    (suggested)
return nil  --not found     (discouraged)

(上述範例遵循 luarefman、PiL、luagems 的第 21 章、BLP 以及 Kepler/LuaRocks。)

沒有標準的註解慣例。

可以模擬文件字串(請參閱 DecoratorsAndDocstrings)。POD 格式也被推薦使用(請參閱 LuaSearch)。此外還有 [LuaDoc]

Kepler 有時使用這種 doxygen/Javadoc 式形式

-- taken from cgilua/src/cgilua/session.lua
-------------------------------------
-- Deletes a session.
-- @param id Session identification.
-------------------------------------
function delete (id)
        assert (check_id (id))
        remove (filename (id))
end

終結符號

因為「end」是許多不同結構的終結符號,如果使用註解來澄清正在終結的是哪個結構,這有助於讀者(特別是在大型區塊中)。 [*3]

  for i,v in ipairs(t) do
    if type(v) == "string" then
      ...lots of code here...
    end -- if string
  end -- for each t

Lua 慣用語

要在條件語句中測試變數是否非 nil,最簡潔的方法是直接寫出變數名稱,而不是明確地與 nil 進行比較。在條件語句中,Lua 將 nilfalse 視為 false(而將所有其他值視為 true

local line = io.read()
if line then  -- instead of line ~= nil
  ...
end
...
if not line then  -- instead of line == nil
  ...
end

但是,如果測試的變數可能包含 false,那麼如果你必須區分這兩個條件,就需要明確表示出來:line == nil 相對於 line == false

andor 可用於簡化程式碼

local function test(x)
  x = x or "idunno"
    -- rather than if x == false or x == nil then x = "idunno" end
  print(x == "yes" and "YES!" or x)
    -- rather than if x == "yes" then print("YES!") else print(x) end
end

複製一個小的表格 t(警告:僅適用於整數鍵;這有一個取決於系統的表格大小限制;在一個系統上剛剛超過 2000)

u = {unpack(t)}

確定表格 t 是否為空(包括 #t 會忽略的非整數鍵)

if next(t) == nil then ...

要附加到陣列,執行 t[#t+1] = 1 會比 table.insert(t, 1) 更加簡潔有效率。

設計模式

Lua 是一種小語言,有少數幾個基本的建構單元,可以用很多種強大的方式加以組合。有了這個自由度,就需要具備自制力,也就是設計模式。通常 Lua 中有一種慣用的方法或設計模式可以達到某種效果,而且可以重複使用(例如 ObjectOrientationTutorialReadOnlyTables)。請參閱 LuaTutorialSampleCode 以取得解決此類問題的常見方法。

編碼標準

以下是各種 Lua 專案中使用的編碼標準清單

個人偏好

本節包含較不精製或較主觀的內容,尚未納入上方的主文。

[*2] (上述樣式的倡導者包括 DavidManura,而像 RiciLake 之類的其他人士也提到類似內容。(在這裡加入你的名字))

[*3] GavinWraith 所註解

[*4] JulioFernandez


頁面留言

我認為「Lua 風格」太過主觀,無法達成共識,因此此頁面無法使用。建議將頁面重新命名為 DavidsLuaStyleGuide?--JohnBelmonte

此文件目的在於幫助使用者改善其程式碼的風格。Lua 有一些既定的慣例,在某些形式中得到廣泛認可,甚至在 Lua 的標準參考文件中也有明文規定(例如避免使用全域變數、限制範圍、避免使用偵錯函式庫、使用 and/or 來縮短條件等)。Lua 的風格確實比其他語言更加多樣化,而且有很多情況並沒有共識。然而,共識並非必要:僅需要說明各種常見方法以及在何處使用,如此使用者才能在自己的專案中更有效地選擇慣例。針對更主觀的風格或個人意見,提供「個人偏好」區段。--DavidManura

嘗試撰寫程式設計指南時,可能應將重點限制在 Lua 語言本身(也就是說,不包含 C API 的使用方式)。--JohnBelmonte針對用於嵌入式語言的程式設計指南而言,有關如何設計繫結,以及如何以 C 和 C++ 編寫繫結,這些部分都是合理的討論對象。-- RiciLake


近期異動 · 偏好設定
編輯 · 歷史紀錄
上次編輯時間為 2022 年 3 月 15 日下午 11:36 GMT (差異)