面试专用Lua基础知识总结!

Posted 游戏蛮牛

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试专用Lua基础知识总结!相关的知识,希望对你有一定的参考价值。

2019年8月刚入职新公司时,因为之前的项目都没有使用lua的经验,所以jojo老大出了一份题让我想尽办法找出答案,当时对于一个无经验的小菜鸟来说,属实费了不少功夫,如今分享出来,希望能对刚使用lua的朋友们有所帮助,如果有大佬看到有错误的地方,欢迎指出,感激不尽。

1、Lua的基础工作原理,.lua文件实时编译之后,给到虚拟机的是什么指令.

具体指令形式有看吗?这个指令占了多少位数据,第n位主句代表啥,稍微看一下,有一个认识。然后这些指令,具体怎么跟lua源码的模块代码相结合呢?比如我们是怎么调用到Talbe里面的add的?其实每个指令具体执行,都有一个switch(指令类型)这样执行的,找到这个文件,然后有时间可以大概了解一下lua的文件结构,大概每个文件都放了一些啥,可以更深入了解一下。lua源码(window项目)可以打开tolua_rumtime-master_5_3_2\lua-5.3.3\lua.sln来看
  • Lua使用虚拟堆栈向C传递值。此堆栈中的每个元素表示Lua值(nil,number,string等)。API中的函数可以通过它们接收的Lua状态参数访问此堆栈。

  • Lua运行代码时,首先把代码编译成虚拟机的指令("opcode"),然后执行它们。Lua编译器为每个函数创建一个原型(prototype),这个原型包含函数执行的一组指令和函数所用到的数据表。

虚拟机指令类型

/*
**虚拟机指令类型;;
**必须是无符号的(至少)4字节(请参阅lopcode .h中的详细信息)
*/
#if LUAI_BITSINT >= 32
typedef unsigned int Instruction;
#else
typedef unsigned long Instruction;
#endif

2、Lua的数据类型

(如果要看源码了,可以看一下会被gc的那个类型数据,是如何被定义的,为啥lua不需要定义数据类型就可以赋值?什么要的数据类型会被放到_G那里去。然后可能还有一些数据类型不不会暴露给我们使用的,比如Proto,这个跟function的实现相关,有兴趣可以了解一下。还有lua_State)
  • Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。


为啥lua不需要定义数据类型就可以赋值?

在赋值的时候,会调用函数expr解析表达式,=号右边的值,它最终会走入函数simpleexp中,在simpleexp中会根据expr解析出来的expdesc结构体里的t.token,用一个switch判断该表达式的类型,初始化expdesc结构体,将具体的数据赋值给expdesc结构体中的nval,所以,lua不需要定义数据类型就可以赋值,因为在解析器中会根据值的类型来进行初始化。

函数localstat中,会读取“=”号左边的所有变量,首先看到在函数localstat中,首先会有一个循环调用函数new_localvar,将“=”左边的所有以","分隔的变量都生成一个相应的局部变量。每一个局部变量,存储它的信息时使用的是LocVar结构体

static void localstat (LexState *ls) {
/* stat -> LOCAL NAME {',' NAME} ['=' explist] */
int nvars = 0;
int nexps;
expdesc e;
do {
new_localvar(ls, str_checkname(ls));
nvars++;
} while (testnext(ls, ','));
if (testnext(ls, '='))
nexps = explist(ls, &e);
else {
e.k = VVOID;
nexps = 0;
}
adjust_assign(ls, nvars, nexps, &e);
adjustlocalvars(ls, nvars);
}

typedef struct LocVar {
TString *varname;
int startpc; /* first point where variable is active */
int endpc; /* first point where variable is dead */
} LocVar;

这里主要存储了变量名,放在该结构体的变量varname中。一个函数的所有局部变量的LocVar信息,是存放在Proto结构体的locvars中。在函数localstat中,会读取“=”号左边的所有变量,创建相应的局部变量信息在Proto结构体中。


我们从通过lua_pushbollean等指令函数看,c通过这些函数将各种类型的值压入lua栈,从而传递给lua。

