写代码也有风格?

当然,写代码就跟写文章一样,每个人或多或少都有自己的风格。不同的语言也就像不同的文体一样,也有自己的独特的风格。Lua是一门脚本语言,写起来轻松惬意,但不代表它没有属于自己的风格指南。

好的代码风格基于可读性和一致性。代码更多的时间是给人看的,如果思考好了结构和逻辑,写代码的过程其实很快。风格的一致性也很重要,这样可以减少复杂度和理解成本。养成一种良好的代码风格会形成一种良好写代码习惯,这种习惯会使编码事半功倍。

下文将从命名,作用域,模块,注释和惯用法(精巧用法)等方面来说明Lua的代码风格,文章的最后会附上一些参考资料的链接以供读者拓展阅读。

命名

最好的代码是自说明代码,这种代码不需要多余的注释,其本身便具备了描述作者意图的信息。一种好的命名风格是自说明代码的基础。

命名法

驼峰命名法

小驼峰式命名法:第一个单字以小写字母开始;第二个单字的首字母大写,例如:firstName、lastName。
大驼峰式命名法:每一个单字的首字母都采用大写字母,例如:FirstName、LastName、CamelCase,也被称为Pascal命名法。

下划线命名法

小下划线命名法:所有字母均为小写,例如登录按钮:login_btn。
大下划线命名法:所有字母均为大写,常见于常量,例如:最小间隔时间MIN_GAP_TIME。
采用驼峰法或者下划线法都不太重要,重要的是你采用了自己喜欢的一种命名法,然后一直保持下去。

变量名长度

通常作用域范围更大的变量名要比作用域范围更小的变量名具有更多的描述信息。例如:i经常用于循环中充当计数变量,而将其作为全局变量使用容易导致诸多问题。

变量命名

对于变量(包括函数),小驼峰式命名法或小下划线命名法是一个好选择。比如:curSpeed表示当前速度,canDrop表示是否能掉落等等。

对于布尔值型的变量,通常前缀加上is可以方便理解,比如isRemoved比Removed更加能表示这是一个布尔值变量。

Lua中有一种特殊的变量名:_,常用来表示可以被忽略的、不会使用到的变量,常使用在循环中。

-- `_`表示表的键可以被忽略,只在循环内使用表中的值`v`
for _,v in ipairs(t) do print(v) end

在表的循环中和函数参数列表中,i常表示ipairs下的数组下标,k常表示pairs下的键,v常表示对应的值,t则表示表。

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

常数命名

Lua里没有严格的常数定义标识符,所以对于常数的命名格外重要。

常数一般采用大下划线命名法。这样每个字母都大写,十分醒目,且各个单词都用下划线分割,便于阅读。

比如:MAX_SPEED表示最大速度,IS_SHOW_DEBUG_ERROR_MSG表示是否显示报错消息等等。

类名

为了不与变量名和常数名混淆,类名通常使用大驼峰式命名法,即首字母大写。比如:TouchManager表示触摸管理器类。

包和模块名

包名和模块名通常很短,并且全部小写,单词间并没有下划线区分。比如:文件读取库名为lfs,表示Lua File System;XML解析库名为lxp,表示Lua XML Parser等等。

文件名

通常为了不与类名混淆,对于文件名,经常使用小驼峰式命名法或小下划线命名法。

作用域

Lua的作用域以关键字end进行标识。

对于变量,有一条原则:在一切能使用local修饰的情况下,使用local进行修饰。

因为不用local修饰的变量会自动变成全局变量。全局变量十分危险,很容易被篡改而不知道在哪里被篡改了,这很容易导致顽固的bug出现。并且全局变量的处理速度也比局部变量的速度要慢很多。

所以,尽可能的用local来修饰变量。

有时候,用do … end可以用来明确限定局部变量的作用域。

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 -- x,y,z的作用域结束,被系统清理
 
local count
do
  local x = 0
  count = function() x = x + 1; return x end
end -- x的作用域结束,被系统清理

模块

Lua中有一个叫module的公有函数,此函数的作用是将一组变量和函数打包在一个模块名下,便于其他文件require。但是这个函数受到了诸多的指责,原因是其会创建一个公共变量,并且这个公共变量中的所有细节都会暴露出来。这其实十分不符合面向对象的规范。

以下有一种办法可以避免这个问题,即不采用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()

Lua内没有类这个变量类型,但是通过Lua的metatable可以轻松实现类的继承,多态等等特性。关于Lua中类的实现原理,请参考我之前写的这篇博客:Lua中实现类的原理。

注释

通常在–前加上一个空格。

return nil  -- not found    (建议)
return nil  --not found     (不建议)

注释通常用在函数接口,或者复杂,精巧的逻辑上。

对于接口的注释,可以按照javadoc类似的来写。

-- Deletes a session.
-- @param id Session identification.
-------------------------------------
function delete (id)
    assert (check_id (id))
    remove (filename (id))
end

惯用法(精巧用法)

尽可能使用local修饰变量(重要的事情要说三遍!)

原因:

使用local的变量会在作用域结束时释放其内存
使用local的变量会比全局变量的存取更快
全局变量会污染全局的命名空间,可能会导致诡异的bug出现
直接判断真假值

-- 不推荐
if obj ~= nil and willBreak == false then
    -- ...
end
 
-- 推荐
if obj and not willBreak then
    -- ...
end

原因:

Lua在逻辑判断时将所有非false和nil的逻辑判断视为真,反之视为假,不需要再与布尔值和nil进行比对。

但是,在需要对false和nil进行区分时,需要写明==:obj == nil和obj == false。

默认参数的实现

范式:param = param or defaultValue

function setName(name)
    name = name or 'noName'
    -- ...
end

原因:or会在第一次为true的时候断路,返回其判断的最后一个值。所以当name为空时,name or ‘noName’返回为’noName’,这会将name的值自动设置为noName。

一行代码实现表的拷贝

u = {unpack(t)}

需要注意的是此法在表内条目大于2000时会失效。

一行代码判断表是否为空

用#t == 0并不能判断表是否为空,因为#预算符会忽略所有不连续的数字下标和非数字下标。

正确做法是:

if next(t) == nil then 
    -- 表为空
    -- ...
end

因为表的键可能为false,所以必须与nil比较,而不直接使用~next(t)来判断表是否空。

更快的插入代码

-- 更慢,不推荐
table.insert(t, value) 
 
-- 更快,推荐
t[#t+1] = value 

原因:[]和#避免了高层的函数调用开销。

参考资料

这篇文章是基于Lua Style Guide而来。

语言的风格大致是通用的,在Python里,有一种叫pythonic的代码风格,详见:让你的python代码更加pythonic。

对于任何程序员,我都力荐《代码大全》这本书。在里面,你可以找到十分完备的从设计,架构到具体编码,注释,到团队协作等等相关的引导。

还有几本书:《程序员修炼之道》,《高效程序员的45个习惯》,《重构》。它们可以作为《代码大全》的补集存在。

关于《高效程序员的45个习惯》这本书,我进行了总结和提炼,阅读之前不妨看看这篇读书笔记。

编辑:糖果

作者:Tim’s Blog