lua学习笔记

根据各个学习资料记录下可能会忘记的点。

学习资料

LUA简明教程

  • 注释
-- 单行注释
--[[
注释块
--]]
  • 变量
lua变量默认是全局变量。
即使是函数内定义的变量,如果不加local限制,在函数调用外也可以使用。
函数内对函数外的全局变量操作会影响到这个全局变量的值。
不过如果函数内用local定义了一个重名的外部全局变量则不会改变外部的值。
  • bool和if
在if判断中只有nil和false能表示false,其他数字0,1,2和字符串等都表示true。
但是如果用==来判断则都不合适。
  • lua没有++或者+=
  • _G
在lua中,没有加local的全局变量放在了一个叫“_G”的Table中。也就是说通过for k,v in pairs(_G) 可以找出所有的全局变量。

Lua程序设计

  • Lua的特点
lua不做c语言已经做的很好的地方,lua小巧简单,是对c的扩展。lua可以整合各个c/c++等静态语言的组件,作为粘合剂glue language。
也就是说产品的组件可以是c/c++所写的,不常更新。但是那些产品中变化较多的应用方向的的开发可以使用lua来实现。
  • 删除全局变量
通过给这个全局变量赋值为nil就可以将其删除,就好像凑没有被使用过一样,因为当一个变量不等于nil时,这个变量才存在。
  • 词法命名
不建议使用用下划线加大写字母的标识符,因为lua的保留字也是这样的。
  • 类型
lua是动态类型语言,变量不需要类型定义。
8种基本类型:nil、boolean、number、string、userdata、function、threadtable
函数type可以得到变量的类型:print(type(x))
  • lua的number都是浮点
表示实数,Lua中没有整数。一般有个错误的看法CPU运算浮点数比整数慢(难道不是吗?)。
  • string
lua中单引号和双引号都表示字符串。
lua中的字符串是字符的序列,8 bit字节序列。
lua中的字符串是不可修改的。需要新的变量存放新的字符串
[[...]]表示多行字符串
对字符串使用算术操作符时,string会被自动转出数字
..是lua的字符串连接符,前后最好有空格,尤其是当前面是数字时,必须在..前加空格。此时会把数字自动转成字符串
但是比较10 == "10"永远都是false
  • string与number的显式转换
如果需要显式将string转成数字可以使用函数tonumber(),如果string不是正确的数字该函数将返回nil可以调用tostring()将数字转成字符串,这种转换一直有效。tonumber("10") == 10      -- truetostring(10) == "10"         -- true
10 .. "" == "10"                 -- true
  • function
在lua中,函数也是一等公民,可以被存储在变量中。
lua可以调用lua或者c的函数,lua所有的标准库都是c实现的。
  • userdata
userdata将c数据存放在lua变量中。userdata用来描述应用程序或者使用C实现的库创建的新类型。例如:用标准I/O库来描述文件。

  • 关系运算符
Table、userdata、function只能比较相等与否,而且仅仅表示两个变量是否指向同一个对象,如果两个table的内容一样,但是也不相等。
  • 逻辑运算符
print(4 and 5)   -- 打印5,意思是如果两个都是true,那么就显示最后一个
print(nil and 13) -- 打印nil,意思是把第一个不为true的打印出来。
也就是说,最后得到的是逻辑运算到可以出结果的时候那个表达式的值。
print(4 or 5)  -- 打印4,因为第一个就可以出结果
print(false or 5) -- 打印5,因为第二个才能出结果
利用这个特点可以做两个事情:
1、如果xfalse或者nil则给x赋初始值v
x = x or v
2、实现三元运算符:
下面例子是让a取值为a和b中的较大者
> a = 10
> b = 5
> a = (b > a and b) or a
> print(a)
10
> b = 15
> a = (b > a and b) or a
> print(a)
15
  • 表的构造
1、当成数组
      a = {1, "a2", 3, 4}  -- 表示的是table a是可以类似数组的概念,下标从1开始。a[1]为1,a[2]为"a2"
2、当成字典
      a = {x=0, y=0}  -- 表示的是a.x为0,a.y为0,也可以用["x"]来索引,表示a["x"]为0
