Lua 学习

本节内容

  1. 迭代器与泛型for
  2. talbe
  3. metatables
  4. 协程
  5. 面向对象

1. 迭代器与泛型for

1.1 范型for为迭代循环处理所有的内容:

首先调用迭代工厂;内部保留迭代函数,因此我们不需要 iter 变量;然后在每一个新的迭代处调用迭代器函数;当迭代器返回 nil 时循环结束。

在循环过程中范性 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。

范性 for 的执行过程:

  1. in 后面表达式的值,表达式应该返回范型 for 需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  2. 常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
  3. 函数返回的值赋给变量列表。
  4. 若返回的第一个值为 nil 循环结束,否则执行循环体。
  5. 第二步再次调用迭代函数。

具体举例:

for var_1, ..., var_n in explist do block end
-- 等价于
do
        local _f, _s, _var = explist
        while true do
                local var_1, ... , var_n = _f(_s, _var)
                _var = var_1
                if _var == nil then break end
                block
        end
end

1.2 无状态迭代器:

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

自己实现ipairs举例:

a = {"one", "two", "three"}

function iter (a, i)
        i = i + 1
        local v = a[i]
        if v then
                return i, v
        end
end

function ipairs (a)
        return iter, a, 0  -- 迭代函数、状态常量、控制变量
end

for i, v in ipairs(a) do
        print(i, v)
end

Lua 库中实现的 pairs 是一个用 next 实现的原始方法:

function pairs (t)
        return next, t, nil
end

for k, v in next, t do
        ...
end

1.3 多状态的迭代器:

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

我们应该尽可能的写无状态的迭代器,因为这样循环的时候由 for 来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包;尽可能不要使用 table 这种方式,因为创建闭包的代价要比创建 table 小,另外 Lua 处理闭包要比处理 table 速度快些。后面我们还将看到另一种使用协同来创建迭代器的方式,这种方式功能更强但更复杂。

多状态迭代器举例:

local iterator

function allwords()
    local state = {line=io.read(),pos=1 }
    return iterator,state  -- 返回迭代函数,状态常量
end

function iterator(state)
    while state.line do
        local s,e = string.find(state.line,"%w+",state.pos)
        if s then
            state.pos=e+1
            return string.sub(state.line,s,e)
        else
            state.line=io.read()
            state.pos=1
        end
    end
    return nil
end

for i in allwords() do
    print(i)
end

--function allwords(f)
--    for l in io.lines() do
--        for w in string.gfind(l, "%w+") do
--            f(w)
--        end
--
--    end
--
--end
--allwords(print)

2. table

2.1 用table实现链表

举例:

list=nil  -- 链表最末尾节点

list={next=list,value=v}  -- 链表创建时往前赋值

local l=list
while l do  -- 遍历链表
    print(l.value)
    l=l.next
end

2.2 队列实现

举例:

List={}

function List.new()
    return {first=0,last=-1}
end

function List.pushleft(list,value)
    local first=list.first-1
    list.first=first
    list[first]=value
end

function List.pushright(list,value)
    local last=list.last+1
    list.last=last
    list[last]=value
end

function List.popleft(list)
    local first=list.first
    if first>list.last then error("list is empty") end
    local value = list[first]
    list[first]=nil
    list.first=first+1
    return value
end

function List.popright(list)
    local last=list.last
    if last < list.first then error("list is empty") end
    local value=list[last]
    list[last]=nil
    list.last=list.last-1
    return value
end

local a =List.new()

List.pushright(a,1)
List.pushright(a,2)
List.pushright(a,3)
List.pushright(a,4)

print(List.popright(a))  -- 4
print(List.popright(a))  -- 3
print(List.popright(a))  -- 2
print(List.popright(a))  -- 1

2.3 集合

举例:

function Set(list)
    local set={}
    for _,l in ipairs(list) do
        set[l]=true
    end
    return set
end

