Lua代码规范

在公司整理了一份Lua语言的代码规范,分享一下。


前言

写代码如同写文章,每个人或多或少都有自己的风格。我们在进行产品开发的过程中,更多的是团队上的协作与交流而非单打独斗。所以,为了 提高开发效率,降低维护成本,促进团队合作,代码的审查,整理出这篇文章。

借助于《Python风格指南》中的警告词,在此处引用一下:代码的阅读频率比编写的要高得多,本风格指南旨在通过一致性提高代码的可读性。一致性在不断增加的度量中,与其他项目、项目内以及单个模块或功能内是重要的。但是最重要的是:知道什么时候不一致,并运用你最好的判断——有时风格指南并不适用。当应用规则会降低代码的可读性时,最好打破规则。

最后,一句话送给大家:

Programming style is an art.




1. 版式

1.1 基础

  • 所有文件均采用: UTF-8 无 BOM 格式

  • 程序块要采用 缩进风格 编写,缩进的空格数为 4个 ,对齐使用 空格键禁用TAB键

    • 避免使用不同编辑器阅读程序时,因TAB键设置的空格数不同而造成程序布局不整齐。
  • 函数内代码不超过 50行 , 单行代码不超过 80列

    • 长表达式要在 低优先级操作符 处拆分成新行,操作符放在新行之首

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      -- bad style
      local monsterModel = {id = monsterData.id, index = 0, entityData = monsterData, type = monsterData.role_type, state = EnumMonsterStatus.Alive, level = 0, coordinate = {toward = EnumEntityToward.None, x = 0, z = 0}, dropModels = { }, hpMedicineDropRate = 0, epMedicineDropRate = 0}

      -- good style
      local monsterModel = {
      id = monsterData.id,
      index = 0,
      entityData = monsterData,
      type = monsterData.role_type,
      state = EnumMonsterStatus.Alive,
      level = 0,
      coordinate = {toward = EnumEntityToward.None, x = 0, z = 0},
      dropModels = {},
      hpMedicineDropRate = 0,
      epMedicineDropRate = 0,
      }



      -- bad style
      if self.pEntity:isAttackStatus() or self.pEntity:isWakeStatus() or self.pEntity:isTobeHitStatus() or self.pEntity:isMoveStatus() or self.pEntity:isJostledStatus() then
      return false
      end

      -- good style
      if self.pEntity:isAttackStatus()
      or self.pEntity:isWakeStatus()
      or self.pEntity:isTobeHitStatus()
      or self.pEntity:isMoveStatus()
      or self.pEntity:isJostledStatus() then
      return false
      end
    • 若函数或过程中的参数较长,则要进行适当的划分

      1
      2
      3
      4
      5
      self.pQualitity = CREATESPINE(
      RES.QUALITYEFFECT_SPINE[_type].ID,
      self.LayerQuality,
      RES.QUALITYEFFECT_SPINE[_type].NAME,
      true, nil, true)



1.2 代码

空行

  • 代码概念与逻辑之间,逻辑段落小节之间,用 单个空行 分割
  • 函数之间用 单个空行 分割
  • 在注释之前增加 单个或多个空行


空格

  • 双目运算符,前后 都要有一个空格

    • 1
      110 + 50 * i, 110 + 50 / (i + 1), time + i - 12)
  • 逗号 前无空格,后跟空格

    • 1
      2
      3
      4
      5
      6
      7
      function cond(c, a, b)
      if c then
      return a
      else
      return b
      end
      end
    • 1
      2
      3
      for k, v in pairs(table) do
      ...
      end
  • 做为 函数参数部分 或 函数调用 的小括号 前后无空格

    • 原因:

      • lua解析语法时是采用空格等分割来解析的,某些情况下,不小心加空格会导致非预期的结果

      • 容易忘记相关空格,导致风格不统一,因此不如都不加

    • 样例:

      • 1
        2
        3
        4
        5
        6
        7
        8
        function totable(val)
        local tab = {}
        if "table" == type(val) then
        return val
        end
        tab[1] = val
        return tab
        end
      • 1
        local clientTable = totable(serverTable)
  • 大括号 前后无空格,后跟运算符看情况

    • 1
      package.loaded[path] = nil


其他

  • 每行代码结束时,禁止使用分号
  • 单字符使用 单引号 , 字符串使用 双引号
  • 使用 小括号 来强行规定运算顺序


注释

本规范的代码目标是自说明代码。

