Lua 表(table)

2023-11-12

介绍

表(Table)是Lua语言中最主要(事实上也是唯一的)和强大的数据结构。使用表,Lua语言可以以一一种简单、统一且高效的方式表示数组、集合、记录和其他很多数据结构。Lua语言也使用表来表示包( package )和其他对象。当调用函数math.sin时,我们可能认为是“调用了math库中函数sin”; 而对于Lua语言来说,其实际含义是“以字符串"sin"为键检索表math”。

Lua语言中的表本质上是一种辅助数组( associative array ),这种数组不仅可以使用数值作为索引,也可以使用字符串或其他任意类型的值作为索引( nil除外)。

Lua语言中的表要么是值要么是变量,它们都是对象(object) 。如果读者对Java或Scheme中的数组比较熟悉,那么应该很容易理解上述概念。可以认为,表是一种动态分配的对象,程序只能操作指向表的引用(或指针)。除此以外,Lua语言不会进行隐藏的拷贝( hidden copies )或创建新的表。

初始化

  1. local t = {}                                     -- 创建一个空表
  2. local t = {x = 10, y = 10}               -- 创建一个包含x,y元素的表
  3. local t = {};t.x = 10; t.y = 10        -- 创建一个包含x,y元素的表

上述2/3中创建表的方式是等价的,不过在第二种写法中,由于可以提前判断表的大小,所以运行速度更快。

删除

删除表的元素只需要把对应元素赋值为nil便可。

t.x = nil

内部结构(数组和哈希)以及运行方式

一般情况下,你不需要知道Lua实现表的细节,就可以使用它。实际上,Lua花了很多功夫来隐藏内部的实现细节。但是,实现细节揭示了表操作的性能开销情况。因此,要优化使用表的程序(这里特指Lua程序),了解一些表的实现细节是很有好处的。

Lua的表的实现使用了一些很聪明的算法。每个Lua表的内部包含两个部分:数组部分和哈希部分。

哈希部分使用哈希算法来保存和查找键。它使用被称为开放地址表的实现方式,意思是说所有的元素都保存在哈希数组中。用一个哈希函数来获取一个键对应的索引;如果存在冲突的话(意即,如果两个键产生了同一个哈希值),这些键将会被放入一个链表,其中每个元素对应一个数组项。当Lua需要向表中添加一个新的键,但哈希数组已满时,Lua将会重新哈希。重新哈希的第一步是决定新的数组部分和哈希部分的大小。因此,Lua遍历所有的元素,计数并对其进行归类,然后为数组部分选择一个大小,这个大小相当于能使数组部分超过一半的空间都被填满的2的最大的幂;然后为哈希部分选择一个大小,相当于正好能容纳哈希部分所有元素的2的最小的幂。

当Lua创建空表时,两个部分的大小都是0。因此,没有为其分配数组。让我们看一看当执行下面的代码时会发生什么:

local a = {}
for i = 1, 3 do
    a[i] = true
end

这段代码始于创建一个空表。在循环的第一次迭代中,赋值语句

a[1] = true

触发了一次重新哈希;Lua将数组部分的大小设为1,哈希部分依然为空;第二次迭代时

a[2] = true

触发了另一次重新哈希,将数组部分扩大为2.最终,第三次迭代又触发了一次重新哈希,将数组部分的大小扩大为4。

类似下面的代码

a = {}
a.x = 1; a.y = 2; a.z = 3

做的事情类似,只不过增加的是哈希部分的大小。

对于大的表来说,初期的几次重新哈希的开销被分摊到整个表的创建过程中,一个包含三个元素的表需要三次重新哈希,而一个有一百万个元素的表也只需要二十次。但是当创建几千个小表的时候,重新哈希带来的性能影响就会非常显著。

旧版的Lua在创建空表时会预选分配大小(4,如果我没有记错的话),以防止在初始化小表时产生的这些开销。但是这样的实现方式会浪费内存。例如,如果你要创建数百万个点(表现为包含两个元素的表),每个都使用了两倍于实际所需的内存,就会付出高昂的代价。这也是为什么Lua不再为新表预分配数组。

如果你使用C编程,可以通过Lua的API函数lua_createtable来避免重新哈希;除lua_State之外,它还接受两个参数:数组部分的初始大小和哈希部分的初始大小[1]。只要指定适当的值,就可以避免初始化时的重新哈希。需要警惕的是,Lua只会在重新哈希时收缩表的大小,因此如果在初始化时指定了过大的值,Lua可能永远不会纠正你浪费的内存空间。

当使用Lua编程时,你可能可以使用构造式来避免初始化时的重新哈希。当你写下

{true, true, true}

