Origin

Lua

1、入门

简单的求阶乘程序:

-- 求fact
function fact(n)
    if n == 0 then
        return 1
    else 
        return n* fact(n-1)
    end
end


print("Enter a number:")
a = io.read("*n")
print(fact(a))

可以用lua -i命令执行完脚本后进入交互模式。或者用-l加载lib

lua -i fact.lua (lua -lfact)
> fact(3)
> 6

还可以通过dofile来在交互模式下进行加载模块。

> dofile("fact.lua")
> fact(6)
> 720

一些词法规范

Lua语言中的 “下划线 + 大写字母” (_VERSION)组成的标识符通常被用作特殊用途,避免使用。”下划线+小写字母”用作哑变量。

单行注释是–

多行注释是

--[[
statemetns;
]]

statements会被忽略,如果启动这段代码可以在注释前添加一个-。

---[[
statemetns;
]]

lua的分号是可选的,即

a=1
b=a+2;

a=1
b=a+2

上述两个块是等价的。

Lua不区分未初始化的变量和被赋值为nil的变量,赋值为nil会回收相应的变量内存。

类型和值

lua有8种基本类型,如下所示:

type(nil)	 --> 			nil
type(true)	 --> 		  boolean
type(10.4 * 3)	 --> 	number
type("hi!")	 --> 			string
type(io.stdin)	 --> 	userdata
type(print)	 --> 			function
type(type)	 --> 			thread
type({})	 --> 				table
type(type(X))	 --> 	string	

userdata用来表示由应用或C语言编写的库创建的新的类型。

Lua中除false和nil的其他值都为真,即使是0和空字符串。 Lua的逻辑运算符 and or not:

  • and 的运算结果为:如果它第一个操作数为false,返回第一个操作数,否则返回第二个操作数。
  • or的运算结果为:如果它第一个操作数不为false,返回第一个操作数,否则返回第二个操作数。
  • not永远返回Boolean类型的值。

Lua中的一些特殊写法:

x= x or v
--等价于
if not x then x = v end
(x>y) and x or y --表示选出x,y中的最大值

2、 数值

Lua5.3后数值有两种选择,64位int型或者双精度浮点型。

由于整型和浮点型的类型都是number,可以相互转化,相同算术值的是相等的:

  1 == 1.0
  -3 == 3.0
  0.2e3 == 200 		

可以用math区分整型值和浮点型值

math.type(3) --> integer
math.type(3.0) --> float		

数学库提供三个取整函数: floor、ceil、modf

  • floor向负无穷取整, floor(3.3) =3
  • ceil向正无穷取整。
  • modf向0取整。

3、字符串

Lua中的字符串是不可变的。

可以用#操作符获取字符串长度