这里需要注意的是变量名字不能以数字开头,所以没有a.1这样,只能a[1]。
3、临时添加
      a = {x=0, y=0}
      a.z = 0  -- 添加z这个key进入a中。也可以写成a["z"] = 0
      a[1] = "new field"  -- 添加[1]这个key进入a中。
如果想要添加一个更加特殊的key进入a,比如希望a有个key为-name,那么需要:
      a["-name"] = "a"  -- 索引的时候必须是a["-name"]
4、混合构造
      a = {x=0, y=0, "a", "b"}  --  等价于{x=0, y=0, [1]="a", [2]="b"} 还等价于{["x"]=0, ["y"]=0, [1]="a", [2]="b"}
最后这种等价方式其实就是:[expression]显示的表示将被初始化的索引,这样理解起来就更方便了。
对于表a来说,索引有两种,一种是用点,另一种是用中括号。中括号是更通用,因为有的索引名字不符合命名规则(以数字或特殊符号开头)。能用.来索引的比如a.x换成中括号要加引号也就是a["x"],如果直接a[x]则为nil,想要用a[x]的话,必须还定义下x = "x",然后才能a[x],也就是表达式索引了。
另外需要注意的是:
      a[10] = 10 与a["10"] = "str_10"不是同一个索引
[]的方式,还可以接受表达式来做索引。
      i = 20
      a[i + 1] = "a21"   -- a[21]为a21
通过[0] = XXX可以让数组下标强制从0开始,但是强烈不推荐这么使用,这会使很多标准库不能使用。
5、嵌套构造
      a = {x=0, y=0, {x=1, y=1}, {1, 2, 3}}   -- a中的元素有a.x为0,a[1]为table,a[1].x为1,a[2][1]为1
6、域分割符
之前看到的都是逗号,做分割符,其实还可以用分号;
一般来说是这样的,用分号来做归类分割不同的类型。
      a = {1, 2, 3; "1", "2", "3"; x=0, y=0}
而且最后可以留个“,”方便后续扩充。
      a = {1, 2, 3,}
7、删除元素
      a.x或者a["x"]置为nil即可
8、使用table构造一个list
> list = {}
> list = {next=list, value="a"}
> list = {next=list, value="b"}
> list = {next=list, value="c"}
>
> print(list.value)
c
> print(list.next.value)
b
> print(list.next.next.value)
a
> print(list.next.next.next.value)

nil
  • 赋值
除了常规赋值,lua支持一行语句做多个变量的同时赋值
      a, b, c = 1, 2, 3
注意,遇到赋值语句lua会先计算右边所有的值再执行赋值操作。所以我们可以利用这个做交换变量值的操作:
      x, y = y, x
      a[i], a[j] = a[j], a[i]
当变量个数和值的个数不一致时,Lua会采用下面这个策略:
    a. 变量个数 > 值的个数             按变量个数补足nil
    b. 变量个数 < 值的个数             多余的值会被忽略
所以赋值 a, b, c = 0只会得到a=0, b和c均为nil

  • 局部变量和代码块
局部变量就是变量名前面加local,此时这个变量名只会在代码块block中使用。
代码块:指一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)。
这里再提一下chunk的概念:Chunk是一系列语句,Lua执行的每一块语句,比如一个文件或者交互模式下的每一行都是一个Chunk
所以在交互的模式下,local的作用范围可能很窄,比如一行一行的敲就是只有一行有效:
> local la = 10
> print(la)

nil
另外可以使用do...end来包装代码段。相当于c语言的{}
推荐尽可能的使用局部变量,这样可以:
    1、避免命名冲突
    2、访问局部变量比全局变量更快
如果在一个lua文件中定义一个全局变量,那么在require("这个文件")之后全局变量依然有效。

  • for循环控制语句
lua的for循环和别的不太一样~
分两类来看:
第一类是数值for循环:

for var=exp1,exp2,exp3 do
    loop-part
end
for将用exp3作为stepexp1(初始值)到exp2(终止值,包含这个值),执行loop-part。其中exp3可以省略,默认step=1