时,Lua知道这个表的数组部分将会有三个元素,因此会创建相应大小的数组。类似的,如果你写下

{x = 1, y = 2, z = 3}

Lua也会为哈希部分创建一个大小为4的数组。例如,执行下面的代码需要2.0秒:

for i = 1, 1000000 do
    local a = {}
    a[1] = 1; a[2] = 2; a[3] = 3
end

如果在创建表时给定正确的大小,执行时间可以缩减到0.7秒:

for i = 1, 1000000 do
    local a = {true, true, true}
    a[1] = 1; a[2] = 2; a[3] = 3
end

但是,如果你写类似于

{[1] = true, [2] = true, [3] = true}

的代码,Lua还不够聪明,无法识别表达式(在本例中是数值字面量)指定的数组索引,因此它会为哈希部分创建一个大小为4的数组,浪费内存和CPU时间。

两个部分的大小只会在Lua重新哈希时重新计算,重新哈希则只会发生在表完全填满后,Lua需要插入新的元素之时。因此,如果你遍历一个表并清除其所有项(也就是全部设为nil),表的大小不会缩小。但是此时,如果你需要插入新的元素,表的大小将会被调整。多数情况下这都不会成为问题,但是,不要指望能通过清除表项来回收内存:最好是直接把表自身清除掉。

你可能会好奇Lua为什么不会在清除表项时收缩表。首先是为了避免测试写入表中的内容。如果在赋值时检查值是否为nil,将会拖慢所有的赋值操作。第二,也是最重要的,允许在遍历表时将表项赋值为nil。例如下面的循环:

for k, v in pairs(t) do
    if some_property(v) then
        t[k] = nil – 清除元素
    end
end

如果Lua在每次nil赋值后重新哈希这张表,循环就会被破坏。

如果你想要清除一个表中的所有元素,只需要简单地遍历它:

for k in pairs(t) do
    t[k] = nil
end

一个“聪明”的替代解决方案:

while true do
    local k = next(t)
    if not k then break end
    t[k] = nil
end

但是,对于大表来说,这个循环将会非常慢。调用函数next时,如果没有给定前一个键,将会返回表的第一个元素(以某种随机的顺序)。在此例中,next将会遍历这个表,从开始寻找一个非nil元素。由于循环总是将找到的第一个元素置为nil,因此next函数将会花费越来越长的时间来寻找第一个非nil元素。这样的结果是,这个“聪明”的循环需要20秒来清除一个有100,000个元素的表,而使用pairs实现的循环则只需要0.04秒。

通过使用闭包,我们可以避免使用动态编译。下面的代码只需要十分之一的时间完成相同的工作:

function fk (k)
    return function () return k end
end

local lim = 100000
local a = {}
for i = 1, lim do a[i] = fk(i) end

print(a[10]()) --> 10

取长度

常用的取长度方式为#
而#的使用又有些需要注意的地方。
首先要明确的是lua中有两部分:数组部分和hash表部分。而基本上所有操作都是先数组后hash表。