相对于代码的维护,注释的维护往往是非常差的,所以切忌乱用注释,尽量只在下列地方添加注释。

  • 代码比较晦涩,用了不同于常人的方法(显然这种方法肯定要比常人方法更高效)
  • 对外的接口部分,描述清楚作用及参数返回
  • 不同于正常逻辑的部分,一般是策划强制要求,要予以说明
  • 所有含有物理含义的变量、常量,如果其命名不是充分自说明的,在声明时必须加以注释,说明其物理含义
  • 数据结构声明,不能充分自说明的,必须加以注释,说明

注释的规范

  • 单行注释,注意 空格

    • 1
      -- 这是单行注释
  • 多行注释,注意缩进。

    最后 ]] 前加一个缩进,是为了让某些编辑器(Sublime、VSCode)折叠时简洁一些

    • 1
      2
      3
      4
      --[[
      这是多行注释
      这是多行注释
      ]]
  • 函数注释格式,位于函数上一行,采用多行注释,三个字段:描述、参数(可缺省)、返回(可缺省)

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      --[[describe:
      这是函数描述
      param1:参数类型
      参数描述
      param2:参数类型
      参数描述
      [param3]:参数类型([]代表可选参数)
      参数描述
      return:返回值类型,返回值类型
      返回值类型 - 描述
      返回值类型 - 描述
      ]]
      function funcName(param1, param2, param3)
      ...
      return return1, return2
      end
  • 文件注释

    • 1
      2
      3
      4
      5
      6
      --[[
      author: author1
      partner: partner1, partner2
      XMind: XMind名称
      describe: 这是一个测试的类,主要负责的功能是啦啦啦啦啦啦啦啦啦啦啦啦
      ]]




2. 命名规范

2.1 谨记

  • 最好的代码应该是 自说明代码,这种代码不需要多余的注释,本身便具备了作者意图的信息。
  • 采用英文单词或单词组合,单词首先要准确,其次要简单。 注意单词拼写,切忌使用拼音!
  • 不要为了避免命名过长而随意截取单词,丢失可读性
  • 所有命名不要与Cocos2d-x引擎或其他第三方工具已有的命名风格冲突
  • 作用域越大的名称应该越详细越能表述自己
    • i 做为小局部变量相对来说很妥当,但是做为全局变量会很糟糕。
  • 尽量不要用 _ 开头的名称,这些一般是lua内部所使用
  • 除非必要,不要用数字或者奇怪的字符来命名