1、三个表达式只会计算一次,并且是在循环之前
这一点就和别的语言的for循环完全不一样,要额外小心。换句话说lua的for循环是钦定的,还没开始就根据计算得到了循环次数。
2、控制变量var是局部变量自动被声明,并且只在循环内有效.

for i=1,10 do
    print(i)
end
max = i       -- probably wrong! 'i' here is global

还有一个例子:

> i = 100
> for i=3,1,-1 do

    print(i)

end
3
2
1
> print(i)
100

3、循环过程中不要改变控制变量的值,那样做的结果是不可预知的。如果要退出循环,使用break语句。
第二类,泛型for循环:
举两个之前见过的例子:
-- print all values of array 'a'
for i,v in ipairs(a) do print(v) end
范型for遍历迭代子函数返回的每一个值。
再看一个遍历表key的例子:
-- print all keys of table 't'
for k in pairs(t) do print(k) end
范型for和数值for有两点相同:
1. 控制变量是局部变量
2. 不要修改控制变量的值
看起来也没有这么夸张,修改也是可以的。只是影响不到外面而已。(除非对外面的变量赋值)

  • pairs和ipairs
for k, v in pairs(一个table) 和 for k, v in ipairs(一个table)的区别在于:
ipair会在遇到第一个k是nil值的时候就停止了,但是pairs会把所有的k,v对都找出来。
判断一个Table是否为空,不能使用if table_a == {},因为这个结果永远是false,用table.maxn(table_a)也是不准确的,这个只能判断数组型的table,应该使用内置函数if next(table_a) == nil来做判断。
  • break和return语句
这个还算正常,但是有一点要求是break和return语句必须在chunk的最后一句。这样就造成了我们不能像别的语言一样在不想跑后续代码的时候直接在前面写return。必须用do...end代码块包起来才可以。


function
 foo ()
    return            --<< SYNTAX ERROR
    -- 'return' is the last statement in the next block
    do return end        -- OK
    ...               -- statements not reached
end

  • 函数的括号
一般来说函数需要有个括号表示调用,但是有一个例外可以省略括号:
当函数只有一个参数并且这个参数是字符串或者表构造的时候,()可有可无
print "Hello World"      --  等价于print("Hello World")

f{a=1, b=2}  --  等价于 f({a=1, b=2})

  • 多返回值的函数
一般来说多返回值就多返回值,和之前一样进行匹配就好,多的部分填扔掉,少的部分填nil。不过这里有几个情况需要看下,实际操作是对对返回值做了截断

print(foo2())            --> a   b  没有截断
print(foo2(), 1)         --> a   1  截断成一个
print(foo2() .. "x")     --> ax     截断成一个
a = {foo0()}             -- a = {}    (an empty table)
a = {foo1()}             -- a = {'a'}
a = {foo2()}             -- a = {'a', 'b'}


