Lua 样式指南

lua-users home
wiki

简介

在应用这些 Lua 样式指南之前,请考虑来自 [Python 样式指南] 的警告,这些警告同样适用于 Lua。代码被阅读的次数远远超过编写次数,本样式指南旨在通过一致性提高代码的可读性。一致性非常重要,在不同项目、同一个项目内以及单个模块或函数内,一致性都至关重要。但最重要的是:要知道何时不一致,并运用你的最佳判断——有时样式指南并不适用。当应用规则会导致代码可读性降低时,打破规则可能是明智之举。

Lua 拥有自己的语法和习惯用法,因此需要自己的样式指南,我们试图在这里制定它。然而,没有必要重复其他更成熟语言(尤其是脚本语言)的程序员几十年经验中汲取的许多教训,因此我们可以从这些其他语言的样式指南中汲取一些灵感。

编程风格是一种艺术。规则中存在一些随意性,但它们有合理的理由。不仅提供关于风格的合理建议,而且了解这些风格建议形成背后的基本原理和人性化方面是有用的。

现在,让我们开始谈论 Lua...在定义推荐风格时,我们将参考一些 Lua 代码来源,这些来源具有准权威性,因为它们来自官方文档、原始 Lua 作者或其他信誉良好的来源。

格式

缩进 - 缩进通常使用两个空格。这在Programming in LuaLua Reference ManualBeginning 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,但这似乎主要是出于教学目的,以便清楚地区分用户定义的变量和内置变量。

函数命名 - 函数通常遵循与值和对象变量命名类似的规则(函数是一等公民)。由多个单词组成的函数名可以像(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

注意:这类似于在 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 字符串。

库和功能

除非必要,否则避免使用调试库,尤其是如果正在运行受信任的代码。(使用调试库有时是一种技巧:字符串插值。)

避免使用已弃用的功能。在 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(这在各种情况下可能对沙箱有害,而且很奇怪)。

可以通过不使用 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

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 == nilline == 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] = 1table.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 (差异)