2.2 类的命名规范

  • 所有 类的定义 均采用 大驼峰命名法(Pascal命名法)

    • 1
      Example = class("Example", nil)
  • 所有 类的函数 均采用 小驼峰命名法

    • 1
      2
      3
      function Example:exampleFunction()
      ...
      end
  • 所有 类的成员 均采用 self.(类型)(名称),类型均为小写,名称开头大写(整体是 小驼峰命名法

    • 类型对照表
类型 缩写 描述
int i 整数
float f 浮点数(不分float or double)
boolean b 布尔
string s 字符串
enum e 枚举
ptr p userdata
function func 函数
table t table,后缀为Array代表数组,后缀为Map代表字典
  • 如果是传入的回调函数,则采用 self.pXXXHandler 来命名

  • 例子:

    1
    2
    3
    4
    self.iExample
    self.bExampleExample
    self.sExampleExampleExample
    self.tExampleArray
  • 一般由 名词 或者 多名词 组成,不要随意简写

  • 根据类的特性和使用场景,加上相关的后缀或者前缀



2.3 变量命名

  • 采用 小驼峰命名法

  • 使用 名词 或者是 形容词+名词 命名

  • 尽量避免出现 仅靠部分字母大小写区分 的相似的变量

    • 1
      local posx, posX



2.4 常量命名

  • 配置常量均用大写,单词间以下划线相连

    • 1
      2
      3
      4
      5
      --幻影设置
      GHOST_STATE = {
      INTERVAL_TIME = 80, --出现间隔时间(毫秒)
      FADE_TIME = 500, --fade时间(毫秒)
      },
  • 类内配置常量,local引用,均用大写,单词间以下划线相连

    • 1
      2
      3
      4
      local LOCAL_CONST = {
      SPINE_ID = 1,
      ...
      }
    • 1
      local GHOST = ConstUI.GHOST_STATE
  • 所有枚举,均采用 大驼峰命名法,并必须加Enum前缀,枚举值采用 大驼峰命名法

    • 1
      2
      3
      4
      5
      EnumExample = {
      Example = 1,
      ExampleExample = 2,
      ExampleExampleExample = 3,
      }
    • 全局枚举值,应该在 global_enum中分模块定义;禁止在Ui或Cache中定义全局枚举值。

  • 全局函数,需要根据功能,分table调用,比如 table.insert, table.remove 这种。除了必要main.lua内函数,禁止没有范围,直接命名使用全局函数




3. 使用规则及技巧

3.1 变量

  • 尽量使用local变量而非global变量

  • 同一行变量赋值不要超过 3 个,且无值时用nil显式赋值

  • 常量避免使用 magic number

    • 1
      2
      3
      4
      5
      6
      7
      -- bad style
      -- 9.8就是 magic number
      local speed = time * 9.8

      -- good style
      _G.ACCELERATION = 9.8
      local speed = time * ACCELERATION
  • 禁止使用三元操作 and or

    • and 后值若为boolean类型,会不符预期,给后期查错造成困扰。
  • 默认参数

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function funcMethod(param)
      param = param or defalutValue
      ...
      end

      -- 默认值为true
      param = param ~= false

      -- 默认值为false
      param = param or false
  • _ 做为想忽略的变量

    • 1
      2
      3
      for _, v in pairs(tab) do
      ...
      end
    • 针对上面情况,即使for循环,尽量不要用 i, k, v,而采用更为详细一些的变量名称



3.2 table

  • 表的拷贝 此方法在表内条目大于2000时会失效

    • 1
      toTable = {unpack(fromTable)}
  • 判断空表

    • 1
      2
      3
      4
      #t == 0 不能判断表为空

      应该使用: next(t) == nil
      PS:next(t) 返回的是第一个找到的键的值,对于存储布尔类型值的表,可能返回false,因此必须与nil比较。
  • 更快的插入值

    • 1
      2
      3
      4
      5
      -- 慢,不推荐
      table.insert(t, value)

      -- 更快!推荐
      t[#t + 1] = value



3.3 函数

  • 明确函数的功能,精确(而不是近似)地实现函数设计

  • 接口函数参数的合法性检查,明确由调用者负责,而非函数设计者

  • 防止将函数的参数作为工作变量,有可能误改参数内容

  • 为简单的功能编写函数

    1
    2
    3
    4
    5
    value = (a > b) and a or b

    function max(a, b)
    return (a > b) and a or b
    end
  • 不要设计多用途而面面俱到的函数

    • 多功能集于一身的函数,很可能使函数的理解、测试、维护变得困难
  • 功能不明确较小的函数,特别使仅有一个上级函数调用它时,应考虑把它合并到上级函数中,而不必单独存在。

  • 设计高扇入、合理扇出(小于7,一般3-5)的函数

    • 说明:扇入指有多少上级函数调用它;扇出是指一个函数直接调用其他函数的数目。
    • 扇入过大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。
    • 扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,表明函数的调用层次可能过多,不利于程序的阅读和函数结构的分析,并且程序运行时会对系统资源(如堆栈空间等)造成压力。
    • 良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。



3.4 其他

对于大数据量的特殊优化

  • 多次 重复使用的全局接口,可以用局部变量保存下再使用

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      -- example1
      for i = 1, 10000000 do
      local x = math.sin(i)
      end

      -- example2
      local sin = math.sin
      for i = 1, 10000000 do
      local x = sin(i)
      end

      example2 效率会高于 example1
  • 创建 非常多 的小size表时,应预先填充好表的大小

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      -- example1
      for i = 1, 10000000 do
      local tab = {}
      tab[1] = 1
      tab[2] = 2
      tab[3] = 3
      end

      -- example2
      for i = 1, 10000000 do
      local tab = {1, 1, 1}
      tab[1] = 1
      tab[2] = 2
      tab[3] = 3
      end

      example2 效率高于 example1
  • 很多个 字符串连接,使用 table.concat 而非 ..

    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      -- example1
      local str1 = ''
      for i = 1, 100000 do
      str1 = str1 .. 'a'
      end

      -- example2
      local str2 = ''
      local tab = {}
      for i = 1, 100000 do
      tab[#tab + 1] = 'a'
      end
      str2 = table.concat(tab, '')

      example2 效率高于 example1,效果十分显著


作用域

有时候,用 do .. end 可以明确限定局部变量的作用域 (顺带一提,未理解禁用)

1
2
3
4
local v
do
local x = 1
end -- x作用域结束,被系统清理







想更进一步?

推荐阅读:

  • Lua Style Guide

  • 《代码大全》

  • 《程序员修炼之道》

  • 《高效程序员的45个习惯》

  • 《重构》