lua 中神奇的表(table)
创始人
2024-03-01 23:26:10
0

最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被 表 ( 表 ) 在 lua 中的应用所镇住了。

表在 lua 中真的是无处不在:首先,它可以作为字典和数组来用;此外,它还可以被用于设置闭包环境、模块;甚至可以用来模拟对象和类。

字典

表最基础的作用就是当成字典来用。 它的键可以是除了 nil 之外的任何类型的值。

t={}
t[{}] = "table"                 -- key 可以是表
t[1] = "int"                    -- key 可以是整数
t[1.1] = "double"               -- key 可以是小数
t[function () end] = "function" -- key 可以是函数
t[true] = "Boolean"             -- key 可以是布尔值
t["abc"] = "String"             -- key 可以是字符串
t[io.stdout] = "userdata"       -- key 可以是userdata
t[coroutine.create(function () end)] = "Thread" -- key可以是thread

当把表当成字典来用时,可以使用 pairs 函数来进行遍历。

for k,v in pairs(t) do
  print(k,"->",v)
end

运行结果为:

1   ->  int
1.1 ->  double
thread: 0x220bb08   ->  Thread
table: 0x220b670    ->  table
abc ->  String
file (0x7f34a81ef5c0)   ->  userdata
function: 0x220b340 ->  function
true    ->  Boolean

从结果中你还可以发现,使用 pairs 进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。

表 中的键最常见的两种类型就是整数型和字符串类型。 当键为字符串时,表 可以当成结构体来用。同时形如 t["field"] 这种形式的写法可以简写成 t.field 这种形式。

数组

当键为整数时,表 就可以当成数组来用。而且这个数组是一个 索引从 1 开始 、没有固定长度、可以根据需要自动增长的数组。

a = {}
for i=0,5 do                    -- 注意,这里故意写成了i从0开始
  a[i] = 0
end

当将表当成数组来用时,可以通过长度操作符 # 来获取数组的长度:

print(#a)

结果为:

5

你会发现, lua 认为数组 a 中只有 5 个元素,到底是哪 5 个元素呢?我们可以使用使用 ipairs 对数组进行遍历:

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

结果为:

1   0
2   0
3   0
4   0
5   0

从结果中你会发现 a 的 0 号索引并不认为是数组中的一个元素,从而也验证了 lua 中的数组是从 1 开始索引的

另外,将表当成数组来用时,一定要注意索引不连贯的情况,这种情况下 # 计算长度时会变得很诡异。

a = {}
for i=1,5 do
  a[i] = 0
end
a[8] = 0                        -- 虽然索引不连贯,但长度是以最大索引为准
print(#a)
a[100] = 0                      -- 索引不连贯,而且长度不再以最大索引为准了
print(#a)

结果为:

8
8

而使用 ipairs 对数组进行遍历时,只会从 1 遍历到索引中断处。

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

结果为:

1   0
2   0
3   0
4   0
5   0

环境(命名空间)

lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。

为了方便,lua 在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。因此,在未手工设置环境的情况下,可以使用 -G[varname] 来存取全局变量的值。

for k,v in pairs(_G) do
  print(k,"->",v)
end
rawequal    ->  function: 0x41c2a0
require ->  function: 0x1ea4e70
_VERSION    ->  Lua 5.3
debug   ->  table: 0x1ea8ad0
string  ->  table: 0x1ea74b0
xpcall  ->  function: 0x41c720
select  ->  function: 0x41bea0
package ->  table: 0x1ea4820
assert  ->  function: 0x41cc50
pcall   ->  function: 0x41cd10
next    ->  function: 0x41c450
tostring    ->  function: 0x41be70
_G  ->  table: 0x1ea2b80
coroutine   ->  table: 0x1ea4ee0
unpack  ->  function: 0x424fa0
loadstring  ->  function: 0x41ca00
setmetatable    ->  function: 0x41c7e0
rawlen  ->  function: 0x41c250
bit32   ->  table: 0x1ea8fc0
utf8    ->  table: 0x1ea8650
math    ->  table: 0x1ea7770
collectgarbage  ->  function: 0x41c650
rawset  ->  function: 0x41c1b0
os  ->  table: 0x1ea6840
pairs   ->  function: 0x41c950
arg ->  table: 0x1ea9450
table   ->  table: 0x1ea5130
tonumber    ->  function: 0x41bf40
io  ->  table: 0x1ea5430
loadfile    ->  function: 0x41cb10
error   ->  function: 0x41c5c0
load    ->  function: 0x41ca00
print   ->  function: 0x41c2e0
dofile  ->  function: 0x41cbd0
rawget  ->  function: 0x41c200
type    ->  function: 0x41be10
getmetatable    ->  function: 0x41cb80
module  ->  function: 0x1ea4e00
ipairs  ->  function: 0x41c970

从 lua 5.2 开始,可以通过修改 _ENV 这个值(lua 5.1 中的 setfenv 从 5.2 开始被废除)来设置某个函数的环境,从而让这个函数中的执行语句在一个新的环境中查找全局变量的值。

a=1                             -- 全局变量中a=1
local env={a=10,print=_G.print} -- 新环境中a=10,并且确保能访问到全局的print函数
function f1()
  local _ENV=env
  print("in f1:a=",a)
  a=a*10                        -- 修改的是新环境中的a值
end

f1()
print("globally:a=",a)
print("env.a=",env.a)
in f1:a=    10
globally:a= 1
env.a=  100

另外,新创建的闭包都继承了创建它的函数的环境。

模块

lua 中的模块也是通过返回一个表来供模块使用者来使用的。 这个表中包含的是模块中所导出的所有东西,包括函数和常量。

定义模块的一般模板为:

module(模块名, package.seeall)

其中 module(模块名) 的作用类似于:

local modname = 模块名
local M = {}                    -- M即为存放模块所有函数及常数的table
_G[modname] = M
package.loaded[modname] = M
setmetatable(M,{__index=_G})    -- package.seeall可以使全局环境_G对当前环境可见
local _ENV = M                  -- 设置当前的运行环境为 M,这样后续所有代码都不需要限定模块名了,所定义的所有函数自动变成M的成员
<函数定义以及常量定义>

return M                        -- module函数会帮你返回module table,而无需手工返回

对象

lua 中之所以可以把表当成对象来用是因为:

  1. 函数在 lua 中是一类值,你可以直接存取表中的函数值。 这使得一个表既可以有自己的状态,也可以有自己的行为:
Account = {balance = 0}
function Account.withdraw(v)
  Account.balance = Account.balance - v
end
  1. lua 支持闭包,这个特性可以用来模拟对象的私有成员变量:
function new_account(b)
  local balance = b
  return {withdraw = function (v) balance = balance -v end,
          get_balance = function () return balance end
  }
end

a1 = new_account(1000)
a1.withdraw(10)
print(a1.get_balance())
990

不过,上面第一种定义对象的方法有一个缺陷,那就是方法与 Account 这个名称绑定死了。 也就是说,这个对象的名称必须为 Accout 否则就会出错。

a = Account
Account = nil
a.withdraw(10)                  -- 会报错,因为Accout.balance不再存在

为了解决这个问题,我们可以给 withdraw 方法多一个参数用于指向对象本身。

Account = {balance=100}
function Account.withdraw(self,v)
  self.balance = self.balance - v
end
a = Account
Account = nil
a.withdraw(a,10)                  -- 没问题,这个时候 self 指向的是a,因此会去寻找 a.balance
print(a.balance)
90

不过由于第一个参数 self 几乎总是指向调用方法的对象本身,因此 lua 提供了一种语法糖形式 object:method(...) 用于隐藏 self 参数的定义及传递。这里冒号的作用有两个,其在定义函数时往函数中地一个参数的位置添加一个额外的隐藏参数 sef, 而在调用时传递一个额外的隐藏参数 self 到地一个参数位置。 即 function object:method(v) end 等价于 function object.method(self,v) end, object:method(v) 等价于 object.method(object,v)

当涉及到类和继承时,就要用到元表和元方法了。事实上,对于 lua 来说,对象和类并不存在一个严格的划分。

当一个对象被另一个表的 __index 元方法所引用时,表就能引用该对象中所定义的方法,因此也就可以理解为对象变成了表的类。

类定义的一般模板为:

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

或者:

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

相比之下,第二种写法可以多省略一个表。

另外有一点我觉得有必要说明的就是 lua 中的元方法是在元表中定义的,而不是对象本身定义的,这一点跟其他面向对象的语言比较不同。

lua

相关内容

不一致的.lua脚本Log...
要解决“不一致的.lua脚本Logitech MoveMouseR...
2025-01-11 08:01:05
不同的“mae”值使用“m...
使用“model.evaluate”和“mean_absolute...
2025-01-08 22:01:41
不同的generator在...
解决方法是确保在两个函数中使用相同的batch size和shuf...
2025-01-08 14:01:05
不使用lua模块匹配ngi...
以下是一个示例的解决方法,使用Nginx的内置变量和指令来匹配请求...
2024-12-29 01:02:02
布雷森汉姆的抛物线算法,L...
布雷森汉姆(Bresenham)算法是一种用于绘制直线、折线和圆的...
2024-12-26 07:00:52
捕获错误:“Evaluat...
这个错误通常表示在代码中引用了未定义的变量或函数。要解决此问题,需...
2024-12-24 22:31:32

热门资讯

Helix:高级 Linux ... 说到 基于终端的文本编辑器,通常 Vim、Emacs 和 Nano 受到了关注。这并不意味着没有其他...
使用 KRAWL 扫描 Kub... 用 KRAWL 脚本来识别 Kubernetes Pod 和容器中的错误。当你使用 Kubernet...
JStock:Linux 上不... 如果你在股票市场做投资,那么你可能非常清楚投资组合管理计划有多重要。管理投资组合的目标是依据你能承受...
通过 SaltStack 管理... 我在搜索Puppet的替代品时,偶然间碰到了Salt。我喜欢puppet,但是我又爱上Salt了:)...
Epic 游戏商店现在可在 S... 现在可以在 Steam Deck 上运行 Epic 游戏商店了,几乎无懈可击! 但是,它是非官方的。...
《Apex 英雄》正式可在 S... 《Apex 英雄》现已通过 Steam Deck 验证,这使其成为支持 Linux 的顶级多人游戏之...
如何在 Github 上创建一... 学习如何复刻一个仓库,进行更改,并要求维护人员审查并合并它。你知道如何使用 git 了,你有一个 G...
2024 开年,LLUG 和你... Hi,Linuxer,2024 新年伊始,不知道你是否已经准备好迎接新的一年~ 2024 年,Lin...
什么是 KDE Connect... 什么是 KDE Connect?它的主要特性是什么?它应该如何安装?本文提供了基本的使用指南。科技日...
Opera 浏览器内置的 VP... 昨天我们报道过 Opera 浏览器内置了 VPN 服务,用户打开它可以防止他们的在线活动被窥视。不过...