Lua 15分钟快速上手(下)

549 阅读16分钟

本系列相关文章:

Flutter 热更新及动态UI生成

Lua 15分钟快速上手(上)

Lua 15分钟快速上手(下)

Lua与C语言的互相调用

LuaDardo中Dart与Lua的相互调用

进阶语法篇

迭代器

迭代器(iterator)是一种可以让我们遍历一个集合中所有元素的代码结构。在Lua语言中,通常使用函数表示迭代器:每一次调用函数时,函数会返回集合中的“下一个”元素。

泛型for迭代器

泛型for在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。语法格式如下:

for var-list in exp-list do
    body
end

var-list是由一个或多个变量名组成的列表,以逗号分隔;exp-list是一个或多个表达式组成的列表,同样以逗号分隔。通常,表达式列表只有一个元素,即一句对迭代器工厂的调用。Lua把变量列表的第一个(或唯一的)变量称为控制变量(control variable),其值在循环过程中永远不会是nil,因为当其值为nil时循环就结束了。

实例:

array = {'Ali','Sina','Meituan'}

for key,value in ipairs(array) do
   print(key, value)
end

执行过程

  1. 初始化,计算 in 后面表达式的值,表达式应该返回通用 for 需要的三个值:迭代函数、状态常量、控制变量

    与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略

  2. 将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)

  3. 将迭代函数返回的值赋给变量列表

  4. 如果返回的第一个值为 nil 循环结束,否则执行循环体

  5. 回到第二步再次调用迭代函数

一般来说,迭代器包含两种类型:

  1. 无状态的迭代器
  2. 多状态的迭代器

无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器。使用无状态迭代器避免创建新闭包的开销。一般情况下,每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用。 一个无状态的迭代器只利用这两个值可以获取下一个元素。

ipairs就是一种无状态迭代器 ,它遍历数组的每一个元素。

实现一个无状态的迭代器:

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

function mypairs (a)
    return iter, a, 0 -- 返回三个值
end

array = {'Ali','Sina','Meituan'}

for key,value in mypairs(array) do
   print(key, value)
end

多状态的迭代器

迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,这时有两种实现方式:

  1. 使用闭包
  2. 将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量 因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数

使用闭包示例:

function walk_it(collection)
   local index = 0
   local count = #collection
   -- 闭包函数
   return function ()
      index = index + 1
      if index <= count
      then
         --  返回迭代器的当前元素
         return collection[index]
      end
   end
end

array = {'Ali','Sina','Meituan'}

for value in walk_it(array) do
   print(value)
end

“迭代器”这个名称多少有点误导性,这是因为迭代器并没有进行实际的迭代:真正的迭代是for循环完成的,迭代器只不过为每次的迭代提供连续的值。或许,称其为“生成器(generator)”更好,表示为迭代生成(generate)元素;不过,“迭代器”这个名字已在诸如Java等其他语言中被广泛使用了。

元表

元表(Metatable)是面向对象领域中的受限制类。像类一样,元表定义的是实例的行为。不过,由于元表只能给出预先定义的操作集合的行为,所以元表比类更受限;同时,元表也不支持继承。

Lua语言中的每种类型的值都有一套可预见的操作集合。例如,我们可以将数字相加,可以连接字符串,还可以在表中插入键值对等。但是,我们无法将两个表相加,无法对函数作比较,也无法调用一个字符串,这时就需要使用元表。例如,假设a和b都是表,那么可以通过元表定义Lua语言如何计算表达式a+b。当Lua语言试图将两个表相加时,它会先检查两者之一是否有元表(metatable)且该元表中是否有__add字段。如果Lua语言找到了该字段,就调用该字段对应的值,即所谓的元方法(metamethod,是一个函数),用于计算两个表的和的函数。

Lua语言中的每一个值都可以有元表。每一个表和用户数据类型都具有各自独立的元表,而其他类型的值则共享对应类型所属的同一个元表。但我们只能为表设置元表;如果要为其他类型的值设置元表,则必须通过C代码或调试库完成。

Lua 提供了两个函数来操作元表:

  1. setmetatable(table,metatable) 对指定 table 设置元表(metatable),如果元表 (metatable) 中存在 __metatable 键值,setmetatable 会失败
  2. getmetatable(table) 返回对象的元表 (metatable)