reserved = Set{"while", "end", "function", "local", "local","while"}  -- 已经将table中的元素去重了
for k,v in pairs(reserved)do io.write(k," ") end  -- local function end while

2.4 table的序列化

递归实现table的序列化。

举例:

function serialize(o)
    if type(o)=="number" then  -- 如果是number则直接写入
        io.write(o)
    elseif type(o)=="string" then  -- 如果是string通过format写入,用%q可以使用双引号表示字符串并且可以正确的处理包含引号和换行等特殊字符的字符串
        io.write(string.format("%q",o))
    elseif type(o)=="table" then  -- 如果是table则遍历table中的元素,递归调用serialize
        io.write("{\n")
        for k,v in pairs(o) do
--            io.write(" ",k," = ")
            io.write("[")
            serialize(k)
            io.write("] = ")
            serialize(v)
            io.write(",\n")
        end
        io.write("}\n")
    else
        error("can not serialize a " .. type(o))
    end
end

a={a="1",b="2",c={1,2,3,4,5},"q","w","e","r","t","y","u" }
serialize(a)
--    {
--        [1] = "q",
--        [2] = "w",
--        [3] = "e",
--        [4] = "r",
--        [5] = "t",
--        [6] = "y",
--        [7] = "u",
--        ["b"] = "2",
--        ["c"] = {
--            [1] = 1,
--            [2] = 2,
--            [3] = 3,
--            [4] = 4,
--            [5] = 5,
--        }
--        ,
--        ["a"] = "1",
--    }

3. metatables

Metatables 允许我们改变 table 的行为,例如,使用 Metatables 我们可以定义 Lua 如何计算两个 table 的相加操作 a+b。当 Lua 试图对两个表进行相加时,他会检查两个表是否有一个表有 Metatable,并且检查 Metatable 是否有__add 域。如果找到则调用这个__add函数(所谓的 Metamethod)去计算结果。

3.1 算数运算的metamethods

举例:

Set={}
Set.mt={}
function Set.new(t)
    local set={}
    setmetatable(set,Set.mt)
    for _,l in ipairs(t) do set[l]=true end
    return set
end

function Set.union(a,b)
    if getmetatable(a)~=Set.mt or getmetatable(b)~=Set.mt then error("attempt to `add' a set with a non-set value",2) end
    local res=Set.new{}
    for k in pairs(a) do res[k]=true end
    for k in pairs(b) do res[k]=true end
    return res
end

function Set.intersection(a,b)  -- 取a,b的交集
    local res=Set.new{}
    for k in pairs(a) do
        res[k]=b[k]
    end
    return res
end

function Set.tostring(set)
    local s="{"
    local sep=""
    for e in pairs(set) do
        s=s..sep..e
        sep=", "
    end
    return s.."}"
end

function Set.print(s)
    print(Set.tostring(s))
end

Set.mt.__add=Set.union
Set.mt.__mul=Set.intersection


s1=Set.new {10,20,30,40,50}
s2=Set.new {30,40,50,60,70}
print(getmetatable(s1))  -- table: 0x0004b540
print(getmetatable(s2))  -- table: 0x0004b540

s3=s1+s2
Set.print(s3)  -- {60, 20, 10, 70, 50, 40, 30}
Set.print((s1+s2)*s1)  -- {50, 30, 20, 40, 10}

-- 对于每一个算术运算符,metatable 都有对应的域名与其对应,除了__add、__mul 外,还有__sub(减)、__div(除)、__unm(负)、__pow(幂),
-- 我们也可以定义__concat 定义 连接行为。__call可以将table按照函数的方法调用,触发__call对应的函数执行

