学习 Lua 的第二天

 

表(table)是 Lua 中的唯一的数据结构,可以近似地理解为其他编程语言里面的字典/关联数组/Map 等,但是 Lua 中的表更灵活,可以模拟出 array, list, dict, set,map 等数据结构。

定义表

Lua 表中的 key 可以是任何非 nil 的值,所以当 key 为整数时,表就相当于数组,为字符串时,表就相当于字典或这关联数组。Lua 表中的 value 的类型没有任何限制。

定义一个表使用花括号: {}

local a = {} -- 一个空表
local b = {3, 5, 7} -- 数组形式
local c = {one = 1, ['two'] = 2} -- 字典形式
local d = {red = 9, [3] = 3, ['a'] = {}} -- 混合形式

定义表时的逗号,可以用分号,没有不同。

操作表

操作表里的元素需要使用方括号:[]

assert(b[1] == 3 and b[2] == 5)
b[1] = 100
assert(c['one'] == 1)
c['three'] = 3

注意当表作为数组来使用时,下标索引必须从 1 开始。

如果 key 时字符串,我们也可以直接使用圆点符号 . 来操作,这是表就可以模拟其他语言里的类或者命名空间:

local x = {}
x['name'] = "samus"
x.job = 'hunter'
print(x.name, " : ", x['job'])

x.mission = function(dst) 
  print('fly to ', dst)
end

x.mission('Zebes')

运算符 # 可以计算数数组形式素表里面元素数量,配合 for 循环可以实现遍历:

assert(#a == 3) 
for i = 1, #a do
  print(a[i], ', ')
end

对于字典形式的表,没有办法能够直接获取元素的数量,使用 # 或返回 0.

范围循环

for 循环的范围循环形式主要用于便利表里面的元素,但需要两个标准函数库的配合: ipairs()pairs() 。这两个函数起到迭代器的作用,逐个返回表里面的 key/value。前者只适合数组形式的表,并且便利到 nil 值就结束; 后者可以支持任意形式的表,并且遍历表里面的锁哟u元素。但速度没有 ipairs() 快。

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

for k, v in pairs(x) do
  print(k, ' => ', v)
end

作为函数的参数

当表作为函数的参数时传递的是表的引用,因此函数体内可直接修改表的元素。

模块

Lua 语言基于表实现了与 C++ 的 namespace,Java 的 package 类似的机制,用户可以用模块来管理组织代码结构。

模块就是一个函数的集合,通常表现为一个 Lua 表,里面有模块作者提供的各种功能函数,使用圆点操作符访问。

使用 require 函数可以加载模块,参数是模块所在的文件名(省略后缀),通常我们需要用变量来保存 require 函数的返回结果。

local cjson = require "cjson"
local str = cjson.encode({a=1, b=2})
print(str)

编写自己的 Lua 模块也很容易,在源码文件里创建一个表,把函数作为表的元素,最后用 return 返回这个表就可以了。

local proto = {
  version = '0.1'
}

-- 模块内部数据,不对外暴露
local data = 0 

-- 定义一个函数,注意不能是 local 的
function proto.run() 
  print("run in mod")
end

-- 返回模块的表
return proto

require 在加载模块的同时会执行文件里的代码(进执行一次,所以多次 require 不会影响效率),在形式上,require 的工作相当于:

-- 定义一个临时函数
local _tmp = function()
  -- 里面是模块文件里的所有的源码
end

-- 执行这个临时函数,最后会返回模块的表
local xxx = _tmp()

面向对象

如果使用字符串作为 可以, 那么表本身就是对象,可以存储任意变量和函数:

local x = {}
x.name = 'snake'
x.mission = function() 
  
end

Lua 不提供 private, public 这样的修饰词,表里的所有成员都是公开的,如果想要实现私有成员,那么可以在模块文件里用 local 来修饰,这样 local 化的变量就对外界不可见了,而成员函数荏苒可以访问。

继承 在 Lua 中不提倡,替代方案是使用 原型(prototype)模式,从一个原型对象克隆出一个新对象,然后再动态变更其属性。

原型模式

原型模式需要使用 Lua 的高级特性 元表(metatable) 和函数 setmetatable()

元表描述了表的基本行为,有些类似 C++ 或者 Python 里的操作符重载,我们需要用的是 __index 方法,他重载了 Lua 里查找 key 的操作,也就是 table.key

函数 setmetatable(t, meta) 把表 t 的元表设置为 meta 并返回 t 。如果 meta 里设置了 __index 方法,那么对 t 的操作 t.key 也会同样作用到 meta 上,即 meta.key 。这样,表 t 就克隆了表 meta 的所有成员,表 meta 就成了表 t 的原型。

-- 声明一个原型对象,现在是空表:
local proto = {}

-- 为表添加一个方法:
function proto.go()
  print("go pikachu")
end

-- 定义元表,注意重载了 __index
local mt = { __index = proto }

-- 调用 setmetatmable 设置元表,返回新表
local obj = setmetatable({}, mt)

-- 新表对象是原型的克隆,可以执行原型的操作:
obj.go()

代码的关键操作是定义元表 mt ,里面只需要设置 __index 方法,然后再使用函数 setmetatable 从 mt 克隆出一个新对象。

这两个步骤也可以合并为一次操作:

-- 函数里面直接定义元表:
local obj = setmetatable({}, {_index = proto})

在克隆原型的时候还可以在 setmetatable 的第一个参数里添加新字段,使之成为不同于原型的新对象,通常这个方法的名字是 new:

function proto.new()
  return setmetatable({name='pokemon'}, mt)
end

self 参数

Lua 为面向对象的方式使用表内成员函数提供了一个特殊的操作符: :,它的功能与 . 基本相同,但是在调用函数时会隐含传入一个 self 参数。

self 类似 C++ / Java 里面的 this 或者 Python 里面的 Self,实际上就是对象自身,通过 self 就可以很方便地获取到对象内部成员。

:self 不仅可以在调用函数时使用,也可以在定义函数时使用:

function proto:hello() 
  print("hello", self.name)
end

local obj = proto:new()
obj:hello()

: 其实是一种语法糖,是简化的 . 写法,相当于:

function proto.hello(self) 
  print("...")
end

obj.hello(obj)

这两种方法可以使用,没有本质区别,但是建议风格一致。

标准库

Lua 的库实际上就是包含了函数成员的表。

Lua 内置的标准库很小,只提供了基本的功能,主要有:

  • base :最核心的函数
  • package :管理 Lua 的模块
  • string : 字符串相关
  • table :表相关
  • math :数学计算相关
  • io : 文件相关
  • os : 操作系统相关
  • debug : 跳使用函数

base 库常用的函数

  • assert(s) : 如果执行结果为 nil 或者 false 则报错,否则返回执行结果。
  • error(msg) : 直接引发一个错误。
  • collectgarbage(opt) :Lua 垃圾货收器的操作函数。opt 的值有很多,常用的 collect 要求立即运行垃圾回收,count 检查 Lua 的内存使用情况(KB)。
  • loadstring(string) :加载一个字符串形式的 Lua 代码片段,返回包含着短代码的函数:

Power by TeXt.