一个表可以成为任意值的元表;一组相关的表也可以共享一个描述了它们共同行为的通用元表;一个表还可以成为它自己的元表,用于描述其自身特有的行为。总之,任何配置都是合法的。

mytable = {name="zhangsan"}           -- 定义一个普通表 
mymetatable = {}                      -- 定义一个普通表作为元表
setmetatable(mytable,mymetatable)     -- 把 mymetatable 设为 mytable 的元表
getmetatable(mytable)                 -- 返回 mymetatable

-- 以上等价于下面这种简略写法
mytable = setmetatable({name="zhangsan"},{})

元方法

__index

Lua 语言中的 __index 字段是常用的表相关的元方法。如果 __index 字段对应的值是另一个表,那么当通过某个键来访问表时,这个键不存在,Lua 就会寻找该此表对应的元表中的 __index 键。也就会去另一 表中查找相应的键值。

Myable = { age = 17 }
-- 定义一个空表
t = {}

-- 给t设置元表
setmetatable(t,{__index = Myable})
print(t.age)  -- 17

如果 __index字段值是一个函数,那么 Lua 就会调用那个函数,表和键会作为参数传递给函数。当访问一个表中不存在的字段时会得到nil。实际上,这些访问会引发解释器查找一个名为__index的元方法。如果没有这个元方法,结果就是nil;否则,则由这个元方法来提供最终结果。

mytable = setmetatable({name="张三"}, 
    {
    __index = function(mytable, key)
        if key == "address" then
            return "my value"
        else
            return "not exist"
        end
    end
    })

print(mytable.name)  -- 张三
print(mytable.age)   -- not exist

这里,我们是可以利用元表的 __index 字段来模拟实现面向对象继承特性。

__newindex

元方法__newindex__index类似,不同之处在于前者用于表的更新而后者用于表的查询。当对一个表中不存在的索引赋值时,解释器就会查找__newindex元方法,如果这个元方法存在,那么解释器就调用它而不执行赋值。像元方法__index一样,如果这个元方法是一个表,解释器就在此表中执行赋值,而不是在原始的表中进行赋值。

利用这个元方法,我们可以很容易创建一个只读的表,只需要对更新操作并抛出异常即可。

__add

这里,我们通过设置元表的 __add 字段,即可实现两个表的加法运算。类似于操作符重载了。

  mytable = setmetatable({ 1, 3, 5 }, {
    __add = function(mytable, newtable)
                for k,v in ipairs(newtable) do
                    table.insert(mytable, v)
                end
                return mytable
           end})

  secondtable = {7,11,13}

  mytable = mytable + secondtable
  for k,v in ipairs(mytable) do
    print(v)
  end

元表可用的操作符字段

模式描述
__add对应的运算符 '+'
__sub对应的运算符 '-'
__mul对应的运算符 '*'
__div对应的运算符 '/'
__mod对应的运算符 '%'
__unm对应的运算符 '-'
__concat对应的运算符 '..'
__eq对应的运算符 '=='
__lt对应的运算符 '<'
__le对应的运算符 '<='

__call

元表字段__call 用于把表变量名当方法调用时的处理。

sum = setmetatable({10,28}, {
  __call = function(mytable, newtable)
    local s = 0
    for k,v in ipairs(mytable) do
        s = s + v
    end

    for k,v in ipairs(newtable) do
      s = s + v
    end
    return s
  end
})

table2 = {1,3,5}
print(sum(table2)) -- 返回两个表元素相加的总和

__tostring

修改表的打印输出行为

tb1 = {"lua","dart","go","rust"}

tb1 = setmetatable(tb1,{
  __tostring = function(mytable)
    s = "{"
    for k, v in pairs(mytable) do
        s = s..v.." ,"
      if k == #mytable then
        s = s..v.."}"
      end
    end
    return s
  end
})

print(tb1)

模块与包

模块类似于一个封装库,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

从用户观点来看,一个模块(module)就是一些代码(要么是Lua语言编写的,要么是C语言编写的),这些代码可以通过函数require加载,然后创建和返回一个表。这个表就像是某种命名空间,其中定义的内容是模块中导出的东西,比如函数和常量。

自定义一个名为 mymodule 的模块 mymodule.lua

mymodule = {}

-- 定义一个常量
mymodule.constant = "这是一个模块常量"

-- 定义一个函数
function mymodule.hello()
    print("Hello World!\n")
end

