lua语言入门
文章目录
1 变量
变量名称
变量由字母、数字、下划线组成,但不能由数字开头,且不建议_A**
这种下划线后接大写字母的方式,通常作为Lua内部特殊用途。下面的几个词是Lua
保留的关键字:
|
|
大小写
Lua
是大小写敏感的。
变量作用域
Lua
默认都是全局变量,包括在函数内声明的变量。
本地变量以local
开头,应该尽量使用local
变量避免变量冲突。
变量不需要声明,直接使用即可,默认值是nil
,给任何变量赋值nil
等价于删除这个变量。
变量类型
Lua
中有八种变量类型,分别是:
nil boolean number string userdata function thread table
可以用type()
函数获取变量类型。
nil
:区别于任意变量类型的特殊类型,表示空值
boolean
:Lua
中只有false
和nil
表示false
,其余值都表示true
,and
和or
是短路判断,a and b
在a
为真时返回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
tonumber
和tostring
函数可以让字符串和数字相互转化。
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.0
和2
是相同的,这里键的判断跟==
是一样的。
#
符号也可以获取table
的长度,但是需要没有nil
值在列表中间,这种连续存储的叫序列。如果想要获取有nil
的table
的长度,需要额外存放一个变量统计长度,并把它放在table
中。
遍历table
,可以用下面几种方式:
|
|
关于嵌套table
的导航,可以用or {}
的方式获取空值,避免报错和对前面层次的反复获取。
table
库有insert,remove
等方法,具体用时可查。
2 注释
单行注释用--
开头。
多行注释用--[[
开头,直到]]
结束。比如:
|
|
多行注释的时候有个小技巧,把结尾的]]
也用--
开头,即:
|
|
当我们需要取消注释的时候,在注释开头添加一个-
即可,即---[[
,因为这个操作会让-[[
这几个字符变成了单行注释的内容,而结束的--]]
也因为块注释的失效而变成了单行注释,此时print
语句就生效了。当然,这种技巧应该只在调试的时候使用,重要的代码建议还是保持整结,这种方式看上去有点混乱。
3 语法
分号和换行都不是必要的,内容用空格分隔即可,但是最好还是用行分隔或者分号分隔,显得清晰。
函数体和if
语句等都有end
标识符结尾,lua里除了false
和nil
,其它值都会判定为真,包括0
if语句
|
|
for 循环
|
|
其中a1
,a2
是起始值和始点值(包含),a3为步进,可选
|
|
k,v分别是下标和值,这个遍历类似于其它语言的foreach
,遍历对象中的所有元素
while 循环
|
|
condition为真时执行
repeat until 循环
|
|
执行直到condition
为真
4 I/O操作
简单版的基于input
和output
两个流来操作,基本只跟以下几个命令有关:
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
长度时调用的方法__pairs
:for
循环时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
或者另一个table
,rawset
方法只写到原始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
多了一个引用而已,所以下面的这种方式创建对象是不可行的:
|
|
我们可以写一个new
方法来返回一个新的对象:
|
|
加入一个new
方法解决了属性的问题,但“对象的方法”仍然存在问题,给上面的例子加一个方法:
|
|
例子中,因为我们的add
方法是对Person.money
硬编码的,导致所有由Person.new
生成的对象,在使用add
方法时,都把money
的值加到了Person.money
上,这样显然是不行的,可以通过在方法中传入一个当前对象的引用来解决:
|
|
修改后的例子中的add
方法,传入了一个self
参数,每次调用方法时,都会把money
加到self
的money
属性上。这里的self
参数,当显式的传入时,可以用任何名称,不过还可以通过:
(冒号)符号隐式的调用,但是方法的定义只能是function obj:fun_name
的方式(前面的例子中Person.add=funtion()
和function Person.add()
都是可以的,add
方法可以改成下面这样,效果跟上面是一样的:
|
|
还有两个可以优化的点:
new
方法可以传入参数,实现“有参”构造函数的功能metatable
实际上也是一个普通的table
,我们完全可以用对象本身的table
来完成这个功能,也就是引入self
参数,所以new
方法也用:
的方式定义和调用
优化后的类如下:
|
|
6.2 继承
继承也是用__index
这个属性实现的,其实确切的说,这个模型更接近原型链。看一下例子:
|
|
例子中的几个关键步骤:
Employee=Person:new()
,这里调用的是Person
的方法,所以隐式传入的self
指向的是Person
,方法返回一个obj
,这个obj
的metatable
的__index
指向了Person
(这里self
是Person
)。因为我们没有传入其它参数,所以Employee
指向的是一个空的table
,但是“继承”了Person
e=Employee:new()
,Employee
先去调用new
方法,发现自身并没有这个方法,然后通过metatalbe
指定__index
属性去它的“原型”里面查找,找到了Person
里的new
方法,并使用这个方法。值得注意的是,调用new
方法时隐式传入的self
对象是Employee
而不是Person
,因为我们就是在Employee
上调用的new
方法,只是它通过“继承”获取到了方法,这一步返回一个空的table
,但是它“继承”了Employee
。e:add(100)
,跟上面类似,先查找Employee
,发现没有add
方法,然后查找Person
,找到后调用,此时传入的self
是e
本身,调用方法时发现自身还没有money
这个属性,继续往上查找,找到Person
的money
,值是100
,然后相加,得200
版权声明 本博客使用CC BY-NC-SA 4.0许可协议(创意共享4.0:保留署名-非商业性使用-相同方式共享)。