关于Lua
1993年由巴西里约热内卢天主教大学计算机系Roberto Ierusalimschy(莱鲁萨利姆斯奇)等人编写
一开始就没有准备去实现C语言已经实现的非常出色的方面
与C语言之间实现非常好的交互能力,一门胶水语言
非常适合C程序号学习,互补性非常强
简易性:轻量级,小巧简单易学,概念不多
高效:可以说是目前最快的脚本语言,运行效率非常高,占用内容很少,源代码量非常小
可移植性:没有使用任何条件编译处理不同平台,纯粹的ANSI C编写完成,
三种开发形式
1.独立使用lua实现功能
2.使用C语言为主,lua为扩展的方式
3.使用lua为主,C语言为扩展的方式
开始
程序块chunk
交互模式里输入的一行代
一个文件里的代码
也就是一连串的语句或命令
连续的lua语句之间不需要分隔符,但也可以使用分号,如果你愿意的话
在交互模式中输入的每条命令都会立即执行
使用-i可以在运行代码块后进入交互模式
使用dofile可以加载代码文件进行调试
词法规则
大小写敏感
任何字母数字和下划线,但不能是数字开头
尽量不要使用以单下划线开头后跟一个或多个大写字母的变量,这些大多用于系统保留了
“_"是哑变量(Dummy Variable)
and break do else elseif end false for function
if in local nil not or repeat return then true until while
"--"为单行注释
--[[ --]]为多行注释
使用---[[可以很方但的取消大块注释
全局变量
在任何在方对一个变量赋值就声明一个全局变量
直接访问未声明的全局变量不会产生错误,会返回nil
要删除一个全局变量可以给它赋值nil,通常是没有必要这么做的
局部变量
local声明
仅在声明的作用域中有效,如do end
尽量使用local来声明变量
类型与值
lua是一种动态类型的语言,在语言中没有类型定义的语法,每个值都携带了它自身的类型信息
lua中有8种基础类型
nil
只与自身相等assert(nil==nil),空值
boolean
true
false
nil和false是假,其它都是真,0和""都是true
number
浮点实数,没有使用整数
string
8位编码也就是个char
可以包含任何字符,包括\0,很象stl里的string类,可以当二进制流看待
string.len和#返回的是实现长度,不以\0为结束标记
不可以修改字符串变量内容,只能重新赋值
lua能高效的处理大字符串,不用怀疑lua字符串的处理能力
特殊字符与c一样,如换行是\n tab是\t
多行字符串定义[[ ]]
\<ddd>一至三个数据表示字符,ASCII码
在字符串上使用数学运算会先前字符串转换成数字
".."是字符串连接操作符"wang".."ning"
在数字后使用字符串连接操作符".."时,要有一个空格,不然会认为是小数点
尽量不要依赖lua的自动类型转换,可以使用tostring,tonumber手动转
userdata
一块由c向lua申请的内存,存在lua里,申请者自行解析
function
lua中的函数是作为”第一类值“,可以随意赋给一个变量
thread
协同线程lua没有真正的多线程,他都是串行执行的,生产者消费者模式
只能自己停止自己
可以使用守护线程来在多个协同线程中通过状态判断来模拟多线程的环境
table
lua里唯一的一种数据结构
lua里的module package object都是用table实现的
table是一个对象,所有对他的
可以表示出多种数据结构
数组
可以不写key添加数据如:t={1,2,3,4}
自动定义从下标1开始顺序向后加一
所有没有显示声明key的项都是自动生成数组下标的
lua里与c不一样的地方就是一标都人1开始,一定记住
t={"a", 2="b", "c", 4=1, 2, 6=3}
t[1] = "a"
t[2] = "c"
t[3] = 2
t["2"] = "b"
t["4"] = 1
t["6"] = 3
后面三项不再是数组而是hash表
使用#可以得到数组长度,但要求所有下标是从1开始连续的
t={}
t[1] = 1
t[100] = 100
的长度是1,因为它以找到nil的值为结束,以上的定义方式数组断了
二维数组就是table套table
t={ {100,200}, {300,400}}
t[1][1] = 100
t[1][2] = 200
t[2][1] = 300
t[2][2] = 400
链表
t1 = {p=t3, v="value", n=t2}
t2 = {p=t1, v="value", n=t3}
t3 = {p=t2, v="value", n=t1}
环形链表
hash表
队列
栈
可以使用lua中的任何类型作key和value,除nil不能当key,当value是删除
只能显示的声明一个table,t = {}
添加数据
t = {"a","b","c",1,2,3}
t={1="a",2="b",3="c",4=1,4=2,6=3}
以上两人个定义不相等,显示声明时字符串类型的key可以不写双引号
t[1] = "a"
t[2] = "b"
t[3] = "c"
t[4] = 1
t[5] = 2
t[6] = 3
这个与第一个相同
t["1"] = "a"
t["2"] = "b"
t["3"] = "c"
t["4"] = 1
t["5"] = 2
t["6"] = 3
这个与第二个相同
t={x="lua"}
t={} t.x="lua" t["x"] = "lua"
使用type函数可以返回一个可以显示的字符串
表达式
算术操作符
+(加法) -(减法) *(乘法) /(除法) ^(指数) %(取模) -(负号)
x%1的结果是x的小数部分,x-x%1是整数部分
关系操作符
< > <= >= == ~=
的有操作符的运算结果都是true或false
nil仅与nil相等
不等于与c不一样,不是!=
a={} a.x=1 a.y=0
b={} b.x=1 b.y=0
c = a
assert( a==c )
assert( a~=b )
table userdata function仅做引用比较,引用的是一个就一样
逻辑操作符
and
a and b
第一个为false返回第一个值,不然返回第二个值
or
a or b
第一个为true返回第一个值,不然返回第二个值
(a and b) or c 相当于c中的a?b:c 但b不为假,太抽象慎用
not
assert(not "wangning" == false)
assert(not nil == true)
只会返回true或false
优先级
^
not # -(负号)
* / %
+ -
..
< > <= >= ~= ==
and
or
语句
赋值
多重赋值
a, b, c, d = 1, 2, 3, 4
a, b, c = 1, 2
assert(c == nil)
控制结构
if then elseif else end
while end
repeat until
for循环
数字型for
for var=exp1,exp2,exp3 do <执行体> end
exp3可以不填,默认1
泛型for
for k,v in ipairs(t) do <执行体> end
使用迭代器遍历
break和return
break用于结束循环
return用于结束函数,后跟返回结果
任何一个函数的结尾处都有一句隐式的return,所以没有返回值的函数可以不用显示写出return
break和return必须是一个块的最后一条语句或是end else until的前一条语句
如果确需要在内容中有return或break可以使用do end来控制
函数
定义
function mytest(a,b,c) <函数体> end
mytest = function(a,b,c) <函数体> end
local function mytest(a,b,c) <函数体> end
local mytest = function(a,b,c) <函数体> end
t = {} t.mytest = function(a,b,c) <函数体> end
t = { mytest = function(a,b,c) <函数体> end}
t = {} function t.mytest(a,c,b) <函数体> end
调用
基本调用
a,b,c = mytest(1,2,3)
冒号语法糖调用
o.foo(o,x) = o:foo(x)
使用冒号调用时会把冒号前的内容当作函数的第一个参数传入
多重返回值
function mytest()
return 1,2,3
end
a,b = mytest()
assert(a==1 and b==2)
a,b,c,d = mytest()
assert(c==3 and d==nil)
assert(select("#",mytest()==3))
变长参数
function mytest(...)
a,b=...
return a,b
end
a,b = mytest(1,2)
assert(a==1 and b==2)
"..."是表达式,代表变长参数如:return ...就是返回所有传入的参数
{...}代表所有变长参数的集合,可以象使用table的一样访问也可以用ipairs{...}遍历
select函数操作访问变长参数的能力
select(n, ...)访问第n个参数
select("#", ...)获得参数个数
特殊调用
当函数仅有一个参数并且是字符串时可以使用 mytest "wangning"方式,不用写()
当函数仅有一个参数并且是table时可以使用 mytest {a=1, b=2, c=3}的方式,不用写()
具名实参
function showwindow(opt)
print(opt.x)
print(opt.y)
print(opt.title)
print(opt.text)
end
showwindow {x=0, y-0, title="hi!", text="helloworld"}
闭包(词法域)
在函数内定义函数,内部函数可以访问外部函数的变量
function mytest()
local i = 0
return function()
i = i + 1
return i
end
end
my = mytest()
print(my()) --> 1
print(my()) --> 2
my1 = mytest()
print(my1()) -->1
print(my1()) -->2
print(my()) --> 3
函数式编程的基础
可以实现如迭代器之类的功能,实用性很高
这种形式就是实现了单一功能的类的对象
尾调用消除 tail-call elimination
最后一句如果是一个函数调用就是尾调用
相当于goto,也可用于状态机
当前函数真正结束,不会在栈中了
return <func>(<args>)才是真正的尾调用
function f(x) g(x) end不是尾调用,它还会回来
错误处理
assert(exp)
error("error message text")
pcall安全调用
协同程序
lua没有真正的多线程,都是使用协同程序也实现的多线程
lua是非对称式协同程序(semi-coroutine),它提供两个函数来处理执行权
任一时刻只能有一个协同程序在执行
只能自己挂起自己才会停止工作,无法从外部停止其工作
与生产者消费者模型一样互斥协同
所有协同程序的函数放在coroutine的table里
co = coroutine.create(function() print("wangning") end)
assert(type(co)=="thread")
create的参数就是协同程序执行的内容,返回一个thread类型的变量
4种状态:挂起(suspended)、运行(running)、死亡(dead)、正常(normal)
刚创建的协同程序处于挂起状态,可以用status检查状态
assert(coroutine.status(co)=="suspended")
coroutine.resume(co)用于启动或再次启动协同程序,将状态改为running
当协同程序执行完成任务函数后处于dead状态
在任务函数中使用coroutine.yield()可以挂起协同程序
然后调用coroutine.resume()再次启动已挂起协同程序
如果协同程序已经处于dead状态时,resume会返回false失败
当协同程序A唤醒另一个协同程序B时,A将停止执行,换做B执行
这时A的状态为正常状态normal
可以使用resume和yield来交换数据
第一次调用resume的额外参数(第二个以后的参数),都将视为协同程序任务函数的参数传入
之后都将成为yield的返回值
resume的第一个返回值表示成功失败,第二个以后的值都是yield的传入参数
当协同程序的任务函数执行完成后,函数的返回值会成为resume的额外返回值
co = coroutine.create(function(a,b,c)
print(a,b,c)
print(coroutine.yield(4,5,6))
return 10
end)
res, a,b,c = coroutine.resume(co,1,2,3) --> 1 2 3
print(res,a,b,c) --> true 4 5 6
res, d = coroutine.resume(co, 7,8,9) --> 7 8 9
print(res, d) --> true 10
print(coroutine.resume(co)) --> false cannot resume dead coroutine
可以使用一个协同程序负责主任务循环,判断其它协同程序状态,有闲置的就使其执行,实现多线程功能
元表与元方法
基本概念
1.lua中每个值都有一个元表
2.table和userdata可以有各自独立的元表
3.其它类型的值共享其类型所属的单一元表
4.lua在创建新table时不会创建元表
5.其它基本类型的加减等操作都有预定义操作,table只能通过元表及元方法
6.getmetatable(t)获得table的元表
7.setmetatable(t, mt)设置table的元表为mt
8.t={} mt={} setmetatable(t,mt) assert(getmetatable(t)==mt)
9.任何table都可以作为任何值的元表,一组table可以共享同一个元表
10.一个table也可以作为自己的元表
11.在lua代码中只能设置table的元表,其它类型值的元表只能在C代码中设置
算术类元方法
__add(加法)对应"+"操作符
__sub(减法)对应"-"操作符
__mul(乘法)对应"*"操作符
__div(除法)对应"/"操作符
__unm(相反数) 对应一元"-"操作符
__mod(取模)对应"%"操作符
__pow(乘幂)对应"^"操作符
__concat(连接)对应".."操作符
__len(求长度)对应"#"操作符
先找第一个值的元方法,如果没有再找第二个值的元方法,都没有报错
关系类元方法
__eq(等于)
__lt(小于)
__le(小于等于)
没有大于和不等于元方法,但可以转化实现
a~=b转化为not(a==b)
a>b转化为b<a
a>=b转化为b<=a
库定义元方法
__tostring(字符串转换)
tostring函数会用此元方法进行转换
__metatable(指向元方法)
setmetatable、getmetatable会访问这个元方法
如果设置成其它内容就可以起到保护元表的功能
__mode(弱引用table模式)
它的值是一个字符串
如果包含"k"则表示table里的key是弱引用模式
如果包含"v"则表示table里的value是弱引用模式
table访问的元方法
可以改变table行为的方法
__index(访问表中不存在的字段)
当没有这个元方法时访问不存在字段会返回nil
当有元方法时两种访问形式
作为函数时有两个参数,第一个是被访问的table,第二个是不存在的key
作为table时就从这个table里找被访问的table里不存在的这个key
通常用于实现继承特性
作为函数的时候开销会大一些,但更灵活,可以实现多重继承和缓存等功能
如果不想涉及元方法,可以使用rawget(t,i)"原始访问",不会加速代码执行
__newindex(给表中不存在的字段赋值)
当没有这个元方法时会在被访问的table里创建新字段并赋值
当有元方法时两种访问形式
作为函数时有三个参数,第一个是被访问的table,第二个是不存在的key,第三个是value
作为table时,会在这个table里赋值而不是在被访问table里赋值
可以使用rawset(t,k,v)绕过元方法赋值
可以利用这两个元方法实现很多table的特殊功能
1.具有默认值的table,把带有值的table作为__index元方法
2.跟踪table的访问
--原table
--私有化访问
--创建代码,名字相同
mt = {}
mt.__index = function(t,k)
print("access "..tostring(k))
--访问原来的table
end
mt.__newindex = function(t,k,v)
print("update "..tostring(k).." to "..tostring(v))
--更新原来的table
end
setmetatable(t, mt)
但这个例无法遍历原来的table,pairs只能操作代理table
3.只读table,__index指向被访问table,__newindex弹错
环境
全局变量table
lua把所有的全局变量存在一个table里,并把这个table赋值给一个全局变量_G
_G也在这个全局变量的table里,它就是一个普通的全局变量
可以用这种方法遍历所有全局变量 for k, v in pairs(_G) do print(k,v) end
可以使用_G["全局变量名"]来访问全局变量,与直接访问一样效果
可以通过对_G设置元表来控制对全局变量的访问行为
_G只是个全局变量,对其重新赋值则它就是别的东西了,这种情况只能通过getfenv找回全局table了
非全局的环境
lua中每个函数都可以有自己的环境
lua中加载每个程序块其实就是把这个程序块看作是一个函数来调用一下
每个程序块最后都可以有一行return xxx
如果有require后返回的就是这个xxx
所以可以想象一个程序文件被加载就是一个函数被调用了,这个文件的内容整个就是一个大函数
因为每个函数都可以有自己的环境,所以每个程序块都有自己的环境
这也正是为什么会命名为setfenv、getfenv了
setfenv(f, table)
1.第一个参数如果是函数则改变这个函数的环境为第二个参数的table
2.第一个参数如果是数字,则1代表当前函数,2代表调用自己的函数,以此类推
getfenv(f)
1.参数f的使用方法与setfenv一样
2.返回当前环境的table
setfenv(1, {})如果这样调用则所有环境变量都将无法访问,包括如print函数,因为环境table是空的
继承环境环境:
envt = {}
setmetatable(envt, {__index = _G})
setfenv(1, envt)
print("hello")
在程序块中调用setfenv(1,table)就会修改当前程序块的环境
每个新创建的函数(包括闭包)的环境都默认继承了创建者的环境
当改变当前环境后,后续创建的函数的环境都将随之改变
利用默认继承的功能方便 的实现命名空间之类的功能
可以来回切换多个环境来实现多套变量定义和值内容
函数在查找要访问的变量时总是以这种顺序,局部->全局->环境table元表的__index来查找
环境主要用于加载模块等环节,另外的一个主要用途就是当作一个独立的数据存储集全
模块与包
使用require加载模块
一个规范的模块应该返回一个table作为这个模块所有导出功能的集合
lua里没通过任何强制性语法规则要求创建模块时反回一个table
但最好这么做,因为大多lua的模块都是这么发布的
--require的实现源代码
function require(name)
if not package.loaded[name] then
local loader = findloader(name)
if loader == nil then
error("unable to load module "..name)
end
package.loaded[name] = true
local res = loader(name)
if res ~= nil then
package.loaded[name] = res
end
end
return package.loaded[name]
end
require的内部实现就是通过loadfile或loadlib来加载程序块
所以加载模块就当相于把程序块看做一个函数
然后使用模块的文件名作参数调用一次就加载完成了
只要使用require加载过一次的模块就不会再重复加载了,除非手动把package.loaded[name]=nil
已加载的模块记录在package.loaded的table里
loader是一个加载器,它会从package.preload里找对应的加载函数来实施加载
require"mod"会得到一个全局的mod变量
local m = require"mod"可以重定义导入模块的名称
require的加载路径如:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
require会以模块名来替换所有"?",每个条目用";"隔开
也就是说require只处理分号和问号
require用于搜索lua文件的路径存放在变量package.path中
当lua启动后便以环境变量LUA_PATH来初始化这个变量
如果没有找到环境变量LUA_PATH就以编译时定义的一个默认常是值来初始化
LUA_PATH里的";;"子串会替换成默认路径
如果require找不到lua文件就会去找c程序库,路径放在package.cpath里用LUA_CPATH初始化
在加载c程序库里如果模块名如a-b,则以b为模块加载
如果一个模块名为mod.sub则认为sub是mod的子模块,在require时会使用配置好的分隔符替换"."
如果mod.sub会替换为mod\sub这样就可以很容易的分目录放置了
使用module创建模块
手工写法
--创建模块-------------------------------------------------------------------------------- --require会把文件名当参数传进来,用这个变量记下来就行了
local modname = ...
--创建一个local的table用于记录导出内容
local _M = {}
--导出的table就等于这个local的table
_G[modname] = _M
--有了这句就不用return _G[modname]给require了
package.loaded[modname] = _M
--设置本环境继承全局环境,不然连接print都不能用,
--其实如果在模块中要用全局内容,可能赋给local变量然后使用
setmatetable(_M, {__index = _G})
--把这个local的table调成当前环境
setfenv(1, _M)
--模块内容 ------------------------------------------------------------------------------ --由于使用了环境,所以local根本就不会进入环境,也就不会导出了
local function test001()
print("test001")
end
--全局函数相当于_M.test002=function()end会导出
function test002()
print("test002")
end
内建module关键字
--这一句把上面创建模块部分的代码都包括了,没有package.seeall就不继承全局环境 module(...,package.seeall)
面向对象编程
对象的实现
在lua中table就是一种对象
1.有自己的状态
2.有自己的唯一标识self
3.有自己的生命周期
使用table可以自己实现面向对象的几乎所有特性
把函数定义在table中,并使用t.func的形式访问,如同方法调用
Account = {balance=0}
function Account.withdraw(v)
Account.balance = Account.ballance - v
end
但在函数中使用全局的Account是一个不好的习惯
带有标识自身对象的方法定义:
function Account.withdraw(self, v)
同上的语法糖定义:
function Account:withdraw(v)
带有标识自身对象的方法调用:
a1.withdraw(a1, v)
同上的语法糖定义:
a1:withdraw(v)
使用":"会把自身当做每一个参数隐式的传入
使用self是面向对象编程的一大核心,很多语言为程序员隐藏了这个参数
self在C++中就相当于this指针
类的实现
在lua中没有类的概念,但可以自己来实现
function Account:new(o)
--如果用户没有提供table,则创建一个
setmetatable(o, self)
self.__index = self
return o
end
当使用new函数来创建对象后(其实就是创建一个新的table),所有的访问都会从Account这个table里找
这种情况就相当于Account是一个类,也可以说是一个原型模具,所有新创建的table都拥有他的属性和方法
a = Account:new{balance=0}
a:deposit(100.00)
由于deposit在新创建的table a里没有定义
因此通过它的元表__index来查找,a的元表是Account,
因此会调用Account的deposit方法,但self传入的是a
这就实现了a继承了Account的方法deposit
在这里也看到了使用self来标识调用对象的好处
继承和派生
sa = Account:new()
s = sa:new{limit=1000.00}
第一行sa继承了Account,sa的元表是Account,找不到的方法就去Account里去找
第二行s继承了sa,这里的new是Account的方法但传入的self是sa,
致使s的元表是sa而sa的元表又是Account
所以一层一层的继承了下去,并且在每一层的派生table里都可以定义重载方法和新的方法
在lua里的可以实现多重继承,就是使元表的__index指向一个函数,然后自行判断并处理
私密性
使用table来实现面向对象的编程方式,几乎可以实现所有面向对象的编程特性
但它没有也不想去实现的就是对象的私密性,也就是c++里的private、public、protected
这与lua设计的初衷有关,lua定位于小型的程序开发,参与一个工程的人不会很多,自行约束
非要实现私密性的话lua也不是不能,只是不能再使用table和元表的方式了
可以使用函数闭包来实现私密性:
function newAccount(init)
local self = {blance=init}
local withdraw = function(v)
self.balance = self.balance - v
end
local deposit = function(v)
self.balance = self.balance + v
end
return{withdraw = withdraw, deposit = deposit}
end
在闭包里定义一个table的upvalue,然后把所有闭包函数都定义在这里table里,
然后返回这个table,用key访问内部方法
使用闭包实现对象的方式比用table效率高并实现了绝对的私密性,但无法实现继承,相当于简单的小对象
甚至可以在闭包里仅定义一个方法,然后通过key来判断调用是什么方法
Tcl/Tk对它的窗口部件就使用这种方法
弱引用table
lua使用自动内存管理机制,通过垃圾回收器来回收内存
垃圾回收器只能回收它认为是垃圾的内容,而不能回收用户认为是垃圾的内容
典型的例子栈,栈一般用一个数组和一个表示顶部的索引值表示
如果弹出一个元素,那么仅是把栈顶索引减一,
但这个元素还留在内存在被这栈数组引用着,所以垃圾回收器不知道它是垃圾
全局变量和table里的内容会被垃圾回收器回收,只能手动置为nil
因此需要一种与回收器可以协作的机制,这就是弱引用(weak reference)
lua里用弱引用table(weak table)来实现这个机制
3种弱引用table
1.具有弱引用key的table
2.具有弱引用value的table
3.同时具有弱引用key和弱引用value的table
无论是哪种类型的弱引用table,只要有一个key或value被回收了
那么他们所在的整个条目都会从table中删除
table的弱引用类型通过其元表中的__mode字段来决定,这个字段是一个字符串
1.__mode字段中如果包含"k"则是key的弱引用
2.__mode字段中如果包含"v"则是value的弱引用
3.__mode字段中包含"k"和"v"则是key、value的弱引用
a={}
b={__mode="k"}
--table a的key就是弱引用
--创建第一个key
a[key]=1
--创建第二个key
a[key]=2
--强制进行一次垃圾收集
for k,v in pairs(a) do print(v) end
--> 2
第二次key={}会覆盖第一个key,这时再没有对第一个key的引用了,所以会回收
第二个key由于还被变量key引用,所以不会回收
lua只会回收用弱引用table中的对象
如果key是number、boolean、string则不会回收,所以上例中用table来当key
可以使用弱引用table来实现缓存等机制,热数据不会被回收,不用的数据自动释放
math库
定义在math中
所有三角函数都使用弧度
指数和对数函数
取整函数
伪随机数math.random
调用时没有参数返回0~1之间的随机实数
调用时仅一个整数参数n,则返回1~n之间的随机整数
调用时如果有两个整数参数m,n,则返回m~n之间的随机整数
math.randomseed可以设置伪随机数种子
math.random使用的就是C的rand函数
数学常量
pi表示圆周率
huge表示最大数字
table库
table.insert
插入元素到数据指定位置
它会移动后续元素以空出空间
t = {10,20,30}
table.insert(t, 1, 15)
--t = {15, 10, 20, 30}
如果没有位置参数(第二个参数),将会把元素插到数组末尾
table.remove
删除数组指定位置上的元素,并将该位置后的所有元素前移
没有指定位置参数则最后一个元素
table.sort
对数组进行排序
第一个在第二个前则返回true
table.concat
连接字符串数组中的所有字符串
有一个参数可以指定插在每个字符串之间的分隔符