-- 定义一个私有函数
local function pfunc()
    print("这是一个定义在 mymodule 模块内的私有函数")
end

-- 定义一个公开函数调用 私有函数 pfunc
function mymodule.call_func()
    pfunc()
end

-- 返回模块
return mymodule

-- 模块定义完成

模块的本质就是定义一个 。 因此可以像操作调用表里的元素那样来操作调用模块里的常量或函数

-- 加载上面定义的模块
require("mymodule")

print(mymodule.constant)
mymodule.call_func()

也可以给模块起一个别名再调用

-- 定义一个别名 m
local m = require("mymodule")

-- 通过别名调用常量 
print(m.constant)
-- 通过别名调用函数
m.call_func()

通常函数只有一个字符串类型参数时,会省略括号,因此加载模块的代码会写作local m = require "mymodule"

模块加载机制

require 函数有着自己的文件路径加载策略,它会尝试从 Lua 文件或 C程序库中搜索并加载模块。其用于搜索 Lua 文件的路径是存放在全局变量 package.path 中。

当 Lua 启动后,会以环境变量LUA_PATH的值来初始这个全局变量。 如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。 当然,如果没有LUA_PATH这个环境变量,也可以自定义配置。

LUA_PATH文件路径以分号 ; 分隔,最后的 2 个分号;; 表示新加的路径后面加上原来的默认路径。例如:

export LUA_PATH="~/lua/?.lua;;"

如果找到目标文件,则会调用package.loadfile函数来加载模块。 否则,就会去找 C 库。搜索 C 程序库的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始化的。搜索 C 库的策略跟上面的一样,只是搜索的文件名由 .lua 换成了 .so.dll 。 如果找到,那么 require 函数就会通过package.loadlib来加载它。

Lua的面向对象

对象由属性和方法组成。Lua中最基本的结构是表,Lua可以使用表来描述对象的属性和方法,那么 Lua 中的类可以通过表+函数模拟出来,而继承,则可以通过元表模拟。

-- 使用 table 定义类
Rectangle = {area = 0, length = 0, breadth = 0}

--  类的方法 new
function Rectangle:new (o,length,breadth)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  self.length = length or 0
  self.breadth = breadth or 0
  self.area = length*breadth;
  return o
end

-- 类的方法 printArea
function Rectangle:printArea ()
  print("矩形面积: ",self.area)
end


r = Rectangle:new(nil,6,8)
print(r.length)  -- 点号访问类的属性
r:printArea()    -- 冒号访问类的成员函数

模拟继承实例:

-- 定义父类
Shape = {area = 0}

-- 定义类方法 new
function Shape:new (o,side)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  side = side or 0
  self.area = side*side;
  return o
end

-- 定义类方法 printArea
function Shape:printArea ()
  print("面积为: ",self.area)
end

-- 定义类 Square 继承 Shape
Square = Shape:new()
function Square:new (o,side)
  o = o or Shape:new(o,side)
  setmetatable(o, self)
  self.__index = self
  return o
end

-- 创建对象
mysquare = Square:new(nil,16)
mysquare:printArea()

协程

协程(coroutine) 与线程类似,拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协程共享全局变量和其它大部分东西。线程与协程的主要区别在于:

一个具有多个线程的程序可以同时运行几个线程,而协程却需要彼此协作的运行。在任一指定时刻只有一个协程在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。协程有点类似同步的多线程,在等待同一个线程锁的几个线程类似于协程。

Lua语言中协程相关的所有函数都被放在表coroutine中。函数create用于创建新协程,该函数只有一个参数,即协程要执行的代码(协程体,即一个函数)。create返回一个thread类型的值,即新协程。通常,函数create的参数是一个匿名函数,例如:

co = coroutine.create(
    function()
        print("hello")
    end
)

print(type(co))

一个协程有四种状态:

  • 挂起(suspended)

  • 运行(running)

  • 正常(normal)

  • 死亡(dead)

可以通过函数coroutine.status来检查协程的状态:

print(coroutine.status(co))

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

coroutine.resume(co) 

如果在交互命令行模式下运行上述代码,最好在最后一行加上一个分号来阻止输出函数resume的返回值。

协程的真正强大之处在于函数yield,该函数可以让一个运行中的协程挂起自己,然后在后续恢复运行。例下例:

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

coroutine.resume(co2) -- 1
coroutine.resume(co2) -- 2
coroutine.resume(co2) -- 3