Set.print(s1+8)
--/usr/local/bin/luajit: metatable/metamethods1.lua:64: attempt to `add' a set with a non-set value
--stack traceback:
--[C]: in function 'error'
--metatable/metamethods1.lua:19: in function '__add'
--metatable/metamethods1.lua:64: in main chunk
--[C]: at 0x0100001770

Lua 选择 metamethod 的原则:如果第一个参数存在带有__add 域的 metatable, Lua使用它作为 metamethod,和第二个参数无关;否则第二个参数存在带有__add 域的 metatable, Lua 使用它作为 metamethod 否则报错。

3.2 关系运算的 Metamethods

举例:

Set={}
Set.mt={}
function Set.new(t)
    local set={}
    setmetatable(set,Set.mt)
    for _,l in ipairs(t) do set[l]=true end
    return set
end

function Set.union(a,b)
    if getmetatable(a)~=Set.mt or getmetatable(b)~=Set.mt then error("attempt to `add' a set with a non-set value",2) end
    local res=Set.new{}
    for k in pairs(a) do res[k]=true end
    for k in pairs(b) do res[k]=true end
    return res
end

function Set.intersection(a,b)  -- 取a,b的交集
    local res=Set.new{}
    for k in pairs(a) do
        res[k]=b[k]
    end
    return res
end

function Set.tostring(set)
    local s="{"
    local sep=""
    for e in pairs(set) do
        s=s..sep..e
        sep=", "
    end
    return s.."}"
end

function Set.print(s)
    print(Set.tostring(s))
end

Set.mt.__add=Set.union
Set.mt.__mul=Set.intersection
Set.mt.__tostring=Set.tostring  -- 特殊方法,print会调用metatable的tostring方法返回的字符串
Set.mt.__metatable = "not your business"  -- 特殊方法,加上这个方法后,就无法访问对象的metatable以及修改了


