Lua 样式指南 |
|
在应用这些 Lua 样式指南之前,请考虑来自 [Python 样式指南] 的警告,这些警告同样适用于 Lua。代码被阅读的次数远远超过编写次数,本样式指南旨在通过一致性提高代码的可读性。一致性非常重要,在不同项目、同一个项目内以及单个模块或函数内,一致性都至关重要。但最重要的是:要知道何时不一致,并运用你的最佳判断——有时样式指南并不适用。当应用规则会导致代码可读性降低时,打破规则可能是明智之举。
Lua 拥有自己的语法和习惯用法,因此需要自己的样式指南,我们试图在这里制定它。然而,没有必要重复其他更成熟语言(尤其是脚本语言)的程序员几十年经验中汲取的许多教训,因此我们可以从这些其他语言的样式指南中汲取一些灵感。
编程风格是一种艺术。规则中存在一些随意性,但它们有合理的理由。不仅提供关于风格的合理建议,而且了解这些风格建议形成背后的基本原理和人性化方面是有用的。
现在,让我们开始谈论 Lua...在定义推荐风格时,我们将参考一些 Lua 代码来源,这些来源具有准权威性,因为它们来自官方文档、原始 Lua 作者或其他信誉良好的来源。
缩进 - 缩进通常使用两个空格。这在Programming in Lua、Lua Reference Manual、Beginning Lua Programming 和 Lua 用户维基中都有遵循。(为什么是这样,我不知道,但可能是因为 Lua 语句即使在 LISP 或函数式方式中也可能倾向于深度嵌套,或者可能是受到这些示例中代码量小且具有教学意义的影响。)你会看到其他常见的约定(例如 3-4 个空格或制表符)。
for i,v in ipairs(t) do if type(v) == "string" then print(v) end end
变量名长度 - 这是一条通用的规则(不一定是 Lua 特定的规则),即作用域较大的变量名应该比作用域较小的变量名更具描述性。例如,i
对于大型程序中的全局变量来说可能是一个糟糕的选择,但在小型循环中用作计数器则完全没问题。
值和对象变量命名 - 保存值或对象的变量通常是小写且简短的(例如 color
)。Beginning Lua Programming 一书对用户变量使用 CamelCase
,但这似乎主要是出于教学目的,以便清楚地区分用户定义的变量和内置变量。
is
前缀可能会有所帮助,例如 is_directory
而不是 directory
(它可能存储目录对象本身)。
函数命名 - 函数通常遵循与值和对象变量命名类似的规则(函数是一等公民)。由多个单词组成的函数名可以像(getmetatable
)一样连在一起,就像标准 Lua 函数中所做的那样,尽管你可以选择使用下划线(print_table
)。一些来自其他编程背景的人使用 CamelCase
,例如 obj::GetValue()
。
Lua 内部变量命名 - [Lua 5.1 Reference Manual - Lexical Conventions] 中说,“按照惯例,以一个下划线后跟大写字母开头的名称(例如 _VERSION
)保留用于 Lua 使用的内部全局变量。”这些通常是常量,但不一定是,例如 _G
。
常量命名 - 常量,尤其是简单的值,通常以 ALL_CAPS
形式给出,单词之间可选地用下划线分隔(例如 PiL2, 4.3 中的 MAXLINES
,以及 PiL2, Listing 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
_
",其中 "_
" 在模式匹配中具有匿名(忽略)变量的特殊含义。在 Lua 中,"_
" 只是一个约定,没有固有的特殊含义。通常会标记未使用的变量的语义编辑器可能会避免对名为 "_
" 的变量这样做(例如,LuaInspect 就是这种情况)。
i
、k
、v
和 t
通常按以下方式使用
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 字符串。
除非必要,否则避免使用调试库,尤其是如果正在运行受信任的代码。(使用调试库有时是一种技巧:字符串插值。)
避免使用已弃用的功能。在 5.1 中,这些功能包括 table.getn
、table.setn
、table.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
(这在各种情况下可能对沙箱有害,而且很奇怪)。
可以通过不使用 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
使用帕斯卡命名法,对象使用混合大小写(驼峰命名法)。注意这种方法的优势。类很容易识别,并且可以与类的实例化(即对象)区分开来,后者使用驼峰命名法。如果你看到类似 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
为了在条件语句中测试变量是否不为 nil
,直接写变量名比显式地与 nil
进行比较更简洁。在条件语句中,Lua 将 nil
和 false
视为 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
。
and
和 or
可以用于编写更简洁的代码
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 中实现某种效果会有一种惯用法或设计模式,可以经常重复使用(例如,ObjectOrientationTutorial 或 ReadOnlyTables)。参见 LuaTutorial 和 SampleCode,了解解决此类问题的常见解决方案。
以下是各种 Lua 项目中使用的编码规范列表
本节包含尚未纳入上述主文本的不太完善或更主观的內容。
[*2] (上述风格的倡导者包括 DavidManura,以及 RiciLake 等其他人也提到了类似的事情。(在此添加您的姓名))
[*3] 由 GavinWraith 指出
[*4] JulioFernandez
我认为“Lua 风格”太主观了,无法达成共识,因此此页面不可行。建议将页面重命名为 DavidsLuaStyleGuide?。--JohnBelmonte
本文档的目的是帮助用户改进其编码风格。Lua 有一些约定,在某些形式中被普遍认可,甚至在标准 Lua 参考中也有说明(例如避免全局变量、限制范围、避免调试库、使用and
/or
来缩短条件等)。事实上,Lua 风格比其他语言更加多样化,并且在许多情况下缺乏共识。但是,共识不是必需的:仅仅描述各种常用的方法,并指出它们的使用位置,对于用户在自己的项目中更有效地选择约定是有用的。提供了一个“个人偏好”部分,用于更主观的风格或个人意见。--DavidManura
尝试编写风格指南可能应该将重点限制在 Lua 语言本身(即排除 C API 使用)。--JohnBelmonte 您设计绑定方式的风格,以及您在 C 和 C++ 中编写绑定的风格,是为嵌入式语言设计的风格指南的合法讨论。-- RiciLake