首先创建了一个协程,调用resume函数开始运行协程,协程进入了一个循环,循环中代码正常执行,直到遇到第一个yield,协程会挂起,再次调用resume函数,则继续执行循环中的代码,直到遇到yield后挂起。不断调用resume,重复这个过程,在最后一次调用resume时,协程体执行完毕(超出for循环的条件,for循环退出)并返回,不输出任何数据。如果我们试图再次resume它,则resume函数将返回false和一条错误信息。

需要注意,只有当我们唤醒(调用resume)协程时,函数yield才会最终返回,这点很重要。来看一个有参数有返回值的示例:

co3 = coroutine.create(
    function (a , b)
        local sum = a + b
        coroutine.yield(sum) 
    end
)

-- 通过resume函数来传入协程体需要的两个参数a、b
-- 协程体若有返回值,则传入yield函数返回。在外部,则是通过resume函数来接收该返回值
-- 需要注意,resume函数的第一个返回值是布尔值,表示协程是否调用成功,接下来第二个返回值才是协程体返回的具体值
print(coroutine.resume(co3,2,3))

实现生产者-消费者模型

这里,我们使用lua的协程来实现一个经典的生成者消费者问题

local new_productor

-- 生产者
function productor()
     local i = 0
     while true do
          i = i + 1
          send(i)     -- 将生产的东西发给消费者
     end
end

-- 消费者
function consumer()
     while true do
          local i = receive()     -- 接收生产者的东西
          print(i)
     end
end

function receive()
     local status, value = coroutine.resume(new_productor)
     return value
end

function send(x)
     coroutine.yield(x)     -- x表示需要发送的值,值返回以后,就挂起该协程
end

-- 启动程序
new_productor = coroutine.create(productor)
consumer()

文件与 I/O

Lua的 I/O 库主要用于读取和处理文件。读写文件时分为 简单模式(和C一样)和 完全模式

  • 简单模式(simple model)拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
  • 完全模式(complete model) 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法

简单模式使用标准的 I/O 或使用一个当前输入文件和一个当前输出文件

-- 以只读方式打开文件
file = io.open("test.lua", "r")
-- 设置默认输入文件
io.input(file)

-- 打印文件第一行
print(io.read())
-- 关闭打开的文件
io.close(file)

----------------------------------------------

-- 以追加的方式打开只写文件
file = io.open("test.lua", "a")
-- 设置默认输出文件
io.output(file)

-- 在文件最后一行写入字符串"end..."
io.write("end...")
-- 关闭打开的文件
io.close(file)

简单模式可做一些简单文件操作,但是在进行一些高级的文件操作时,就显得力不从心。例如同时读取多个文件这样的操作,使用完全模式则较为合适。

-- 以只读方式打开文件
file = io.open("test.lua", "r")
-- 打印文件第一行
print(file:read())
-- 关闭打开的文件
file:close()

----------------------------------------------

-- 以追加方式打开只写文件
file = io.open("test.lua", "a")
-- 在文件最后一行写入字符串
file:write("--test")

-- 关闭打开的文件
file:close()

注意,完全模式使用file:方式调用函数,而不是io.的方式。

Lua打开文件操作的函数原型如下:

file = io.open (filename [, mode])

其中参数mode有许多可选的值:

模式描述
r以只读方式打开文件,文件必须存在
w以只写方式打开文件,若文件存在则以新的空文件覆盖;若文件不存在则新建文件
a以追加的方式打开只写文件。若文件不存在,则新建文件;若文件存在,写入的数据会被追加到末尾(EOF符保留)
r+以可读写方式打开文件,文件必须存在
w+打开可读写文件,若文件存在则以新的空文件覆盖;若文件不存在则新建文件
a+a模式类似,但文件可读可写
b以二进制模式打开
+表示对文件既可读也可写

其他的一些io 方法:

函数描述
io.tmpfile()返回一个临时文件句柄,该文件以更新模式打开,程序结束时自动删除
io.type(file)检测file是否一个可用的文件句柄
io.flush()将缓冲中的所有数据写入文件
io.lines(optional file name)返回一个迭代函数,每次调用将获得文件中的一行内容,当到文件尾时,将返回nil,但不关闭文件

Lua 15分钟快速上手(上)


关注公众号:编程之路从0到1 编程之路从0到1

或关注博主的视频网校

云课堂