(lapi.c) 556行
LUA_API void lua_pushboolean (lua_State *L, int b) {
lua_lock(L);
setbvalue(L->top, (b != 0)); /* ensure that true is 1 */
api_incr_top(L);
lua_unlock(L);
}

(lobject.h) 225行
#define setsvalue(L,obj,x) \
{ TValue *io = (obj); TString *x_ = (x); \
val_(io).gc = obj2gco(x_); settt_(io, ctb(x_->tt)); \
checkliveness(L,io); }

可以看到从虚拟栈里取出top之后,把值传给了setbvalue(L,obj,x)。

而在 setbvalue 里,obj 被转换成了 TValue 类型,接着又调用了两个宏 val_(),settt_()来设置 TValue 类型的两个成员。

由此可见,lua 栈中所有类型的值都是用 TValue 结构体来表示的。

那么TValue结构体是什么样的呢?

(lobject.h) 110行
#define TValuefields Value value_; int tt_

typedef struct lua_TValue {
TValuefields;
} TValue;

它由一个实际的 value 和一个int类型的 tag 组成。

基本类型

(lua.h)
/*
** basic types
*/
#define LUA_TNONE (-1) // 无类型
#define LUA_TNIL 0 // 空类型
#define LUA_TBOOLEAN 1 // 布尔
#define LUA_TLIGHTUSERDATA 2 // 指针 (void *)
#define LUA_TNUMBER 3 // 数字 (lua_Number)
#define LUA_TSTRING 4 // 字符串 (TString)
#define LUA_TTABLE 5 // 表 (Table)
#define LUA_TFUNCTION 6 // 函数 (CClosure)
#define LUA_TUSERDATA 7 // 指针 (void *)
#define LUA_TTHREAD 8 // LUA虚拟机 (lua_State)

value_ 是一个 union 类型 Value,所以它可以存储多种类型的值,根据注释可知全称叫Tagged Values

(lobject.h 100行)
/*
** Tagged Values. This is the basic representation of values in Lua,
** an actual value plus a tag with its type.
*/

/*
** Union of all Lua values
*/
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;

Lua内部用一个宏,表示哪些数据类型需要进行gc操作的: (lobject.h)

#define iscollectable(o)    (rttype(o) & BIT_ISCOLLECTABLE)

/* TValue的原始类型标签*/
#define rttype(o) ((o)->tt_)

/*可收集类型的位标记*/
#define BIT_ISCOLLECTABLE (1 << 6)


#define rttype(o) ((o)->tt_)

可以看到,tt_的第六位用于标记类型是否需要进行垃圾回收,

可进行垃圾回收的类型:GCObject

/*
** Common type has only the common header
*/
struct GCObject {
CommonHeader;
};

#define CommonHeader GCObject *next; lu_byte tt; lu_byte marked

可以看到GCObject结构中只有一个CommonHeader,CommonHeader主要由一个指向下一个回收类型的指针,一个对象类型tt和一个对象标记marked组成。

所以,lua中所有类型都的结构示意图如下:

TValue 里不是已经有一个 tt_ 字段用于表示类型了吗?为什么在 GCObject 里还需要这个字段呢?

答:要从 GCObject 反向得到 TValue 是不行的,假如 GCObject 没有 tt 字段,单单持有 GCObject 的时候,没法判断这个 GCObject 的类型是什么。GC 在回收对象的时候需要根据类型来释放资源。基于第一点,必须在 GCObject 里加一个表示类型的字段 tt。

