1 变量

变量名称

变量由字母、数字、下划线组成,但不能由数字开头,且不建议_A**这种下划线后接大写字母的方式,通常作为Lua内部特殊用途。下面的几个词是Lua保留的关键字:

1
2
3
and or false true not repeat
if break do else elseif then end  for  goto while until in
local nil return function

大小写

Lua是大小写敏感的。

变量作用域

Lua默认都是全局变量,包括在函数内声明的变量。
本地变量以local开头,应该尽量使用local变量避免变量冲突。

变量不需要声明,直接使用即可,默认值是nil,给任何变量赋值nil等价于删除这个变量。

变量类型

Lua中有八种变量类型,分别是:

nil boolean number string userdata function thread table

可以用type()函数获取变量类型。

nil:区别于任意变量类型的特殊类型,表示空值

boolean:Lua中只有falsenil表示false,其余值都表示true,andor是短路判断,a and ba为真时返回b,a为假时返回a,a or b反之。Lua中不相等的判断符号是~=,其余的跟大部分语言没差别。

number

所有的整数和浮点数都可以用number表示,3==3.0也是true,math.type可以区别浮点数类型。

0x开头表示16进制数,可以用p*的形式表示位移,正数表示左移,如0x1p1=4.0,0x1p4=16.0

支持常规四则运算,//(两个除号)可以表示向下(负无穷)取整,如3//2==1,-3//2=-2,3//-2=-2,跟取余计算有如下关系:

a % b == a - ((a // b) * b)

a,b分别出现正负号时,有4种情况:

  • a,b都为正,余数为正,3%2=1
  • a,b都为负,-3%-2=-1
  • a正b负,3%-2=-1
  • a负b正,-3%2=1

总结一下就是,余数的符号跟除数一致。

string

string实际上是byte的序列。

#s可以获取字符串变量s内的长度,包含其中的空格等,返回的是byte的数量。

Lua的string跟java里一样,也是不可变的,但是可以通过函数用原来的字符串替换内容后生成新的字符串。

..(两个点号)可以连接两个字符串,"hello".." wolrd"->"hello world

tonumbertostring函数可以让字符串和数字相互转化。

userdata

可以保存任何C语言的数据

function:

function可以传入任意数量的参数,多余的会被自动丢弃,没有传入的参数默认为nil

函数可以有多个返回值,当函数作为中间过程调用时,只取第一个返回值,多返回值只在以下4个情况出现:

  • 多个变量接收返回值,此时多余的变量赋值为nil,多余的返回值被丢弃
  • 作为另一个函数的参数传递,并且是最后一个传入的参数,如g(a,b,f())
  • 作为table构造时的参数,并且是最后一个传入的参数,如tab={a,b,f()}
  • return语句调用 上面的情况都必须是直接符合条件,不能有括号,比如return (fun())这里的fun()的返回值是先被()处理,而不是直接被return,所以只能返回第一个值

函数可以接收变长参数,用...(3个点号)表示,可以直接使用,如a,b=...获取前两个参数。在函数体内用{...}的方式可以获取到所有参数的列表,然后用ipairs{...}的方式来遍历等,当然,如果参数包含nil这种方式就有缺陷,此时需要用args=table.pack(...)的方式获取参数,再用args.n获取参数数量,遍历排除nil

table:

一种强大的存储数据对象,用键值对的方式保存数据,除了nil以外的任何值都可以作为键。很像一个自动扩容的数组。

初始化方式为:a={},初始化时可以带元素,没有指定key的数据从1开始按顺序排key,也可以用key=value这种键值对的方式传入多个初始化的数据。key可以用[]包裹,来使用表达式作为key
这里a只是一个指向这个table的变量,table本身是匿名的,借助a来操作这个table,如果所有指向这个table的变量都被赋值为nil,那么这个table的资源就会被系统回收。

浮点数和整数作为键时,如果值相等,则指向同一个index,比如2.02是相同的,这里键的判断跟==是一样的。

#符号也可以获取table的长度,但是需要没有nil值在列表中间,这种连续存储的叫序列。如果想要获取有niltable的长度,需要额外存放一个变量统计长度,并把它放在table中。

遍历table,可以用下面几种方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
for k,v in pairs(a) do -- 对任意table都有效,但不保证元素输出的顺序
    --dosth
end

for k,v in ipairs(a) do --遍历列表
    --dosth
end

for k=1,#a do  --对于没有nil值的序列
    --dosth
end

关于嵌套table的导航,可以用or {}的方式获取空值,避免报错和对前面层次的反复获取。

table库有insert,remove等方法,具体用时可查。

2 注释

单行注释用--开头。
多行注释用--[[开头,直到]]结束。比如:

1
2
3
--[[
    print("hello world");
]]

多行注释的时候有个小技巧,把结尾的]]也用--开头,即:

1
2
3
--[[
    print("hello world");
--]]

当我们需要取消注释的时候,在注释开头添加一个-即可,即---[[,因为这个操作会让-[[这几个字符变成了单行注释的内容,而结束的--]]也因为块注释的失效而变成了单行注释,此时print语句就生效了。当然,这种技巧应该只在调试的时候使用,重要的代码建议还是保持整结,这种方式看上去有点混乱。

3 语法

分号和换行都不是必要的,内容用空格分隔即可,但是最好还是用行分隔或者分号分隔,显得清晰。

函数体和if语句等都有end标识符结尾,lua里除了falsenil,其它值都会判定为真,包括0

if语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if(condition)
then
--body
end

if(condition)
then
--body
else
--body
end

if(condition) then
--body
elseif condition then
--body
elseif condition then
--body
else
--body
end

for 循环

1
2
3
for a1,a2,a3 do
 --body
end

其中a1,a2是起始值和始点值(包含),a3为步进,可选

1
2
3
for k,v in ipairs(arr) do
  --body
end

k,v分别是下标和值,这个遍历类似于其它语言的foreach,遍历对象中的所有元素

while 循环

1
2
3
while(condition) do
--body
end

condition为真时执行

repeat until 循环

1
2
3
repeat 
--body
until(condition)

执行直到condition为真

4 I/O操作

简单版的基于inputoutput两个流来操作,基本只跟以下几个命令有关:

  • io.input:指定输入流来源,默认是标准输入
  • io.output():指定输出流目标,默认是标准输出(stardard output)
  • io.write:写内容到输出流,需要io.flush()或者关闭流才能输出到文件
  • io.read():从输入流读取内容,根据传入的参数表现不同的读取方式:
    • "a":读取文件所有内容
    • "l":读一行,不包含换行符
    • "L":读一行,包含换行符
    • "n":读一个数字
    • n:读取n个字符,这里的n表示数量

5 metatable和metamethod

Lua的面对向象主要是借助table实现的,在学习面向对象之前,先了解一下metatable相关的内容。

5.1 metatable

metatable本身是一个普通的table,而table可以用一个setmetatable方法来设置自身的metatable值,getmetatable方法来获取相应的值。

只设置metatable属性是不够的,还需要结合对应的metamethod(元方法)。

5.2 metamethod

Lua内核原生支持以下几种方法(都是2个下划线开头):

  • 算术操作符重载
    • __add:给这个字段指定方法后,+操作就会使用这个方法来实现,默认情况下table是不支持+操作的。看下面的例子:

       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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      
      Testtab={};
      local mt={}; -- 初始化metatable
      Testtab.new=function(t1)
          local tab={};
          setmetatable(tab,mt); -- 设置metatable
          for index, value in ipairs(t1) do
              tab[#tab+1]=value;
          end
          return tab;
      end
      
      local tab1=Testtab.new({10,20});
      local tab2=Testtab.new({100,200})
      
      --以下两种实现方式的效果是一样的,输出合并后的结果
      
      -- Testtab.merge=function (t1,t2)
      --     local res={}
      --     for index, value in ipairs(t1) do
      --         res[#res+1] = value
      --     end
      --     for index, value in ipairs(t2) do
      --         res[#res+1] = value
      --     end
      --     return res
      -- end
      -- mt.__add=Testtab.merge;
      -- for index, value in ipairs(Testtab.merge(tab1,tab2)) do
      --     print(value.." ")
      -- end
      
      mt.__add=function (t1,t2)
          local res={}
          for index, value in ipairs(t1) do
              res[#res+1] = value
          end
          for index, value in ipairs(t2) do
              res[#res+1] = value
          end
          return res
      end
      
      for index, value in ipairs(tab1+tab2) do
          print(value.." ")
      end
      
    • __sub-减号操作,二元操作

    • __mul: *乘号操作

    • __div/除号操作

    • __idiv//向下取整除号操作

    • __mod%取余操作

    • __unm-负号,一元操作符

    • __pow^指数操作

  • 位操作符重载
    • __band&按位与
    • __bor|按位或
    • __bxor:~按位异或,二元操作
    • __not~,按位取反,一元操作
    • __shl<<位左移
    • __shr>>位右移
  • __concat:连接
  • __len:用#符号获取table长度时调用的方法
  • __pairsfor循环时pairs(k,v)方法的重载
  • 关系比较操作符重载
    下面这三个是关系比较操作符,这3个操作就可以解决所有情况,所以只需要实现这3个,剩下的情况可以通过组合这3个来实现。
    • __lt<,less than
    • __le<=,less or equal than
    • __eq:==,equal
  • table元素读写
    • __index:这个是Lua实现“继承”或者说“原型”的重要属性,当我们在table中读取一个不存在的元素,它就会尝试去__index中寻找,__index的值可以是一个function,也可以是另一个table,用function可以实现的功能更多更灵活,比如添加多个table实现“多继承”。
      如果我们只想查找table自己的数据,而不包含从__index“继承”的,那么用rawget(t,i)方法。

    • __newindex:跟__index对应,这个操作是“写”相关的,当我们给table设置一个值,如果key不存在,那么会调用__newindex去完成这个操作。跟__index一样,值可以是fuction或者另一个tablerawset方法只写到原始table。一个简单的例子:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      SubA={}
      ParentA={}
      local mt={}
      setmetatable(SubA,mt)
      mt.__index=ParentA
      -- 下面的两种方式是一样的,注意fuction中的第一个参数mytable,它只是一个变量名,
      --可以是table,tab,甚至 _ 等任意值,用来赋值的function格式就是这样的,
      --__index中的function也类似
      mt.__newindex=ParentA
      
      -- mt.__newindex=
      -- function (mytable,k,v)
      --     ParentA[k]=v
      -- end
      SubA["foo"]="bar"
      print(SubA["foo"]); --"bar",从ParentA里继承
      print(rawget(SubA,"foo")) --nil,因为值设置到了ParentA里
      print(ParentA["foo"]) --"bar"
      

总的来说,metatable里包含了各种约定table行为的数据。

6 面向对象

metatable的基础上,看一下Lua如何实现面向对象的。

首先,Lua里面没有class的概念,但是__index可以实现“继承”的功能,我们可以用这个属性去构造一个类似原型链的模型。

6.1 创建一个对象

保存table的变量实际上是保存对这个table的引用,把这种变量赋值给另一个变量并不会产生新的table,而是对同一个table多了一个引用而已,所以下面的这种方式创建对象是不可行的:

1
2
3
4
Person={money=0}
p1=Person
p1.money=20
print(Person.money) --输出20,因为两者指向的是同一个table

我们可以写一个new方法来返回一个新的对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Person={money=0}
Person.new=function(){
    obj={} --一个新的table,用来返回新的对象
    mt={} --metatable
    setmetatable(obj,mt) --设置metatable
    mt.__index=Person --设置obj的“原型”为Person
    return obj
}

p1=Person.new
p1.money=20
print(Person.money) --0,p1是一个新创建的table,跟Person已经不是同一个

加入一个new方法解决了属性的问题,但“对象的方法”仍然存在问题,给上面的例子加一个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Person={money=0}
Person.new=function ()
    obj={}
    mt={}
    setmetatable(obj,mt)
    mt.__index=Person
    return obj
end

Person.add=function (n)
    Person.money=Person.money+n
end
p1=Person.new()
p2=Person.new()
p1.add(10) --把money添加到了Person.moeny中
print("p1's money:"..p1.money)
print("p2's money:"..p2.money)
print("Person's money:"..Person.money)

例子中,因为我们的add方法是对Person.money硬编码的,导致所有由Person.new生成的对象,在使用add方法时,都把money的值加到了Person.money上,这样显然是不行的,可以通过在方法中传入一个当前对象的引用来解决:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Person={money=0}
Person.new=function ()
    obj={}
    mt={}
    setmetatable(obj,mt)
    mt.__index=Person
    return obj
end

Person.add=function (self,n)
    self.money=self.money+n
end
p1=Person.new()
p2=Person.new()
p1.add(p1,10) 
print("p1's money:"..p1.money)
print("p2's money:"..p2.money)
print("Person's money:"..Person.money)

修改后的例子中的add方法,传入了一个self参数,每次调用方法时,都会把money加到selfmoney属性上。这里的self参数,当显式的传入时,可以用任何名称,不过还可以通过:(冒号)符号隐式的调用,但是方法的定义只能是function obj:fun_name的方式(前面的例子中Person.add=funtion()function Person.add()都是可以的,add方法可以改成下面这样,效果跟上面是一样的:

1
2
3
4
5
function Person:add(n) -- 隐式的传入参数self
    self.money=self.money+n
end

-- p1.add(p1,10)===p1:add(10)

还有两个可以优化的点:

  • new方法可以传入参数,实现“有参”构造函数的功能
  • metatable实际上也是一个普通的table,我们完全可以用对象本身的table来完成这个功能,也就是引入self参数,所以new方法也用:的方式定义和调用

优化后的类如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Person={money=0}
function Person:new(obj)
    obj=obj or {} --如果没有传入参数,则obj=nil,根据or的短路判断,初始化为{},
    self.__index=self --把self当作metatable,设置它的__index属性为self自身指向的table,节省一个table变量的命名
    setmetatable(obj,self) --给obj设置metatable,结合__index属性,obj相当于“继承”了self(也就是Person)的内容
    return obj
end


function Person:add(n)
    self.money=self.money+n
end

p1=Person:new()
p2=Person:new()
p1:add(10) 
print("p1's money:"..p1.money)
print("p2's money:"..p2.money)
print("Person's money:"..Person.money)

6.2 继承

继承也是用__index这个属性实现的,其实确切的说,这个模型更接近原型链。看一下例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Person={money=100}
function Person:new(obj)
    obj=obj or {}
    self.__index=self
    setmetatable(obj,self)
    return obj
end


function Person:add(n)
    self.money=self.money+n
end

Employee=Person:new()
function Employee:say()
    print("I have "..self.money.." dollors")
end
e=Employee:new()
e:add(100)
e:say() -- I have 200 dollors

例子中的几个关键步骤:

  1. Employee=Person:new(),这里调用的是Person的方法,所以隐式传入的self指向的是Person,方法返回一个obj,这个objmetatable__index指向了Person(这里selfPerson)。因为我们没有传入其它参数,所以Employee指向的是一个空的table,但是“继承”了Person
  2. e=Employee:new()Employee先去调用new方法,发现自身并没有这个方法,然后通过metatalbe指定__index属性去它的“原型”里面查找,找到了Person里的new方法,并使用这个方法。值得注意的是,调用new方法时隐式传入的self对象是Employee而不是Person,因为我们就是在Employee上调用的new方法,只是它通过“继承”获取到了方法,这一步返回一个空的table,但是它“继承”了Employee
  3. e:add(100),跟上面类似,先查找Employee,发现没有add方法,然后查找Person,找到后调用,此时传入的selfe本身,调用方法时发现自身还没有money这个属性,继续往上查找,找到Personmoney,值是100,然后相加,得200