a = "hello"
print(#a)                             --> 5
print(#"good bye")       --> 8		

返回的是字符串占用的字节数,在某些编码中可能与字符的个数不同。

字符串连接使用…运算符。

a= "hello"
a.."World"  -> "Hello World"

多行字符串使用[[ ]]来括起来。可以用\z跳转到下一个非空白符,相当于C语言中的\。

Lua在运行时提供了数值和字符串之间的自动转化,针对字符穿的所有算术操作会尝试将字符串转化为数值.。同时也可以通过tonumber 和 tostring函数进行显式的类型转换。tonumber可以指定进制。

tonumber("3") --> 3
tonumber("10e") --> nil (not a valid number)
tonumber("100101",2) --> 37

Lua中多行字符串可用用[[ … ]]。包裹起来。如果最后的字符也是]],那么可以用[==[…]==]来包裹。即在两个[之间加入任意个=,不过要保证前后的=个数相同,如:

[=[
a=b[c[i]]
]=]

4、表

lua表要么是值要么是变量,他们都是对象(object)。Lua不会进行隐藏的拷贝或者创建新的表。

a={}
a.x =10
a["x"] =12

在lua中a.x与a[“x”]是相同的,表示在表a中查找键为”x”的值。表的键可以是任意类型,比如数值和字符串,注意

a["10"] =3
a[10] =4

上面两个是不同的键。

表构造器

最简单的表构造器是{}

days = {"Sunday","Monday", "Tuesday"}	
a = {x=10, y=20}
a ={}; a.x=10;a.y=20;

上面第二个表达式和第三个表达式相同。但第二个会提前判断表的大小,会更快。

对于特殊的键不能用上述的构造,如+,=等符号作为键。可以用下面的方式:

opnames = {["+"]="add", ["-"]="sub", 
						["*"]="mul", ["/"]="div"}

下面集中构造器相互等价:

{x=0, y=0} <--> {["x"]=0, ["y"]=0}
{"r", "g", "b"} <--> {[1]="r", [2]="g", [3]="b"}

可以用#符号得到表的长度。

遍历表

可以用pairs便利表的键值对:

t = {10, print, x=12, k="hi"}
for k, v in pairs(t) do
	print(k,v)
end

对于列表可以用ipairs迭代器:

t = {10, print, 12, "hi"}
for k, v in ipairs(t) do
	print(k,v)
end

此时得到的k是索引下标: 1234

表的标准库

常用的两个函数 insert 和remvoe。

table.insert 向序列的指定位置插入一个元素,其他元素后移。

table.remove 删除指定位置的元素,然后将后面元素前移。

table.remove(a, f, e, t)

table.move函数讲表a从索引f到e的元素移动到位置t上。

-- 插入一个元素
table.move(a, 1, #a, 2)
a[1] = newElement

--删除一个元素
table.move(a, 2, #a, 1)
a[#a] =  nil

5、函数

在Lua语言中,函数是对语句和表达式进行抽象的主要方式。函数既可以用与完成某种特定的任务,也可以只是进行一些计算然后返回计算结果。调用函数时如果只有一个参数且参数是字符串常量或者表构造器时,可以省略()。

print "Hello world"
f {x=10, y = 20}

Lua函数传递的值可以和定义的不一样,传入的多余参数会被丢弃,如果没有传入指定的参数那么会将对应的参数置为nil。应用这一特点可以构造带有默认参数的函数:

function incCount (n)
	n = n or 1
	globalCounter = globarCounter + n
end

n的默认参数是1。

Lua的函数返回值可以返回多个值:

function foo0 () end									-----返回0个结果
function foo1() return "a" end				------返回1个结果
function foo2() return "a", "b" end			----返回2个结果

只有当函数调用是一系列表达式中的最(唯一)一个表达式时才会返回多值结果,否则返回一个结果:

x,y = foo2(),20  --x="a", y = 20
x,y = foo0(),20, 30   -- x=nil, y =20

同样只有当函数调用是表达式列表中最后一个时才会返回多值:

t= {foo0(), foo2(), 4} -- t[1]=nil, t[2]="a", t[3]=4

讲函数调用用一对圆括号括起来可以强制只返回一个结果。

print ((foo0())) --> nil
print ((foo1())) --> a
print ((foo2())) -->a	

可变参数

Lua中函数的参数可以是可变参数,用…来表示。

function add(...)
    local s = 0
    for _, v in ipairs{...} do
        s = s + v 
    end
    return s
end
print (add(3,4,5,6,7))

可以用可变参数给局部变量赋值:

function foo(...)
	local a, b ,c =...
end

上述可以模拟lua中的普通函数参数传递机制。

同时可变参数可以和固定参数结合。要遍历可变长参数,可以用{…}组合成一个表。如果变长参数中有nil那么获得的表不是一个有效的序列。此时可用用table.pack函数得到一个表,该表保存了nil值,且有一个多余n用来表示表的大小。

function nonils (...)
    local arg = table.pack(...)
    for i=1, arg.n do
        if arg[i] == nil then return false end
    end
    return true
end    

print(nonils(2,3,nil))   -->false
print(nonils(2,3))  -->true

还可以用函数select访问变长的参数,如果selector的数值为n,那么函数select则返回第n个参数后的所有参数,selector还可以是字符串”#”表示返回额外参数的总数。

select(1, "a" , "b", "c")           -->a, b ,c
select(2, "a" , "b", "c")           -->b ,c
select(3, "a" , "b", "c")           -->,c
select("#", "a" , "b", "c")           -->3

可以用select实现上面的add

function add(...)
    local s = 0
    for i=1, select("#", ...)do
        s = s + select(i, ...)
    end
    return s
end
print (add(3,4,5,6,7))

对于参数较少的情况,第二个add比较快,因为不用创建一个新的表。但参数较多第一个版本的add较快。

table.unpack函数可以将一个数组解包为多个返回元素:

print (table.unpack{10,20,30})  --> 10,20,30
a,b = table.unpack{10,20,30}       --> a=10, b =20	

正确的尾调用

Lua支持尾调用消除。尾调用(tail call)是被当做函数调用使用的跳转。当一个函数的最后一个动作是调用另外一个函数没有在进行其他工作时,就形成了尾调用。如:

function f (x) x = x+1; return (x) end

当g返回时直接返回到调用f的位置。

判断尾调用的方式是怕段你在调用后是否进行其他的操作,如下面的都不是:

return g(x) + 1
return x or g(x)
return (g(x))

只有 return func(args)的调用才是尾调用。func可以是复杂的表达式。

return x[i].foo(x[j] + a* b, i +j)

上述是尾调用。

6、输入输出

简单IO模型

io.input(filename)打开文件,之后的所有io.read()都会从文件中读取,除非再次调用io.input()。io.output()同理。

io.write()类似于print(),但是捕获对输出的内容隐式地转化为string类型,且不会自动添加\n。一般与string.format配合使用。

io.write("sin(3) = ",math.sin(3), "\n")
io.write(string.format("sin(3) = %.4f\n", math.sin(3)))

io.read可以从当前输入流中读取字符串,其参数决定了读取的数据。见下表

字符 意义
“a” 读取整个文件
“l” 读取下一行(丢弃换行符)
“L” 读取下一行(保留换行符)
“n” 读取一个数值
num 以字符串读取num个字符

逐行迭代一个文件,可以使用io.lines

local count = 0
for line in io.lines() do
	count = count + 1
	io.write(string.format("%6d ",count),line, "\n")
end

read可以读取块,即指定大小的内容

while true do
	local block = io.read(2^13)  -- 块大小是8kb
	if not block then break end
	io.write(block)
end	

完整IO模型

类似与c语言,可以用io.open()打开文件读取。

print(io.open("/etc/passwd", "r")) 

当发生错误时,会返回nil,以及系统相关的错误码。

读取文件示例:

local f = assert(io.open(filename, "r")) -- assert用来判断不是nil
local t = f:read("a")
f:close()

IO库提供了3个C语言流的句柄:

io.stdin; io.stdout; io.stderr
io.stderr::write(message)

可以切换默认的输入流:

local temp = io.input()		--- 保存当前输入流
io.input("newinput")		----打开一个新的当前输入流
(some operations)
io.input():close()					---关闭当前流
io.input(temp)						-- 回复此前的流

其他的文件操作

io.tmpfile返回一个临时文件,以读写方式打开。程序结束后会自动删除。

flush()用来刷新流,setvbuf设置流的缓冲模式。可以设置为”full”, “no”, “line”。

7、补充知识

局部变量和代码块

Lua中的变量默认情况下是全局的变量,所有局部变量在使用前必须声明。可以用local关键字声明一个局部变量:

while i<=x do
	local x = i*2   --对于循环来说是局部的
	print(x)   -- 2,4,6,8....		
	i = i +1
end

8、闭包

Lua中所有函数都是匿名的,当讨论函数名时,实际上指的是保存该函数的变量。如下面两个函数定义是等价的:

function foo(x) return 2* x end
foo = function(x) return 2*x end

函数可以存储在表字段和局部变量中:

Lib={}
Lib.foo = function(x, y) return x+ y end
Lib.goo = function(x,y) return x-y emd

除此之外,Lua还可以这样定义这类函数:

Lib={}
function Lib.foo(x,y)  return x+ y end
function Lib.goo(x,y) return x-y emd

局部函数

当把一个函数存储到局部变量时,就得到一个局部函数,即一个被限定在指定作用域的函数。

local function f(params)
	body
end

注意定义局部递归函数时需要提前声明函数,否则其递归的调用会选择全局函数:

local fact
fact = function(n)
	if n==0 then return 1
	else return n* fact(n-1)
	end
end

Lua在展开局部函数语法糖时,使用的展开方式如下:

local function foo(params) body end
-- 被展开为:
local foo; foo = function(params) body end

因此使用该方式定义的局部函数不会出现递归问题。

9、模式匹配

模式匹配相关函数

string.find用于在指定目标字符串中搜索指定的模式。返回两个值,匹配到模式开始位置的索引和结束位置的索引。没有返回nil。

s = "hello world"
i, j = string.find(s, "hello")
print(i,j)        ---> 1,5	

find函数第3、4个参数是可选参数,第三个参数是一个索引,表示从哪个位置开始搜索。第四个参数是一个bool值,用于说明是否进行简单搜索。简单搜索会忽略模式匹配的专用字符,比如’[’ ,’*‘等。

string.match()也用于在一个字符串中搜索,返回匹配那部分的子串。

data = "Today is 17/7/1990"
d = string.match(date, "%d+/%d+/%d+")
print(d) --> 17/7/1990

string.gsub()用于全局替换,3个必选参数:目标字符串、模式和替换字符串。第4个参数为可选参数,表示替换次数,函数返回两个结果,替换后的字符和替换的次数。

s = string.gsub("add lii", "l", "x",1)
print(s)  --> axl lii

第三个参数可以是函数或表,用来生成替换的字符串。

string.gmatch()返回一个函数,通过返回的函数可以遍历一个字符串中所有出现的指定模式。

---找出 s中出现的所有单词
s = "some string"
words={}
for w in string.gmatch(s, "%a+") do
	words[#words + 1] = w
end

模式

Lua中不使用\而是使用%作为转义字符。如%a 表示所有字母。常用的转义字符如下表所示:

显示 表示的字符
. 任意字符
%a 字母
%c 控制字符
%d 数字
%g 除空格外的可打印字符
%l 小写字母
%p 标点符号
%s 空白字符
%u 大写字母
%w 字母和数字
%x 十六进制数字

这些类的大写形式表示类的补集,%A表示非字母的字符。

print((string.gsub("hello, up-down!", "%A",".")))
---> hello..up.down

捕获

string.match()可以返回捕获的字符串

pair = "name = Anna"
key, value = string.match(pair, "(%a)%s*=%s*(%a+)")
print(key, value) --> name ,Anna

10、日期和时间

os.time()会以数字形式返回当前的日期和时间。

如果以日期表传入参数调用os.time(),会返回该日期表的secondes

> os.time({year=2015, month=8,  day=15, hour=12, min=45, sec=20})
	--> 1439653520

os.date()根据秒数返回一个可读的日期。

os.data("*t", 90600490)
--会返回一个表 *t 表示格式化字符串

os.date可以格式化日期

print(os.date("a %A in %B")) --> a Tuesday in May
print(os.date("%d/%m/%Y"), 90600490)  --> 16/09/1998

11、数据结构

数组

Lua中用整数来索引表即可实现数组。

local a={}		-----新数组
for i=1, 1000 do
	a[i] = 0
end

也可以用表构造器创建和初始化数组

suqres= {1, 4, 9, 16};

多维数组(比如矩阵)可以用数组的数组来表示:

local mt={}
for i=1, N do
	local row = {}
	mt[i] = row
	for j=1, M do
		row[j]=0
	end
end

链表

list = nil
list = {next = list, value=v}
--- 遍历链表
local l = list
while l do
	visit l.value
	l = l.next
end

12、数据文件和序列化

数据文件

对于自定义的数据文件格式,可以高效的读取每条数据。假如有以下的数据格式文件 entry.txt

Entry{
	"Donald E Knuth",
	"Literate Programming",
	"CSLI",
	"1992"
}

Entry{
	"Donald E Knuth",
	"Literate Programming",
	"CSLI",
	"1992"
}

可以用下面代码统计entry的个数以及作者:

local count = 0
local authors = {}
function Entry(b)
	authors[b[1]] = true 
	count = count + 1
end
dofile("entry.txt")

13、 编译执行

Lua中dofile函数可以加载文件然后执行其代码,它只是loadfile的一个包装。loadfile只返回错误码不跑出异常,可以认为:

function dofile(filename)
	local f = assert(loadfile(filename))
	return f()
end

loadfile只是返回一个函数没有调用,儿而dofile会自动地调用。

load()函数类似于loadfile,不过是从字符串中读取。

f = load("i = i+1")
i=0
f(); print(i)   --> 1
f(); print(i)   --> 2

-- 下面两个语句等价
f = load("i=i+1")
f=function() i= i+1 end

上面两个函数的定义第二个要快一些,因为它会与其外层函数一起编译。

Lua中将所有独立的代码段当做匿名可变长参数函数的函数体,例如下面两条等价:

load("a=1")
function(...) a=1 end

错误处理

assert()函数检查第一个参数是否为真,如果为真则返回该参数,否则引发一个错误,第二个参数是一个可选的错误信息。

n = io.read()
assert(tonumber(n), "invalid input")

可以用pcall保护地调用函数。pcall会调用它的第一个参数,无论是否有错误发生,pcall都不会引发错误。如果没有错误发生,那么pcal返回true及被调用函数的所有返回值。否则返回false以及错误信息。

返回的err是一个错误对象。

local status, err = pcal(function() error({code=121}) end)
print(err.code)  --> 121

函数error的第二个参数是调用的层数,int值。

function foo(str)
	if type(str) ~= "string" then
		error("string expected", 2)
	end
	--regular code
end

14、模块和包

Lua中模块可以用require导入,,返回一个表。

local m = require "math"
print(m.sin(3.14))

编写模块的基本方法

local M = {}

function M.add(c1, c2)
	return c1 + c2
end

-- 其他的一些函数

--最后返回模块
return M

也可以直接将模块放入到package.loaded,可以省略最后的返回

local M = {}
package.loaded[...]  = M

还可以直接在最后返回模块:

local M = {}

function add(c1, c2)
	return c1 + c2
end

-- 其他的一些函数

--最后返回模块
return {
    add = add,
}

关于子模块,require “a.b”时会尝试搜索下面的目录:

./a/b.lua
/usr/local/lua/a/b.lua
/usr/local/lua/a/b/init/lua

这种行为使得一个包中的所有模块能够放在一个目录中,例如,一个具有模块 p 、 p.a 和 P .b 的包对应的文件可以分别是 p/init. lua 、 p/a.lua 和 p/b.lua ,目录 p 又位于其他合适的目 录中 。

15、元表

Lua中的每一个表都有一个元表用来存储元方法,可以在元表中添加元方法。

t = {}
print(getmetatable(t)) --> nil

只能给类型为表的元素设置元表。例子见set.lua。

__index元方法只有在表没有该键时才会调用。它可以定义为一个表,表示在该表的查询操作。

__newindex方法用于表的更新,当对一个表中不存在的索引赋值时,就会查找该方法,如果这个方法存在那么调用它不进行赋值,否则会进行赋值。

可以使用代理的方法实现只读的表:

--返回一个readoly的table,以传入的参数t为原型
function readOnly1(t)
    proxy = {}
    mt = {
        __index = t,
        __newindex = function(t, k, k) 
            error("attempt to update a read-only table",2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

16、面向对象

Lua中一张表就是一个对象,表与对象一样,可以拥有状态。其次,表与对象一样,拥有一个与其值无关的标识(self)。表可以有自己的操作:

Account = {balance = 0}
function Account.withdraw(v)
	Account.balance = Account.balance - v
end

这个函数可以称为Account表的方法,但是该函数只有在对象保存在特定的全局变量中时才能工作。

还有一种有效的方法是对操作的接受者进行操作,方法需要一个额外的参数来标识接受者。通常为self。

function Account.withdraw(self, v)
	self.balance = self.balance - v
end

a1 = Account; Account = nil
a1.withdraw(a1, 100.0)

同时可以创建新的对象调用同一个方法:

a2 = {balance = 0, withdraw = Account.withdraw}
a2.withdraw(a2, 260)

可以使用’:’隐藏self

function Account:withdraw( v)
	self.balance = self.balance - v
end
-- 也可以简化调用
a2:withdraw(260)  -- a2.withdraw(a2, 260)

Lua采用原型模式来模拟类,类似JavaScript。每一个类都有一个原型,该原型也是一个普通的对象。对象并不属于类。如果两个对象A和B,要让B成为A的一个原型,只需要:

setmetatable(A, {__index=B})

在此之后, A 就会在 B 中查找所有它没有 的操作 。 如果把 B 看作对象 A 的类, 则只不过是术 语上的一个变化。上述例子可以优化为:

function Account:new(o)
	o = o or {}
	self.__index = self
	setmetatable(o, self)
	return o 
end

调用Account:new()时,隐藏的参数self得到的实参是Account,Account.__index等于Account,并且Account被用作新对象的元表。

继承

假设有一个类Account:

Account = {balance = 0}

function Account:new(o)
    -- body
    o = o or {}
    self.__index = self
    setmetatable(o, self)
    return o
end


function Account:deposit(v)
    self.balance = self.balance + v
end

function Account:withdraw(v)
    if v>self.balance then error "insufficient funds" end
    self.balance = self.balance - v
    -- body
end

若想要根据Account派生出一个类,可以用:

SpecialAccount = Account:new()
s = SpecialAccount:new{limit = 1000.00}

s是一个元表为SpecialAccount的对象,当调用s的withdraw方法时首先在s中查找,没有找到,然后在SepcialAccount表中查找,没有找到,然后在SepcialAccount的元表Account中查找找到了,这种方式类似继承。

还可以重写SpecialAccount的方法。

function SepcialAccount:withdraw(v)
	..
end

17、环境

lua中有一个全局环境表_G。我们定义的变量实际上存在于全局环境表中

a= "10"
print (a)   ---> 10
print (_G["a"])  -->10

可以给全局环境表设置代理:

setmetatable(_G, {
	__newindex = function(_, n)
		error("attempt to write to undeclared variable ".. n, 2)
		end,
		__index = function(_, n)
			error("attempt  to read undeclared variable" .. n ,2)
		end,
})

这时访问一个未声明的变量会抛出错误,为了新加一个元素到表中,可以用rawset跳过元表的访问。

function declare(name, initval)
	rawset(_G, name. initval or false)
end

非全局环境

lua中没有全局的变量。

local z = 10
x  = y+z

等价于下面的代码

local _ENV = some value
return function(...)
	local z = 10
	_ENV.x  = _ENV.y +z
end

Lua语言中处理全局变量的方式:

  • 编译器在编译所有代码段前,在外层创建局部变量_ENV
  • 编译器讲所有自由名称var变换为_ENV.var
  • 函数load(或函数loadfile)使用全局环境初始化代码段的第一个上值,即Lua语言内部维护的一个普通的表。

通常_G和ENV指向同一个表。但是它们是很不一样的实体。ENV是一个局部变量,所有对“全局变量”的访问实际上访问的都是 _ ENV 。 _G 则是一个在任何情况下都没有任何特殊状态的全局变量 。 按照定义,一 ENV 永远指向的是当前的环境;而假设在可见且无人改变过其值的前提下, _G 通常指向的是全局环境 。

18、协程

Lua中协程有4种状态,挂起(suspended), 运行(running), 正常(normal)和死亡(dead)。可以通过coroutine.status来检查协程的状态。

co = coroutine.create(function() print("hi") end)
print (coroutine.status(co))   --- suspended

当一个协程被创建时,它处于挂起状态,即协程不会在被创建时自动执行。函数coroutine.result用于启动或再次启动一个协程的执行,并将其状态由挂起改为运行:

contine.resume(co)  --> hi

yield函数可以让一个运行中的协程挂起自己,然后在后续恢复运行。

co = coroutine.create(function ()
	for i=1, 10 do
		print("co", i)
		coroutine.yield()
		end
	end)
	

Lua中通过一对resume-yield来交换数据。第一个resume函数会包所有的额外参数传递给协程的主函数:

co = coroutine.create(function (a, b, c)
	print("co",a,b,c+2)
	end)
coroutine.resume(co, 1,2,3) --> co  2 5

在函数 coroutine. resume 的返回值中,第一个返回值为 true 时表示没有错误,之后 的返回 值对应函数 yield 的参数。

co = coroutine.create(function (a,b)
	coroutine.yield(a+b, a-b)
	end)
print(coroutine.resume(co, 20,10)) --> true 30 10

与之对应的是,函数coroutine.yield的返回值是对应的resume的参数:

```lua co = croutine.create(function (x)) print(“co1”, x) print(“co2”, coroutine.yield()) end) coroutine.resume(co, “hi”) –>co1 hi coroutine.resume(co, 4, 5) –> co2 4 5