Set.mt.__le=function(a,b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    return true
end

Set.mt.__lt=function(a,b)
    return a<=b and not (b<=a)
end

Set.mt.__eq=function(a,b)
    return a<=b and b<=a
end

s1=Set.new {10,20,30,40,50,60,70}
s2=Set.new {30,40,50,60,70 }

print(s1)  -- {50, 30, 60, 70, 20, 40, 10}
print(s2)  -- {50, 70, 40, 60, 30}

print(s1<=s2)  -- false
print(s1<s2)  -- false
print(s1>=s2)  -- true
print(s1>s2)  -- true
print(s1==s1*s2)  -- false

print(getmetatable(s1))  -- not your business
print(setmetatable(s1,{}))
--/usr/local/bin/luajit: metatable/metamethods2.lua:82: cannot change a protected metatable
--stack traceback:
--    [C]: in function 'setmetatable'
--    metatable/metamethods2.lua:82: in main chunk
--    [C]: at 0x0100001770

3.3 表相关的 Metamethods

举例:

-- 通过__index设置默认值
-- 访问一个表的不存在的域,触发 lua 解释器去查找__index metamethod:如果不存在, 返回结果为 nil;如果存在则由__index metamethod 返回结果。

window={}
window.prototype={x=0,y=0,width=100,height=100 }
window.mt={}

function window.new(o)
    setmetatable(o,window.mt)
    return o
end

--window.mt.__index=function(table,key)
--    return window.prototype[key]
--end

window.mt.__index=window.prototype  -- 这样也可以,就不用特意定义一个匿名函数了

w=window.new({x=10,y=20})
print(w.width)  -- 100  # 先寻找w中,没找到width属性,去它的metatable里面调用__index方法,如果找到则返回,如果没找到返回nil

print(rawget(w,width))  -- nil  # 不会通过__index的方式去获取w中的属性,如果没有,直接返回nil

-- __newindex metamethod用来对表更新,__index则用来对表访问。当你给表的一个 缺少的域赋值,
-- 解释器就会查找__newindex metamethod:如果存在则调用这个函数而不进行赋值操作。调用 rawset(t,k,v)不调用任何 metamethod 对表 t 的 k 域赋值为 v。
------------------------------------------------------------------------------------------------------------------------
-- 使用key为{}隐藏默认值

local key={}
local mt={__index=function(t) return t[key] end }  -- 返回t[{}]的值,意思是如果没有找到key,则调用__index,永远返回t[{}]的值
function setDefault(t,d)
    t[key]=d  -- t[{}]=d
    setmetatable(t,mt)  -- 设置mt为metatable
end
table={x=10,y=20 }
print(table.x,table.z)  -- 10   nil  # 访问z找不到值
setDefault(table,0)
print(table.x,table.z)  -- 10   0  # 访问z之前已经设置了默认值


------------------------------------------------------------------------------------------------------------------------
-- 监控table的实现

local index={}

local mt={
    __index=function(t,k)
        print("*access to element" .. tostring(k))
        return t[index][k]
    end,

    __newindex=function(t,k,v)
        print("update of element " .. tostring(k) .. " to " .. tostring(v))
        t[index][k]=v
    end
}

function track(t)
    local proxy={}  -- 创建代理table
    proxy[index]=t  -- 将原始table赋值到代理table的{}键上,外部不能访问
    setmetatable(proxy,mt)  -- 将mt设置到proxy的metatable上,当从proxy读取不到值的时候就必然会调用metatable的__index,就可以实现监控
    return proxy
end

table={x=10,y=20}

table=track(table)
print(table.x)  -- *access to elementx  -- 10
table.z=30  -- update of element z to 30

------------------------------------------------------------------------------------------------------------------------
-- 只读__index实现

function readOnly(t)
    local proxy={}
    local mt={
        __index=t,
        __newindex=function()  -- 控制修改表,保持Proxy里面是空的,那么修改表就一定会走__newindex方法
            error("attempt to update a read-only table",2)
        end
    }
    setmetatable(proxy,mt)
    return proxy
end

days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday"}

print(days[2])  -- Monday
days[2]="Noday"
--/usr/local/bin/luajit: metatable/metamethods3.lua:91: attempt to update a read-only table
--stack traceback:
--[C]: in function 'error'
--metatable/metamethods3.lua:80: in function '__newindex'
--metatable/metamethods3.lua:91: in main chunk
--[C]: at 0x0100001770

4. 协程

4.1 协程实现的基础:

Lua 的所有协同函数存放于 coroutine table 中。 create 函数用于创建新的协同程序,其只有一个参数:一个函数,即协同程序将要运行的代码。若一切顺利,返回值为 thread类型,表示创建成功。通常情况下, create 的参数是匿名函数。

协程有三个状态:挂起态(suspended)、运行态(running)、停止态(dead)。创建协同程序成功时,其为挂起态,即此时协同程序并未运行。我们可用 status 函数检查协同的状态。

举例:

-- 协程创建,状态,执行

co= coroutine.create(function()  -- 创建协程,此时处于挂起状态
    print("hello world")
end)

print(co)  -- thread: 0x0004afa0
print(coroutine.status(co))  -- suspended
coroutine.resume(co)  -- hello world  -- 运行协程,得到想要的输出。resume 运行在保护模式下,因此,如果协同程序内部存在错误, Lua 并不会抛出错误,而是将错误返回给 resume 函数。
print(coroutine.status(co))  -- dead  -- 协程结束,检查状态为dead

4.2 yield的使用:

举例:

-- yield挂起协程执行

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

coroutine.resume(co)  -- co     1
print(coroutine.status(co))  -- suspended
coroutine.resume(co)  -- co     2
coroutine.resume(co)  -- co     3
coroutine.resume(co)  -- co     4
coroutine.resume(co)  -- co     5
coroutine.resume(co)  -- co     6
coroutine.resume(co)  -- co     7
coroutine.resume(co)  -- co     8
coroutine.resume(co)  -- co     9
coroutine.resume(co)  -- co     10
print(coroutine.resume(co))  -- true  # 最后一个yield后面的代码被执行,这时候彻底执行完了该协程
print(coroutine.resume(co))  -- false   cannot resume dead coroutine  # 再去执行协程,返回false,提示报错 协程已经到了dead状态


-- yield传递参数

co=coroutine.create(function(a,b,c)
    coroutine.yield(a+b,a-c)
end)

print(coroutine.resume(co,1,2,3))  -- true      3       -2  # 返回yield传回来的结果
print(coroutine.resume(co))  -- true
print(coroutine.resume(co))  -- false   cannot resume dead coroutine

co=coroutine.create(function()
    print("co",coroutine.yield())  -- co        1       2  # 传递参数给yield
    a,b=coroutine.yield()
    print(a,b)  -- 1    2
end)

coroutine.resume(co)
coroutine.resume(co,1,2)
coroutine.resume(co,1,2)


-- return结果返回

co=coroutine.create(function()
    return 4,5
end)

print(coroutine.resume(co))  -- true    4       5  # return的结果被resume接收了

4.3 管道和过滤器

举例:

-- 协同是一种非抢占式的多线 程。管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在 独立的协同代码中。
-- 管道在读(consumer)与写(producer)之间提供了一个缓冲,因此 两者相关的的速度没有什么限制,在上下文管道中这是非常重要的,
-- 因为在进程间的切 换代价是很高的。协同模式下,任务间的切换代价较小,与函数调用相当,因此读写可 以很好的协同处理。
function receive(prod)  -- 执行协程,将拿到结果返回
    local status,value=coroutine.resume(prod)
    return value
end

function send(x)  -- yield 夯住,等待下次调用
    coroutine.yield(x)
end

function producer()  -- 创建一个协程,读取输入,并调用send,返回输入的值,并夯住
    return coroutine.create(function()
        while true do
            local x=io.read()
            send(x)
        end
    end)
end

function filter(prod)  -- 创建一个协程,调用receive执行producer创建的协程,拿到结果后格式化,后调用send,返回格式化后的结果,并夯住
    return coroutine.create(function()
        local line = 1
        while true do
            local x= receive(prod)
            x=string.format("%5d %s",line,x)
            send(x)
            line=line+1
        end
    end)
end

function consumer(prod)  -- 循环调用receive执行filter创建的协程,并接受返回结果,打印出来
    while true do
        local x=receive(prod)
        io.write(x,"\n")
    end
end

consumer(filter(producer()))  -- 多层嵌套执行协程

4.4 用作迭代器的协同

举例:

-- 第一版,用递归迭代器实现输出所有组合
function Permgen(a,n)
    if n==0 then
        printResult(a)
    else
        for i=1,n do  -- 将列表前面所有的数值和最后一个数值替换,都能出现一种情况,然后递归调用,将遍历出所有组合的情况
            a[n],a[i]=a[i],a[n]
            Permgen(a,n-1)
            a[n],a[i]=a[i],a[n]
        end
    end
end


function printResult(a)
    for i,v in ipairs(a) do
        io.write(v," ")
    end
    io.write("\n")
end

Permgen({1,2,3},3)
--2 3 1
--3 2 1
--3 1 2
--1 3 2
--2 1 3
--1 2 3

-------------------------------------------------------------------------------------------------------------------------
-- 使用协程替换迭代器实现


function Permgen(a,n)
    if n==0 then
        coroutine.yield(a)
    else
        for i=1,n do
            a[n],a[i]=a[i],a[n]
            Permgen(a,n-1)
            a[n],a[i]=a[i],a[n]
        end
    end
end

function printResult(a)
    for i,v in ipairs(a) do
        io.write(v," ")
    end
    io.write("\n")
end

--function perm(a)  -- 在这种情况下,被下面的方法替代了
--    local n=table.getn(a)
--    local co=coroutine.create(function()Permgen(a,n) end)
--    return function()
--        local code,res=coroutine.resume(co)
--        return res
--    end
--end

-- 一般情况下,coroutine.wrap 比 coroutine.create 使用起来简单直观,前者更确切的提 供了我们所需要的:一个可以 resume 协同的函数,
-- 然而缺少灵活性,没有办法知道 wrap 所创建的协同的状态,也没有办法检查错误的发生。
function perm (a)
    local n = table.getn(a)
    return coroutine.wrap(function () Permgen(a, n) end)
end

for p in perm{"a","b","c"} do

    printResult(p)

end

5. 面向对象

Lua 不存在类的概念,每个对象定义他自己的行为并拥有自己的形状(shape)。然而,依据基于原型(prototype)的语言比如 Self 和 NewtonScript,在 Lua中仿效类的概念并不难。 在这些语言中, 对象没有类。 相反, 每个对象都有一个 prototype(原型),当调用不属于对象的某些操作时,会最先会到 prototype 中查找这些操作。在这类语言中实现类(class)的机制,我们创建一个对象,作为其它对象的原型即可(原型对象为类,其它对象为类的 instance)。类与 prototype 的工作机制相同,都是定义了特定对象的行为。

5.1 类的基本实现

举例:

-- 定义方法的时候带上一个额外的参数,来表示方法作用的对象。这个参数经常为 self 或者 this
-- self 参数的使用是很多面向对象语言的要点。大多数 OO 语言将这种机制隐藏起来,这样程序员不必声明这个参数(虽然仍然可以在方法内使用这个参数)。
-- Lua 也提供了通过使用冒号操作符来隐藏这个参数的声明。冒号的效果相当于在函数定义和函数调用的时候,增加一个额外的隐藏参数。

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

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

Account.deposit(Account,200.00)
Account:withdraw(100.00)
print(Account.balance)

5.2 类的继承与多重继承

举例:

-- 类的继承

Account={balance=0 }

function Account:new(o)
    o=o or {}
    setmetatable(o,self)
    self.__index=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
end

SpecialAccount=Account:new()  -- Account的子类

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

function SpecialAccount:getLimit()
    return self.limit or 0
end

s=SpecialAccount:new({limit=1000.00})  -- SpecialAccount的子类

function s:getLimit()  -- 如果子类s中定义了getLimit,则不会调用SpecialAccount中的getLimit
    return self.limit*0.10 or 0
end

--s:withdraw(200)  -- insufficient funds

--print(s.balance)  -- insufficient funds

-- SpecialAccount 从 Account 继承了 new 方法,当 new 执行的时候, self 参数指向SpecialAccount。所以, s 的 metatable 是 SpecialAccount, __index 也是 SpecialAccount。这样, s 继承了 SpecialAccount,后者继承了 Account。当我们执行:s:deposit(100.00) Lua 在 s 中找不到 deposit 域,他会到 SpecialAccount 中查找,在 SpecialAccount 中找不到,会到 Account 中查找。使得 SpecialAccount 特殊之处在于,它可以重定义从父类中继承来的方法(继承和重写父类方法)
------------------------------------------------------------------------------------------------------------------------
-- 多继承

local function search(k,plist)
    for i=1,table.getn(plist) do
        local v=plist[i][k]
        if v then return v end
    end
end

function creatClass(...)
    local c={}
    local args={...}  -- 这里注意,...虽然在lua中是代表不定参数,但是要获取这些不定参数,需要使用{...}获取,会生成一个tables,里面放了所有的参数,以及一个key为n,对应总共有多少个参数被接受。
    setmetatable(c,{__index=function(t,k)
--        return search(k,args)
        local v=search(k,args)   -- 这样改造,访问到一个函数在t中没有的,则从父类中找到该方法后,将该方法赋值给子类t中,
                                -- 加快之后访问该方法的速度,缺点是无法在之后修改父类的方法,因为修改后不会影响到子类
        t[k]=v
        return v
    end})
    c.__index=c

    function c:new(o)
        o=o or {}
        setmetatable(o,c)
        return o
    end

    return c

end

Named = {}
function Named:getname()
    return self.name
end

function Named:setname(n)
    self.name = n
end

NamedAccount=creatClass(Account,Named)

account=NamedAccount:new({name="Paul"})

print(account:getname())