lua 遍历删除_Lua入门

4e8206ac85292d9c746ce0ff2b27a4a9.png

整理一下Lua的基本语法和常识。

全局变量

Lua中,全局变量无需声明即可使用,使用未初始化的全局变量也不会导致错误,只是结果为nilnil是一种和其他所有值进行区分的类型,表示无效的值,一个全局变量在第一次被赋值前的默认值就是nil,而将nil赋值给全局变量则相当于将其删除。

Boolean类型

  • Lua中将除falsenil外的所有其他值视为真。
  • not只返回truefalse
  • and运算结果为:如果它的第一个操作数为false,则返回第一个操作数,否则返回第二个操作数。
  • or运算结果为:如果它的第一个操作数不为false,则返回第一个操作数,否则返回第二个操作数。

有一种常用的表达式:a and b or c(注意and的优先级比or高),如表达式x > y and x or y会选出x和y中较大的一个。

独立解释器(standalone interpreter)

我们用lua命令lua [options] [script [args]]来运行lua脚本

数值

以下公式是取模运算的定义:

a % b == a - ((a // b) * b),注意这里(a//b) * b会根据整数和小数的不同,得到抹去最后几位数字为零的结果。

对整型操作数来说,取模运算结果的符号永远和第二个操作符一致。对于实数类型的操作数来说,x - x % 0.01恰好是x保留两位小数的结果。

小于2^53的所有整形值的表示与双精度浮点型值的表示一样,对于绝对值超过了这个值的整型而言,将其转换为浮点型可能导致精度损失。顺带一提,将一个整型值加上0.0就能转换为浮点型,将浮点型与0按位或运算就能转化为整型。

解决八皇后问题

检查斜对角的方法比较巧妙,8x8的棋盘上如果从第一排摆棋子直到最后一排,那么每次检查只需要检查当前排上面的排,无非就是三种情况,同列、左对角、右对角,而检查左右对角线时可以利用列减行的套路,如果两个棋子在对角线上,那么其坐标中的纵坐标与横坐标的差必定相等。

N = 8;
function printBoard(a)
    for i = 0, N - 1 do
        for j = 0, N - 1 do
            io.write(a[i] == j and "X" or "-", " ")
        end
        io.write("n")
    end
    io.write("n")
end

--check if the queen on row n, column col can be attacked
function check(a, n, col)
    for i = 0, n - 1 do
        if(a[i] == col) or --self column
            (a[i] - i == col - n) or --left corner
            (a[i] + i == col + n) then -- rigt corner
            return false
        end
    end
    return true
end

count = 0
--在第n排放皇后
function placeQueen(a, n)
    if(n > N - 1) then
        --printBoard(a)
        count = count + 1
        return
    end
    for col = 0, N - 1 do --go for columns
        if check(a, n, col) then
            a[n] = col --record column
            placeQueen(a, n + 1)
        end
    end
end

placeQueen({}, 0)
print(count)

注意这个算法并不是最优。

表(table)

可以把表的索引当成成员名称使用,即a.name与a["name"]等价,注意是a["name"]不是a[name]。

表构造器{}可以用来初始化表,如果初始化时没有定义键值,则默认从1开始,而不是从0开始,Lua很多机制都遵循这个惯例。

我们可以用pairs遍历表中的键值对,遍历中元素出现的顺序是随机的,且每个元素只会出现一次,对于没有指定键(key)的列表,可以用ipairs迭代器:

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

--> 1  10
--> 2 function: 0x420610
--> 3 12
--> 4 hi

此时会确保遍历是按照顺序进行。

函数

Lua中函数是第一类值,所有函数都是匿名的,函数并没有名字,调用时实际上访问的是保存该函数的变量,通常该变量为全局变量,从而看起来函数有了一个名字,但在很多场合下依然会保留函数的匿名性。

举例来说,foo = function(x) return 2 * x end就是将一个函数赋值给了变量foo

table.sort函数可以为我们排序表中的元素,

network = {
    {name = "aaa", IP = "192.1.1.1"},
    {name = "bbb", IP = "192.1.1.2"},
    -- ...
}
table.sort(network, function (a, b) return (a.name > b.name) end)

再比如,导数的计算公式为f'(x) = (f(x + d) - f(x)) / d,d趋于无穷小,我们可以写出计算导数的函数,

function derivative(f, delta)
    delta = delta or 1e-4
    return function (x)
        return f(f(x + delta) - f(x)) / delta
    end
end

myCos = derivative(math.sin)

非全局函数

由于函数是第一类值,因此函数还可以被储存在表字段和局部变量中

lib = {
    foo = function (x, y) return x + y end,
    goo = function (x, y) return x - y end
}
--或者
lib = {}
function lib.foo(x, y) return x + y end
function lib.goo(x, y) return x - y end

还可以用关键词local来将一个函数声明为局部函数

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

注意上面我们不能直接对fact函数初始化,否则在递归调用的时候会在函数体中尝试寻找全局的fact函数,但是它并不存在。

词法定界(lexical scoping)和闭包(Closure)

这是一个很重要的概念,如果一个函数A包含函数B,即函数A中包含了函数B,函数B中的代码其实可以访问函数A中的局部变量,此时在函数B中该变量既不是局部变量也不是全局变量,而是所谓的非局部变量(non-local variable),也被称为上值(upvalue)。

function myCounter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

c1 = myCounter()
print(c1()) ---> 1
print(c1()) ---> 2

c2 = myCounter()
print(c2()) ---> 1
print(c2()) ---> 2
print(c2()) ---> 3

上述代码就是一个产生闭包的例子,函数myCounter中的匿名函数访问了非局部变量count,此时它与被赋值的函数名称绑定,在每次运行时加1,因此当再次将函数myCounter赋值给c2时,新的count与新的闭包被创建,就会重新开始计数。此时c1c2是不同的闭包,但建立在相同的函数上,拥有各自的非局部变量。

以下是wiki对闭包的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。

闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。

闭包也被经常用来重新定义函数

do
    local oldSin = math.sin
    math.sin = function(x)
        return oldSin(x * (math.pi / 180))
    end
end

这段代码中do被用来限制局部变量oldSin的作用范围。

画个圆

function circle(cx, cy, cr)
    return function(x, y)
        return (x - cx) ^ 2 + (y - cy) ^ 2 <= cr ^ 2
    end
end

function plot(area, width, height)
    f = io.open("img.pbm", 'w')
    f:write("P1n", width, " ", height, "n") -- 位图
    for j = 1, height do
        for i = 1, width do
            f:write(area(i, j) and "1" or "0")
        end
        f:write("n")
    end
    f:close()
end

c = circle(400, 300, 100) -- 在图中心,半径100个像素
plot(c, 800, 600)

数据结构

Lua中的表并不是一种数据结构,而是其他数据结构的基础。

元表

元表定义的是实例的行为,但它只能给出预先定义的操作集合的行为,可以看作是oop中受限制的类,每一个表和用户数据类型都具有各自独立的元表,Lua中在创建新表时不带元表,可以用函数setmetatable来设置或修改元表,通常我们也只能为表设置元表。

字符串标准库为所有的字符串都设置了同一个元表(函数getmetatable用来获取元表),而其他类型在默认情况下都没有元表。

一个表可以成为任意值的元表,甚至成为自身的元表,用于描述自身特有的行为。

下面是一个集合的例子,来表现元表的用法,集合中有得到并集和交集的方法,现在为了让两个集合能直接用运算符+来计算并集,x来计算交集,我们在创建每个集合的函数中加入创建元表的代码:

local Set = {}
local mt = {} --meta table

function Set.new(l)
    local set = {}
    setmetatable(set, mt) --in this way, all sets created by this method share the same meta table
    for _, v in ipairs(l) do 
        set[v] = true 
    end
    return set
end

function Set.union(a, b)
    local res = Set.new{}
    print("a: ")
    for k in pairs(a) do
        --print(k)
        res[k] = true
    end
    print("b: ")
    for k in pairs(b) do
        res[k] = true
        --print(k)
    end
    return res
end

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

function Set.printSet(set)
    for k, v in pairs(set) do print(k, v) end
end

set1 = Set.new{10, 20, 30, 40}
set2 = Set.new{50, 70, 30}
mt.__add = Set.union -- 将方法union与加法运算符绑定
mt.__mul = Set.intersection -- 将方法intersection与乘法运算符绑定
Set.printSet(set1 + set2) --> 10, 20, 30, 40, 50, 70
print()
Set.printSet(set1 * set2) --> 30

还有诸如对运算符==(eq)、<=(le)、<(__lt)的扩展,就不展开了。另外如果需要将元表对用户隐藏,只需要在原表中设置__metatable字段,那么getmetatable就会返回这个字段的值,而setmetatable则会引发一个错误。

总的来说元表就是可以修改一个值在面对位置操作时的行为,通常是用户自定义的操作。其中若干定义具体操作的方法,比如上面例子的__add被称为元方法

__index元方法

当访问一个表中不存在的字段时会得到nil,而事实上,系统会在这种情况发生时去查找一个名为__index的元方法,如果它存在,那么就由它来提供最终的结果。

根据这个特性,我们可以为要定义的表事先定义一个原型表,如果后面在初始化时只初始化了部分字段,其余字段就会由原型中的默认值提供,而不是直接返回nil了。

prototype = {x = 0, y = 0, width = 100, height = 100}
local mt = {}
function new(o)
    setmetatable(o, mt)
    return o
end

mt.__index = function (_, key)
    return prototype[key]
end

w = new{x = 10, y = 20}
print(w.width) --> 100

我们也可以将__index定义成一个函数,这样可以为不存在的索引提供一个统一的默认值。

local mt = {__index = function(t) return t.___ end}
function setDefault(t, d)
    t.___ = d
    setmetatable(t, mt)
end

如果我们不希望在访问一个表时调用__index元方法,可以使用函数rawget(table, i),但这并不会加快代码的执行。

__newindex元方法

index元方法类似,只是newindex用于表的更新而非查找,如果对一个表中不存在的索引赋值,解释器就会查找__newindex元方法,如果它存在,解释器就会调用它而不执行赋值,如果它是一个表,就会在这个表中进行赋值,而不是在原始的表中进行赋值。同样的,如果我们不希望在赋值时调用它,可以使用函数rawset(table, k, v)

跟踪对表的访问

我们用index和newindex跟踪对某个表的所有访问,但这两个元方法都是在表为空时才会被调用,因此如果需要监控一个表,要创建一个表代理(proxy),它是一个空表,用于追踪所有访问并将访问重新定向到原来的表,下面是实现代码:

function track(t)
    local proxy = {}

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

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

        __pairs = function()
            return function(_, k)
                local nextkey, nextvalue = next(t, k)
                if nextkey ~= nil then
                    print("*traversing element " .. tostring(nextkey))
                end
                return nextkey, nextvalue
            end
        end,

        __len = function() 
            return #t 
        end
    }

    setmetatable(proxy, mt)

    return proxy
end

function test(t)
    print("{")
    for k, v in pairs(t) do
        print(k, v)
    end
    print("}")
end

t = {}
t2 = track(t)
t2[2] = "hello"
print(t2[2])
print(t)

此时t指向的是我们建立的空表proxy,但赋值和取值操作都在它的元表中被重定向到之前的表t了。需要遍历时,迭代器pairs会找到代理表中的__paris方法为我们输出表中的数据。

如果需要监控多个表,并不用为每个表创建不同的元表,只需要将每个代理与其原始表映射起来,然后让所有的代理共享一个公共的元表即可。

只读的表

根据上面学到的,要创建一个只读的表并不难,一样用代理的方法,将原始表赋值给__index,然后将__newindex设置为抛出异常:

function readOnlyTable(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(t, k, v)
            error("permission denied", 2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

OOP

创建一个账户Account,其中有存钱、取钱的功能:

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

--use ":" to hide "self" parameter
function Account:deposit(amount)
    self.balance = self.balance + amount
end

Account.deposit(Account, 200)
print(Account.balance) ---> 200
Account:withdraw(100)
print(Account.balance) ---> 100

如果需要创建多个银行账户,那么可以把Account作为一个原型类,后面的账户都是它生成的实例,那么可以将实例中元表的__index = Account

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

思路很简单,在Account“类”中新建一个“成员函数”new用来生成实例,它只做一件事,就是将新的表的元表设置成我们定义好的mt,那么这个用new方法新建的表就会含有Account中的“成员”。

而事实上,我们并不需要mt做转换,可以直接将Account本身作为元表,在它的成员中加入元方法__index,由上面的思路,这个元方法就是它自己。这里有一点绕,需要理清思路想清楚逻辑,优化后的代码如下:

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

这种写法下Account直接作为实例的元表,其中元方法__index就是它自己,如果生成的实例中某种行为或变量没有定义,则会到元表也就是Account中去查看是否存在元方法__index,然后会发现存在并且就是Account,那么就会在Account中查找有没有前面需要调用的成员。注意这里并不会像递归那样无限重复这个过程,因为Lua只会对定义了元表的值去检查,而Account自己是没有定义任何元表的。

继承

有了类就会有继承,如果我们需要某些基类没有提供的新特性,比如某个VIP账号可以透支,那么就需要创建新的取钱方法,此时相当于基类派生出了子类,并且子类复写了基类提供的取钱方法,基类提供了默认的取钱方法,但子类可以有自己的取钱实现,如果需要就自己去实现,即多态。下面是对Account基类派生子类VIP_Account的代码:

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

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

VIP_Account = Account:new()
function VIP_Account:withdraw(amount)
    if(amount - self.balance > self:getLimit()) then
        error("insufficient funds")
    end
    self.balance = self.balance - amount
end

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

vip = VIP_Account:new{ limit = 1000 }
account = Account:new()
vip:deposit(2000)
vip:withdraw(1000)
vip:withdraw(1500)
print()
account:deposit(2000)
account:withdraw(1000)
account:withdraw(1500)

--deposit done, balance:    2000
--withdraw done, balance:   1000
--withdraw done, balance:   -500

--deposit done, balance:    2000
--withdraw done, balance:   1000
--insufficient funds

子类VIP_Account和基类的取钱方法名称相同,因此解释器在调用时会直接用子类的版本而不会去访问Account中的取钱方法。

有意思的是,我们创建VIP_Account这个子类时其实调用的是Account中生成实例的方法new,它本质上还是一个表,我们在这个表中加入了新的方法withdrawgetLimit,然后再用它去调用new,Lua会发现VIP_Account这个表中没有这个方法,于是在原表中的__index(也就是Account)中去找,从而调用了Accountnew方法,这个过程就可以看作是VIP_Account这个类从基类Account中继承了方法new。 可见在Lua中实例和类的概念是模糊的,一个表既可以是一个实例也可以是一个类,生成新的实例,这和传统的语言如Java、C++都很不一样。上面的例子中VIP_AccountAccount中其实都是一个表,只要弄清楚它们之间的关系就很好理解了。

多继承

根据上面的思路,如果一个类需要继承自多个父类,我们需要将__index变成一个可以在多个父类中搜索成员变量或函数的函数,同时需要一个专门的函数createClass来创建这个有多个父类的子类:

--find member k in parents
function search(parents, k)
    for i = 1, #parents do
        local v = parents[i][k]
        if v then return v end
    end
end

--create multi inherit class
function createClass(...)
    local c = {}
    local parents = {...}
    setmetatable(c, { __index = function(t, k)
        return search(parents, k)
    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(_name)
    self.name = _name
end

NamedAccount = createClass(Account, Named)
namedAccount = NamedAccount:new{name = "Tizeng"}
print(namedAccount:getName())
namedAccount:setName("Tizeng Yan")
print(namedAccount:getName())
namedAccount:deposit(1000)
namedAccount:withdraw(1000)
namedAccount:withdraw(10)

--Tizeng
--Tizeng Yan
--deposit done, balance:    1000
--withdraw done, balance:   0
--insufficient funds

在创建新类时,在createClass函数的参数中输入要继承的父类,新类元表的__index就不再是一个表,而是一个在父类中搜索成员的函数,然后就是比较让人混淆的一点,createClass用来创建新派生的子类,而其中的new函数用来生成新类的实例,思路和之前定义Account类一样,将自己作为新类的元表,并将元方法__index定义成自己。将new的定义放在createClass中是因为只有在创建了新类后才能创建它包含的函数,这样一来根据闭包的特性,生成的每个实例的元表都会是c

一个类不能同时成为它实例和子类的元表。

私有性

在Lua中,我们通过把需要隐藏的成员与函数绑定成闭包来实现成员的私有化,直接用代码举例:

function newAccount(_balance)
    local self = {balance = _balance}

    local withdraw = function(amount)
        self.balance = self.balance - amount
    end

    local deposit = function(amount)
        self.balance = self.balance + amount
    end

    local getBalance = function() return self.balance end

    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end

acc = newAccount(100)
acc.withdraw(40)
print(acc.getBalance()) --> 60

上面的代码中,返回的表只有三个“公有”的方法,对“私有”成员self进行操作,self对外部不可见,调用这些方法时,我们也不需要之前的:操作符。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值