3、为什么说Lua一切皆Table,Table有哪两种存储形式,Table是如何Resize的

  • table 最基础的作用就是当成字典来用。它的 key 值可以是除了 nil 之外的任何类型的值,当把 table 当成字典来用时,可以使用 ==pairs== 函数来进行遍历,使用==pairs==进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。

  • 当 key 为整数时,table 就可以当成数组来用。而且这个数组是一个 ==索引从1开始== ,没有固定长度,可以根据需要自动增长的数组,我们可以使用使用 ipairs 对数组进行遍历。

  • 其他语言提供的所有结构---数组,记录,列表,队列,集合这些在lua中都用==table==来表示。

  • 向table中插入数据时,如果已经满了,Lua会重新设置数据部分或哈希表的大小,容量是成倍增加的,哈希部分还要对哈希表中的数据进行整理。需要特别注意的没有赋初始值的table,数组和部分哈希部分默认容量为0。

  • resize代价高昂,当我们把一个新键值赋给表时,若数组和哈希表已经满了,则会触发一个再哈希(rehash)。再哈希的代价是高昂的。首先会在内存中分配一个新的长度的数组,然后将所有记录再全部哈希一遍,将原来的记录转移到新数组中。新哈希表的长度是最接近于所有元素数目的2的乘方。

local a = {}     --容量为0
a[1] = true --重设数组部分的size为1
a[2] = true --重设数组部分的size为2
a[3] = true --重设数组部分的size为4

local b = {} --容量为0
b.x = true --重设哈希部分的size为1
b.y = true --重设哈希部分的size为2
b.z = true --重设哈希部分的size为4

4、Lua的面向对象实现

所以,实际上,class.new是什么呢?然后new完之后,返回的是什么东西?
  • 使用元方法模拟面向对象的实现

--[[
云风的lua面向对象编程架构,用来模拟一个基类
--]]

local _class={}

function class(super)
local class_type={}
class_type.ctor=false
class_type.super=super

--[[
模拟构造函数的function
--]]
class_type.new=function(...)
local obj={}
do
local create
create = function(c,...)
--如果本类存在着基类,就递归调用基类的创建函数初始化基类的成员
if c.super then
create(c.super,...)
end
-- 如果本类有构造函数,就执行本类的构造函数操作
if c.ctor then
c.ctor(obj,...)
end
end
--前面的这段代码是声明create function,下面的就是执行
create(class_type,...)
end
--将此对象的元表的__index元方法设为下面的虚函数表
setmetatable(obj,{ __index=_class[class_type] })
return obj
end

-- 用一个table来构造类的函数表
local vtbl={}
_class[class_type]=vtbl

--[[
设置表class_type的元表并定义__newindex字段,字段对应的函数,
参数1就是表class_type本身,当添加一个新方法的时候就会执行此__newindex的实现
--]]
setmetatable(class_type,{__newindex=
function(t,k,v)
vtbl[k]=v
end
})

--[[
如果本类有父类的话,将本类虚函数表的原表__index设从父类的函数表,直接从父类的函数表中查找。
--]]
if super then
setmetatable(vtbl,{__index=
function(t,k)
local ret=_class[super][k] --这里,就是查找父类的函数表的操作
vtbl[k]=ret
return ret
end
})
end

return class_type
end

5、Lua元表是什么?

  • 元表主要用于对两个table进行操作,例如两个table相加,当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"add"的字段,若找到,则调用对应的值。"add"等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。

6、Lua的gc机制简述

  • 在Lua5.0及其更早的版本中,Lua的GC是一次性不可被打断的过程,使用的++Mark算法是双色标记算法(Two color mark)++,这样系统中对象的非黑即白,要么被引用,要么不被引用,这会带来一个问题:在GC的过程中如果新加入对象,这时候新加入的对象无论怎么设置都会带来问题,如果设置为白色,则如果处于回收阶段,则该对象会在没有遍历其关联对象的情况下被回收;如果标记为黑色,那么没有被扫描就被标记为不可回收,是不正确的。

  • 为了降低一次性回收带来的性能问题以及双色算法的问题,在Lua5.1后,Lua都采用分布回收以及++三色增量标记清除算法(Tri-color incremental mark and sweep)++

  • 将所有对象分成三个状态:

  • White状态,也就是待访问状态。表示对象还没有被垃圾回收的标记过程访问到。==(白色又分为White0和White1,主要为了解决上面所说到的在GC过程中新加入的对象的处理问题)==

  • Gray状态,也就是待扫描状态。表示对象已经被垃圾回收访问到了,但是对象本身对于其他对象的引用还没有进行遍历访问。

  • Black状态,也就是已扫描状态。表示对象已经被访问到了,并且也已经遍历了对象本身对其他对象的引用。

  • GC流程