a = {foo0(), foo2(), foo2()}  -- a[1] = nil, a[2] = 'a', a[3] = 'a', a[4] = 'b'
可以使用圆括号强制使调用返回一个值。
print((foo0()))      --> nil
print((foo1()))      --> a
print((foo2()))      --> a
一个return语句如果使用圆括号将返回值括起来也将导致返回一个值。
介绍一个特殊的函数,unpack,这个函数也是多返回值的函数。目标是接受一个数组多输入,然后返回数组中的所有元素。(只返回table中数组的那部分内容,非数组样式的不会返回

  • 可变参数的函数
在函数定义的时候括号内写... :function varArgs(...) print(arg.n) end
其中arg就是保存函数变长参数的table,arg.n表示本次传入函数中的变长参数个数的域。
比如:
function g (a, b, ...) end
g(3, 4, 5, 8)  就是   a=3, b=4, arg={5, 8; n=2}

  • 函数的调用时指定参数名
假如有一个函数:rename(old="temp.lua", new="temp1.lua"),在Python中我们可以在调用时指定函数的参数名字。但是在lua中,这种做法是不被支持的。
此时需要修改函数的定义,入参设计为一个table变量而不是多个变量。此时的函数定义为:
function rename (arg)
    return os.rename(arg.old, arg.new)
end


  • 函数本质
lua中的函数有两个特质:1、一等公民;2、词法定界(嵌套的函数可以访问他的外部函数中的变量,upvalue)
之前的函数定义function foo() ... end其实是在做一个赋值的操作:foo = function() ... end
  • 高级函数
以其他函数作为参数的函数在lua中称为高级函数。
举个例子:

network = {
    {name = "grauna",    IP = "210.26.30.34"},
    {name = "arraial",   IP = "210.26.30.23"},
    {name = "lua",       IP = "210.26.23.12"},
    {name = "derain",    IP = "210.26.23.20"},
}
table标准库提供一个排序函数来做排序
table.sort(network, function (a,b)
    return (a.name > b.name)
end)

  • 闭包
1、函数内部嵌套函数,内部的嵌套函数使用外部函数的入参和局部变量,或是对入参进行修改或是返回一个结果。
2、外部函数的返回值就是内部的函数。此时调用完外部函数后,创建入参变量或者局部变量的外部函数已经返回了,但是内部函数还在手上,此时的内部函数使用到的这些upvalue的值就是内部函数新的一个局部变量来保存。
  • 尾调用

Lua中函数的另一个有趣的特征是可以正确的处理尾调用(proper tail recursion,一些书使用术语“尾递归”,虽然并未涉及到递归的概念)。
尾调用是一种类似在函数结尾的goto调用,当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。例如:
function f(x)
    return g(x)
end
g的调用是尾调用。
例子中f调用g后不会再做任何事情,这种情况下当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如Lua解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语言支持正确的尾调用。
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调用不论n为何值不会导致栈溢出。

尾调用是指调用函数调用其他函数(尾函数)后不再做任何其他事情。比如下面几个就不是尾调用:

function f (x)
    g(x)
    return
end
上面这个例子中f在调用g后,不得不丢弃g地返回值,所以不是尾调用,同样的下面几个例子也不时尾调用:
return g(x) + 1      -- must do the addition
return x or g(x)     -- must adjust to 1 result
return (g(x))        -- must adjust to 1 result
Lua中类似return g(...)这种格式的调用是尾调用。但是gg的参数都可以是复杂表达式,因为Lua会在调用之前计算表达式的值。例如下面的调用是尾调用:
return x[i].foo(x[j] + a*b, i + j)

  • dofile、loadfile和require
loadfile只是读取加载编译一个文件,然后返回一个函数。
dofile()内部调用了loadfile(),最后执行了loadfile返回的函数。
require()是加载文件的时候不用带目录,lua自己会搜索和加载目录的路径,而且会判断文件是否加载过,如果加载过则不重复加载。
【判断加载与重复加载】判断是否加载过的标识就是在_G.package.loaded这个表中,比如在require('reqTest')之后,_G.package.loaded表中就会有'reqTest'这一项了,对应的值为reqTest.lua文件中最后的返回值,如果没有显式的return,那么_G.package.loaded.reqTest的值为true,如果有显式的返回值那么就是具体的那个返回值。当下次再调用require('reqTest')时,会先去_G.package.loaded找是否存在'reqTest'这一项,如果存在那么就直接用_G.package.loaded.reqTest的值作为require的结果。
另外require的加载会从LUA_PATH中加载lua文件,然后从LUA_CPATH中加载so文件。具体想知道代码中的PATH和CPATH值可以通过打印package这个table得到。也就是package.path和package.cpath。
还有一个loadstring()则是从string中读取chunk。

  • 异常和错误处理
error(),里面的参数是要抛出的错误信息字符串
assert(),第一个参数是检查项(对表达式或者返回值做检查),第二个参数是抛出错误时的报错信息
pcall(),这个作用有点类似try ... catch ...,是指在protected模式下执行函数内容,同时捕获所有的异常和错误。里面的参数是一个函数(可以不带参数只是一个或者带参数),也就是把要捕获异常和错误的代码封装到这个函数内,然后用pcall调用,如果返回值是true和被执行函数的返回值那么就是没有错误产生,如果返回值是nil以及错误信息,那么就是捕获到了错误。

假定你想运行一段Lua代码,这段代码运行过程中可以捕捉所有的异常和错误。
第一步:将这段代码封装在一个函数内
function foo ()
    ...
    if unexpected_condition then error() end
    ...
    print(a[i])   -- potential error: `a' may not be a table
    ...
end
第二步:使用pcall调用这个函数
if pcall(foo) then
    -- no errors while running `foo'
    ...
else
    -- `foo' raised an error: take appropriate actions
    ...
end
当然也可以用匿名函数的方式调用pcall
if pcall(function () ... endthen ...
else ...


local status, err = pcall(function () error({code=121}) end)
print(err.code)  -->  121
这种机制提供了强大的能力,足以应付Lua中的各种异常和错误情况。我们通过error抛出异常,然后通过pcall捕获之。

  • coroutine
协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针(IPinstruction pointer),但与其它协同程序共享全局变量等很多信息。线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。就是说,一个具有多个协同程序的程序在任何时刻只能运行一个协同程序,并且正在运行的协同程序只会在其显示地挂起时,它的执行才会暂停。
协同是非常强大的功能,但是用起来也很复杂。
举个create的例子:
    首先定义一个函数:
    function co_func(...)
        dosomething
        for XXX do
            coroutine.yield(...)
        end
    end
    然后先创建
    co_create = coroutine.create(co_func)
    再运行
    print(coroutine.resume(co_create, 正确的co_func函数入参))
    再次运行时,入参可以任意设置,输入与否甚至是错误的都不会有任何影响
    print(coroutine.resume(co_create, dasfafasd))
    直到最终打印出false cannot resume dead coroutine表示运行完毕
再举个wrap的例子:
    还是定义了一个co_func(...)的函数
    然后用wrap创建
    co_wrap = coroutine.wrap(co_func)
    初次运行的时候和resume方式类似,只是不用显式的使用
    print(co_wrap(正确的参数))   -- 这里看到结果的一个不同是没有true和false的指示
    再次运行的时候也无需正确的参数了
    print(co_wrap(错误的参数或者没有参数也就是nil))   -- 判断运行是否完毕可以通过是否返回值为nil
推荐参考Lua Coroutine详解
  • table能做数组、矩阵、链表、(双向)队列、集合
数组就是以连续数字做索引的table
矩阵:1、table的table;2、一个table但是索引是合成的
链表:使用next索引做指针
队列:使用first和last索引做指针
集合:{["value1"]=true, ["value2"]=true, ...}
  • 字符串缓冲
前面已经看到,在lua中,字符串是不可变的,每次产生新的字符串都是使用新的变量来存贮。那么就会产生内存问题,也即是内存的不停复制以及gc的不停回收。
比如下面这段代码的效率就很低:

-- WARNING: bad code ahead!!
local buff = ""
for line in io.lines() do
    buff = buff .. line .. "\n"
end
尽管这段代码看上去很正常,但在Lua中他的效率极低,在处理大文件的时候,你会明显看到很慢,例如,需要花大概1分钟读取350KB的文件。(这就是为什么Lua专门提供了io.read(*all)选项,她读取同样的文件只需要0.02s
为什么这样呢?Lua使用真正的垃圾收集算法,但他发现程序使用太多的内存他就会遍历他所有的数据结构去释放垃圾数据,一般情况下,这个算法有很好的性能(Lua的快并非偶然的),但是上面那段代码loop使得算法的效率极其低下。
为了理解现象的本质,假定我们身在loop中间,buff已经是一个50KB的字符串,每一行的大小为20bytes,当Lua执行buff..line.."\n"时,她创建了一个新的字符串大小为50,020 bytes,并且从buff中将50KB的字符串拷贝到新串中。也就是说,对于每一行,都要移动50KB的内存,并且越来越多。读取100行的时候(仅仅2KB),Lua已经移动了5MB的内存,使情况变遭的是下面的赋值语句:
buff = buff .. line .. "\n"
老的字符串变成了垃圾数据,两轮循环之后,将有两个老串包含超过100KB的垃圾数据。这个时候Lua会做出正确的决定,进行他的垃圾收集并释放100KB的内存。问题在于每两次循环Lua就要进行一次垃圾收集,读取整个文件需要进行200次垃圾收集。并且它的内存使用是整个文件大小的三倍。
这个问题并不是Lua特有的:其它的采用垃圾收集算法的并且字符串不可变的语言也都存在这个问题。Java是最著名的例子,Java专门提供StringBuffer来改善这种情况。
在继续进行之前,我们应该做个注释的是,在一般情况下,这个问题并不存在。对于小字符串,上面的那个循环没有任何问题。为了读取整个文件我们可以使用io.read(*all),可以很快的将这个文件读入内存。但是在某些时候,没有解决问题的简单的办法,所以下面我们将介绍更加高效的算法来解决这个问题。

改进方案一:
使用汉诺塔思路,如果新入栈的字符串长度比下一个长,那么就合并,如果需要会一直合并到最后一个,保证沉入栈底的字符串是最长的。最后再统一做一次合并。
合并的时候可以使用table.concat(t_list, "\n")来实现。
改进方案二:
直接使用io.read("*all")

  • metatable和metamethod
lua的每一个表都有其metatable,不过默认创建的新表的metatable是nil
比如:t = {}; print(getmetatable(t))  --> nil
需要通过setmetatable函数来设置或者改变一个表的metatable
比如:t_method = {}; setmetatable(t, t_method); assert(getmetatable(t) == t_method)
此时如果再创建一个表t1也绑定它的metatable为t_method,那么t1和t就有共同的metatable
接着再定义t_method.__add = function (a, b) ... end,就定义了一个“+”操作。
然后t + t1就可以按照上面的函数实现具体的操作了。
补充下:其实这里Lua选择metamethod的原则:如果第一个参数存在带有__add域的metatableLua使用它作为metamethod,和第二个参数无关;否则第二个参数存在带有__add域的metatableLua使用它作为metamethod ;再否则二者都没有就报错。也就是说这里其实t和t1只要有一个设置matetable就可以了

setmetatable/getmetatable函数也会使用metafield,在这种情况下,可以保护metatables。假定你想保护你的集合使其使用者既看不到也不能修改metatables。如果你对metatable设置了__metatable的值,getmetatable将返回这个域的值,而调用setmetatable 将会出错:
Set.mt.__metatable = "not your business"

s1 = Set.new{}
print(getmetatable(s1))     --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable

  • 面向对象
方法:在表中新加一个域值为函数时,可以在函数的参数中使用self,或者用:来表示增加一个额外的隐藏参数。
类和对象:把一个表当作“类”是指,让其有个:new()的函数(方法),在其中返回的obj与self为metatable关系,然后self.__index = self,这样obj所有找不到索引都会跑到self中找。

  • weak表和内存回收
Lua会自动进行内存管理,但是有时候需要人工协助,比如某个Table中设置了一个key和value,接着把key的值和value都更新了,此时在Table中会有老key和新key两个域的值,那么可能作者的意思是key已经更新了,老的就不再需要了,此时就需要用到weak表来辅助做内存回收。
表的weak性由他的metatable__mode域来指定的。在这个域存在的时候,必须是个字符串:如果这个字符串包含小写字母‘k’,这个table中的keys就是weak的;如果这个字符串包含小写字母‘v’,这个table中的vaules就是weak的。下面是一个例子,虽然是人造的,但是可以阐明weak tables的基本应用:
a = {}
b = {}
setmetatable(a, b)
b.__mode = \"k\"     -- now 'a' has weak keys

key = {}             -- creates first key
a[key] = 1

key = {}             -- creates second key
a[key] = 2

collectgarbage()     -- forces a garbage collection cycle

for k, v in pairs(a) do print(v) end
--> 2
在这个例子中,第二个赋值语句key={}覆盖了第一个key的值。当垃圾收集器工作时,在其他地方没有指向第一个key的引用,所以它被收集了,因此相对应的table中的入口也同时被移除了。可是,第二个key,仍然是占用活动的变量key,所以它不会被收集。





评论

此博客中的热门博文

Python一些技巧和知识点