local test1 = { 1 , 2 , 3 , 4 , 5 }
print(#test1)
打印结果: 5


local test1 = { 1, 3 , 5 , 2 , 4 }
print(#test1)
打印结果: 5 (好吧。。。。当然跟上面一样,都是作为数组中的值。。。)

local test1 = {[1] = 1 , [2] = 2 , [3] = 3 , [4] = 4 ,[5] = 5}
print(#test1)
打印结果: 5 (这里table中没有数组部分了,只有hash表部分)

local test1 = {[1] = 1 , [3] = 3 , [4] = 4 , [6] = 6 ,[2] = 2}
print(#test1)
打印结果: 6


明明写的table中只有5个元素,怎么会变成6那。。。。这里的原因就要看下lua源码实现

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/*

** Try to find a boundary in table `t'. A `boundary' is an integer index

** such that t[i] is non-nil and t[i+1] is nil (and 0 if t[1] is nil).

*/

int luaH_getn (Table *t) {

  unsigned int j = t->sizearray;

  if (j > 0 && ttisnil(&t->array[j - 1])) {

    /* there is a boundary in the array part: (binary) search for it */

    unsigned int i = 0;

    while (j - i > 1) {

      unsigned int m = (i+j)/2;

      if (ttisnil(&t->array[m - 1])) j = m;

      else i = m;

    }

    return i;

  }

  /* else must find a boundary in hash part */

  else if (isdummy(t->node))  /* hash part is empty? */

    return j;  /* that is easy... */

  else return unbound_search(t, j);

}



还是先数组,数组没有后hash部分。再来看下关于hash表部分的取长度

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

static int unbound_search (Table *t, unsigned int j) {

  unsigned int i = j;  /* i is zero or a present index */

  j++;

  /* find `i' and `j' such that i is present and j is not */

  while (!ttisnil(luaH_getint(t, j))) {

    i = j;

    j *= 2;

    if (j > cast(unsigned int, MAX_INT)) {  /* overflow? */

      /* table was built with bad purposes: resort to linear search */

      i = 1;

      while (!ttisnil(luaH_getint(t, i))) i++;

      return i - 1;

    }

  }

  /* now do a binary search between them */

  while (j - i > 1) {

    unsigned int m = (i+j)/2;

    if (ttisnil(luaH_getint(t, m))) j = m;

    else i = m;

  }

  return i;

}

j++保证j是hash部分的第一个值,从j开始,如果j位置是有值的,那么将j扩大两倍,再检查两倍之后hash表中是否可以取到值,直到找到没有值的地方,这个值就在i 到 j这个区间中。然后再用折半查找找到 i 到 j之间找到的最后一个nil的,前面的就是它的长度了。 错略看来。luaH_getint用来取值 
const TValue *luaH_getint (Table *t, int key)而它的声明看来 ,第二个参数是key,通过key来取value, 而外面对传入的key是++的操作 可知计算长度用来寻找的这个key一定是个整形,而且还得是连续的(不一定)。(当然这个是不深究细节实现错略看下来的分析。。。。。)

遍历

ipairs遍历  从下表为1的元素开始遍历,遇到nil结束。

local t = {
    [5] = "105",
    [6] = "106",
    [8] = "108",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in ipairs(t)  do
    print(v)
end

输出:

1

2

3

4

105

106

 

pairs遍历,受限于表在lua语言中的底层实现机制,遍历过程中元素的出现顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。

local t = {
    [105] = "105",
    [106] = "106",
    [107] = "107",
    [108] = "108",
    [109] = "109",
    [110] = "110",
    [111] = "111",
    [112] = "112",
    [113] = "113",
    [114] = "114",
 }

for i =1 , 4 do
    t[i] = i
end
for i, v in pairs(t)  do
    print(v)
end

输出: 

112

113

114

3

4

2

1

105

106

107

108

109

110

111

转载自:https://blog.csdn.net/wangmanjie/article/details/52793902

https://blog.csdn.net/weixin_42973416/article/details/103294010

https://blog.csdn.net/summerhust/article/details/18599375

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Lua 表(table) 的相关文章

  • Redis持久化机制:AOF和RDB

    前言 我们都知道Redis操作的数据都来源于内存 所以Redis读写速率极快 那为什么我们还需要用到持久化勒 当我们Redis服务器宕机或者Redis进程被kill或者异常退出的时候 如果没有持久化机制将数据保存到磁盘的化 那么之前保存到R
  • base64编码解码器【C++】

    在线编码解码工具https base64 us 所有结果可以使用上述网站检验 什么是base64编码 base64编码是一种编码方式 用64 1 个字符表示字符 本质是将三位8比特字符扩增为四位8比特字符 但是这么说开始可能很闷逼 给个图
  • postman接口传参案例

    目录 案例1 接口A 接口B 案例2 断言 案例1 接口A 根据返回值需要从返回值中提取userid值 在Tests标签栏下编写脚本 获取返回的响应值 并转化为json格式 var jsonData pm response json 获取返

随机推荐

  • Springboot中管理Spring容器重写工具类进行使用

    说明 SpringUtils 既spring工具类 方便在非spring管理环境中获取bean 在SpringBoot或者SpringMVC框架中 基于Spring进行管理容器以及上下文或者等前置操作等 因此需要实现 BeanFactory
  • Fiddler抓包工具之fiddler设置手机端抓包

    fiddler设置手机端抓包 安卓手机抓包 第一步 配置电脑和安卓的相关设置 1 手机和fiddler位于同一个局域网内 首先从fiddler处获取到ip地址和端口号 点击online 最后一行就是ip地址 2 路径 Tools Optio
  • uni-app跨端开发微信小程序之手把手带你写一个用程序自动打开微信开发者工具的小插件

    摘要 本文通过获取微信开发者工具安装路径 调用shelljs执行vue cli编译命令 fs和path组合来读取编译后的目录 动态修改AppId和title这四个方面入手 一步步带领看官制作一个自动打开微信开发者工具的小插件 完美解决日常多
  • Typora设置修改字体颜色快捷键

    目录 1 typora如何设置修改字体颜色快捷键 2 AutoHotKey软件安装 3 typora关于AutoHotKey的具体操作 1 typora如何设置修改字体颜色快捷键 typora本身是不能直接修改字体颜色的 不过若是想修改还是
  • buck和boost电路

    文章目录 buck和boost电路 1 占空比计算 2 电感值计算 buck和boost电路 归属于DCDC非隔离电源的一部分 最常用的拓扑方式 1 占空比计算 电感两端电压与电流关系 V L d i
  • Windows下编译VTK-9.1.0

    VTK 9编译要点 VTK 9 1 0 src CMake vtkModule cmake 第4075行可以修改Debug的库后缀 VTK 9 2 0 src CMake vtkModule cmake 第4230行可以修改Debug的库后
  • 游戏开发unity打包相关系列:使用IL2CPP时打包windows程序出现Currently selected scripting backend (IL2CPP) is not installed

    安装对应平台需要的构建支持
  • NPN与PNP型传感器的区别

    NPN与PNP型传感器其实就是利用三极管的饱和和截止 输出两种状态 属于开关型传感器 但输出信号是截然相反的 即高电平和低电平 NPN输出是低电平0 PNP输出的是高电平1 沧正称重传感器 NPN与PNP型传感器 开关型 分为六类 1 NP
  • Unity3D开发小贴士(十四)JsonUtility

    Json是现在非常常用的数据格式 因为 Net的版本问题 所有没有很方便的方法可以直接在Unity里面使用C 官方的Json库 于是Unity3D自己提供了自己的一套Json工具 JsonUtility 参考下面的示例 using Unit
  • 前端学习--多益

    什么是跨域 它主要解决什么问题 如果你有8个不同的css样式 整合进网站的最好方式是 如果仅需要引入一个CSS文件 则使用连接方式 如果需要引入多个CSS文件 则首先用连接方式引入一个 目录 CSS文件 这个 目录 CSS文件中再使用导入式
  • 2023年第三届智能制造与自动化前沿国际会议(CFIMA 2023)

    2023年第三届智能制造与自动化前沿国际会议 CFIMA 2023 重要信息 会议网址 www cfima org 会议时间 2023年6月9 11日 召开地点 中国大理 截稿时间 2023年4月20日 录用通知 投稿后2周内 收录检索 E
  • Spring boot整合pagehelper

    一 导入分页插件依赖
  • ZVM Bugs (持续更新)

    问题1 Cmake配置问题 CMake Error at CMakeLists txt 5 find package Could not find a package configuration file provided by Zephy
  • can通道采样频率_CAN总线基础(上)

    概述 汽车电子设备的不断增多 对汽车上的线束分布以及信息共享与交流提出了更高的要求 传统的电气系统往往采用单一连接的方式通信 这必将带来线束的冗余以及维修的成本的提高 传统的单一通信的对接方式 已经不能满足现代汽车电子发展的需求 采用更为先
  • 软件设计师——多媒体基础

    文章目录 音频相关概念 图像相关概念 媒体的种类 多媒体相关计算 常见多媒体标准 数据压缩 有损压缩与无损压缩 题目举例 软件设计师中该部分分值为 1 3 分 音频相关概念 次声波 小于20Hz 超声波 大于20kHz A D转换 采样 g
  • dockerfile使用报错记录

    使用centos镜像默认是8 报错 解决 修改源 RUN cd etc yum repos d RUN sed i s mirrorlist mirrorlist g etc yum repos d CentOS RUN sed i s b
  • pytorch预训练模型加载与使用(以AlexNet为例)

    目录 1 概况 2 代码讲解 2 1 加载必要的包 2 2 设置GPU和transform 2 3 数据预处理 2 4 引入模型 2 5 训练模型 2 6 测试模型 2 7 保存模型 3 完整代码 4 结果 本文主要是提供过程 不要在意结果
  • 前沿技术,目前为止功能最全最强大的PLC智能远程模块,物联网模块

    前沿技术 目前为止功能最全最强大的PLC智能远程模块 物联网模块 如下图 巨控PLC智能远程控制终端不同应用场合的不同型号 巨控GRM模块分为以下4大类 GRMOPC GRM530 GRM230 GRM110 一 巨控GRMOPC系列的PL
  • 线程池有几种创建方式?

    总体来说线程池的创建可以分为以下两类 通过 ThreadPoolExecutor 手动创建线程池 通过 Executors 执行器自动创建线程池 而以上两类创建线程池的方式 又有 7 种具体实现方法 这 7 种实现方法分别是 Executo
  • Lua 表(table)

    介绍 表 Table 是Lua语言中最主要 事实上也是唯一的 和强大的数据结构 使用表 Lua语言可以以一一种简单 统一且高效的方式表示数组 集合 记录和其他很多数据结构 Lua语言也使用表来表示包 package 和其他对象 当调用函数m