每个新创建的对象颜色设置为White
//初始化阶段
遍历root节点中引用的对象,从白色置为灰色,并且放入到Gray节点列表中
//标记阶段
while(Gray集合不为空,并且没有超过本次计算量的上限):
从中取出一个对象,将其置为Black
遍历这个对象关联的其他所有对象:
if 为White
标记为Gray,加入到Gray链表中

//回收阶段
遍历所有对象:
if 为White,
没有被引用的对象,执行回收
else
重新塞入到对象链表中,等待下一轮GC
  • 在每个步骤之间,由于程序可以正常执行,所以会破坏当前对象之间的引用关系。black对象表示已经被扫描的对象,所以他应该不可能引用到一个white对象。当程序的改变使得一个black对象引用到一个white对象时,就会造成错误。解决这个问题的办法就是设置barrier。barrier在程序正常运行过程中,监控所有的引用改变。如果一个black对象需要引用一个white对象,存在两种处理办法:

  • 将white对象设置成gray,并添加到gray列表中等待扫描。这样等于帮助整个GC的标识过程向前推进了一步。

  • 将black对象改回成gray,并添加到gray列表中等待扫描。这样等于使整个GC的标识过程后退了一步。

这种垃圾回收方式被称为"++Incremental Garbage Collection++"(简称为"IGC",Lua所采用的就是这种方法。使用"IGC"并不是没有代价的。IGC所检测出来的垃圾对象集合比实际的集合要小,也就是说,有些在GC过程中变成垃圾的对象,有可能在本轮GC中检测不到。不过,这些残余的垃圾对象一定会在下一轮GC被检测出来,不会造成泄露。

7、Lua的全局变量跟local变量的区别,Lua是如何查询一个全局变量的,local的作用域

  • Lua将所有的全局变量保存在一个常规的table中,这个table称之为环境(_G),使 用下面的代码可以打印当前环境中所有全局变量的名称

for n in pairs(_G) do
print(n)
end
  • 在Lua中,要声明全局变量很简单,那就是定义变量的时候,前面不要加上 local。这个神秘的全局环境,其实本质上也是一个table,它把我们创建的全局变量都保存到一个table里了。而这个table的名字是:_G

  • 本地变量定义在一个函数体中, 那么作用域就在函数中.

  • 如果定义在一个控制结构中, 那么就在这个控制结构中.

  • 如果定义在一个文件中, 那么作用域就在这个文件中.

一些lua使用中要注意的点

  1. 使用local,在代码运行前,Lua会把源码预编译成一种中间码,类似于Java的虚拟机。这种格式然后会通过C的解释器进行解释,整个过程其实就是通过一个while循环,里面有很多的switch...case语句,一个case对应一条指令来解析。自Lua 5.0之后,Lua采用了一种类似于寄存器的虚拟机模式。Lua用栈来储存其寄存器。每一个活动的函数,Lua都会其分配一个栈,这个栈用来储存函数里的活动记录。每一个函数的栈都可以储存至多250个寄存器,因为栈的长度是用8个比特表示的。有了这么多的寄存器,Lua的预编译器能把所有的local变量储存在其中。这就使得Lua在获取local变量时其效率十分的高。

  2. 如果你有很多非常多的很小的表需要创建时,你可以将其预先填充以避免rehash。

比如:

{true,true,true}

Lua知道这个表有三个元素,所以Lua直接创建了三个元素长度的数组。

所以,当需要创建非常多的小size的表时,应预先填充好表的大小。


以上是关于面试专用Lua基础知识总结!的主要内容,如果未能解决你的问题,请参考以下文章

Vue面试题总结 - 基础知识总结 - 复习专用

Linux常用命令总结-软件测试面试专用

java中创建错误总结(面试专用)

脚本语言Lua的基础知识总结

HTML/HTML5 基础知识 | 面试题专用

Java进阶之光!2021必看-Java高级面试题总结