LUA 面试复习笔记
LUA 游戏面试复习题Lua的特性Lua数据类型基本类型function(函数)hread(线程)userdata(自定义类型)Lua元表(Metatable)__index 元方法__newindex 元方法__call 元方法__tostring 元方法操作符元方法LUA闭包Lua的特性轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。可扩展: L
LUA 游戏面试复习题
Lua的特性
- 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
- 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
- 其它特性:
- 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
- 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
- 语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统 所支持的线程)支持;
- 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。
Lua数据类型
基本类型
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
- nil(空)nil 类型表示一种没有任何有效值,它只有一个值 – nil,例如打印一个没有赋值的变量,便会输出一个 nil 值:
- boolean(布尔)boolean 类型只有两个可选值:true(真) 和 false(假),Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true:
- number(数字)Lua 默认只有一种 number 类型 – double(双精度)类型(默认类型可以修改 luaconf.h 里的定义),以下几种写法都被看作是 number 类型:
print(type(2))
print(type(2.2))
print(type(0.2))
print(type(2e+1))
print(type(0.2e-1))
print(type(7.8263692594256e-06))
- string(字符串)字符串由一对双引号或单引号来表示。
- table(表)在 Lua 里,table 里包含数组与哈希表,如下面所示,a[1] 是不存在数组中的,而a[“key”]是保存在哈希表里面的
a = {}
a[1] = 10
a["key"] = "value"
因此遍历时ipairs 与pairs的区别就是 ipairs是遍历整个table(数组与hash表),ipairs 只是遍历从1开始的数组
function(函数)
在 Lua 中,函数是被看作是"第一类值(First-Class Value)",函数可以存在变量里:
function testFun(tab,fun)
for k ,v in pairs(tab) do
print(fun(k,v))
end
end
tab={key1="val1",key2="val2"}
testFun(tab,
function(key,val)--匿名函数
return key.."="..val
end
)
hread(线程)
在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。
线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。
userdata(自定义类型)
userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和 指针)存储到 Lua 变量中调用。
Lua元表(Metatable)
在 Lua table 中我们可以访问对应的key来得到value值,但是却无法对两个 table 进行操作。因此 Lua 提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。例如,使用元表我们可以定义Lua如何计算两个table的相加操作a+b。
当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫"__add"的字段,若找到,则调用对应的值。"__add"等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。
有两个很重要的函数来处理元表:
setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table): 返回对象的元表(metatable)。
__index 元方法
Lua查找一个表元素时的规则,其实就是如下3个步骤:
- 在表中查找,如果找到,返回该元素,找不到则继续
- 判断该表是否有元表,如果没有元表,返回nil,有元表则继续。
- 判断元表有没有__index方法,如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值。
我们经常会看到类似下面的代码块:
father = {
house=1
}
father.__index = father -- 把father的__index方法指向自己
son = {
car=1
}
setmetatable(son, father)
print(son.house)
如果上诉father不指定__index方法,则print的值为空
__index 也可以是一个函数,如果__index包含一个函数的话,Lua就会调用那个函数,table和键会作为参数传递给函数。__index 元方法查看表中元素是否存在,如果不存在,返回结果为 nil;如果存在则由 __index 返回结果。
在这里插入代码片mytable = setmetatable({key1 = "value1"}, {
__index = function(mytable, key)
if key == "key2" then
return "metatablevalue"
else
return nil
end
end
})
print(mytable.key1,mytable.key2)
__newindex 元方法
__newindex 元方法用来对表更新,__index则用来对表访问 。当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。
以下实例演示了 __newindex 元方法的应用:
mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })
print(mytable.key1)
mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)
mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)
结果:
value1
nil 新值2
新值1 nil
以上实例中表设置了元方法 __newindex,在对新索引键(newkey)赋值时(mytable.newkey = “新值2”),会调用元方法,而不进行赋值。而如果对已存在的索引键(key1),则会进行赋值,而不调用元方法 __newindex。
以下实例使用了 rawset 函数来更新表:
mytable = setmetatable({key1 = "value1"}, {
__newindex =
function(mytable, key, value)
rawset(mytable, key, "\""..value.."\"")
end
})
mytable.key1 = "new value"
mytable.key2 = 4
print(mytable.key1,mytable.key2)
结果:
new value "4"
__call 元方法
__call 元方法在 Lua 调用一个值时调用。
以下实例演示了计算表中元素的和:
-- 自定义计算表中最大键值函数 table_maxn,即计算表的元素个数
function table_maxn(t)
local mn = 0
for k, v in pairs(t) do
if mn < k then
mn = k
end
end
return mn
end
-- 定义元方法__call
mytable = setmetatable({10}, {
__call = function(mytable, newtable)
sum = 0
for i = 1, table_maxn(mytable) do
sum = sum + mytable[i]
end
for i = 1, table_maxn(newtable) do
sum = sum + newtable[i]
end
return sum
end
})
newtable = {10,20,30}
print(mytable(newtable))
以上实例执行输出结果为:70
__tostring 元方法
__tostring 元方法用于修改表的输出行为。以下实例我们自定义了表的输出内容:
mytable = setmetatable({ 10, 20, 30 }, {
__tostring = function(mytable)
sum = 0
for k, v in pairs(mytable) do
sum = sum + v
end
return "表所有元素的和为 " .. sum
end
})
print(mytable)
以上实例执行输出结果为:
表所有元素的和为 60
__gc元方法
__gc元方法在 table 被回收时会触发的回调,可以用来做一些 lua内存泄露 及 资源释放 等操作,在lua5.2以上的版本才可以直接使用。
local tab = { _name = "default" }
setmetatable(tab, {
__gc = function ( t )
print("__gc, _name:", t._name)
end
})
collectgarbage("collect") -- 强制垃圾回收
结果:__gc, _name: default
__mode 弱引用元方法
// 如果__mode元方法被定义
if (mode && ttisstring(mode)) { /* is there a weak mode? */
// 判断是弱键还是弱值
weakkey = (strchr(svalue(mode), 'k') != NULL);
weakvalue = (strchr(svalue(mode), 'v') != NULL);
if (weakkey || weakvalue) { /* is really weak? */
// 如果其中之一的条件满足
// 首先将原来的弱键/弱值标记位清除
h->marked &= ~(KEYWEAK | VALUEWEAK); /* clear bits */
// 标记这次的标记位
h->marked |= cast_byte((weakkey << KEYWEAKBIT) |
(weakvalue << VALUEWEAKBIT));
// 加入weak链表中
h->gclist = g->weak; /* must be cleared after GC, ... */
g->weak = obj2gco(h); /* ... so put in the appropriate list */
}
}
将表标记为弱引用表,则会加到弱表中,它所引用的对象不会认为这个table对它有引用,很绕,就是包含__mode元方法的table,可以设置其key或者value或者两者都有所保存的引用是弱的,不加入引用计算。
t1, t2 = {}, {}
arr = {}
arr[1] = t1
arr[2] = t2
setmetatable(arr, {__mode = "v"})
t1 = nil --此时我们将t1赋值为nil,表明这一部分的内容我们不需要再使用,可以被回收
collectgarbage() -- 手动执行gc回收操作
for k, v in pairs(arr) do
print(k, v)
end
结果:
table: 007CB158
这里,第一个表被回收了。
如果去掉setmetatable(arr, {__mode = “v”}),
结果:
table: 0053B068
table: 0053B158
如果将表作为键,那么选择"k"的模式,则会标记键为弱引用。而如果标记"kv",则这两个条件同时生效,只要其中一个可被回收则会被直接回收。
操作符元方法
方法 | 操作符 |
---|---|
__add | ‘+’ 加法 |
__sub | ‘-’ 减法 |
__mul | ‘*’ 除法 |
__div | ‘/’ 乘法 |
__mod | ‘%’ 模运算 |
__unm | ‘-’,对相反数进行重载 |
__concat | ‘…’ 对连接操作符进行重载 |
__eq | ‘==’ 重载相等 |
__lt | ‘<’ 重载小于 |
__le | '<= 重载小于等于 |
例子:
tbA={1,3}
tbB={5,7}
tmetatable={}
tmetatable.__add=function(t1,t2)
for _,var in ipairs(t2) do
table.insert(t1,var)
end
return t1
end
setmetatable(tbA,tmetatable)
tbSum=tbA+tbB
for _,v in ipairs(tbSum) do
print(v)
结果: 1 3 5 7
LUA闭包
在Lua中,函数是第一类类型值,这意味着定义函数跟定义其他普通变量是一样的,区别是函数对应的值是对应的函数体指令。因为这个特性,我们可以在函数体再定义内嵌函数,也就是我们所说的闭包(Closure),内嵌函数可以访问其外包函数中所有的局部变量,这种特性称为词法域,我们称这些局部变量为该内嵌函数的外部局部变量,也就是常说的UpValue.
typedef struct LClosure {
ClosureHeader;
struct Proto *p;
UpVal *upvals[1];
} LClosure;
闭包结构中,可以看到UpVal是一个数组,当在外嵌函数中调用时,引用的局部变量保存在UpVal中,是引用的lua_State中的openupval双向链表,我们称之为open状态,当在外部调用时,该Closer改为close状态,UpVal会把当前的局部变量的值直接保存到UpVal,作为一个函数原型,这样在出去函数作用域以后,仍然可以访问到局部变量。
function test1()
local a = 1
func = function()
a = a + 1
print(a)
end
return func
end
func1 = test1()
func2 = test1()
func1()
func1()
func2()
func2()
运行结果如下:
2
3
2
3
可见,func1,func2,实际是新建了2个变量,每一个变量都是test1()返回的函数实例,他们引用了一个Proto 的实例,各自单独保存了一个自己的UpVal数组,各自维护各自的UpVal.
LUA GC算法
GC算法的原理:遍历系统中所有的对象,查询到没有引用关系的对象,给与清除。
引用计数之殇
引用计数算法,会在一个对象被引用的情况下,将该对象的引用计数加一,反之减一。如果引用计数为0,就是可以清除的。
优点:不需要扫描每个对象,计数到0即被回收
缺点:循环引用,导致不可回收
右图中,3个节点其实已经没有使用了,但是计数都是1,无法回收
标记清除
双色标记清除算法
每次GC的时候,从root开始,扫描并且标记系统中所有的对象,被扫描并且标记到的对象认为是可达的,不会被回收,反之,被回收。
优点:有效的解决了循环引用不可回收的问题
缺点:对象的状态是“二元”的,没有中间状态,因此GC操作不能被打断,必须一次清除完所有对象。
三色标记清除算法
引入灰色节点,我们可以把已经扫描但是其引用的对象还未被扫描的对象置为灰色,挂到灰色链表上,由于引入了中间状态,我们可以不需要一次GC操作全部进行完。
typedef struct global_State {
stringtable strt; /* hash table for strings */
lua_Alloc frealloc; /* function to reallocate memory */
void *ud; /* auxiliary data to `frealloc' */
lu_byte currentwhite;
lu_byte gcstate; /* state of garbage collector */
int sweepstrgc; /* position of sweep in `strt' */
GCObject *rootgc; /* list of all collectable objects */
GCObject **sweepgc; /* position of sweep in `rootgc' */
GCObject *gray; /* list of gray objects */
GCObject *grayagain; /* list of objects to be traversed atomically */
GCObject *weak; /* list of weak tables (to be cleared) */
// 所有有GC方法的udata都放在tmudata链表中
GCObject *tmudata; /* last element of list of userdata to be GC */
Mbuffer buff; /* temporary buffer for string concatentation */
// 一个阈值,当这个totalbytes大于这个阈值时进行自动GC
lu_mem GCthreshold;
// 保存当前分配的总内存数量
lu_mem totalbytes; /* number of bytes currently allocated */
// 一个估算值,根据这个计算GCthreshold
lu_mem estimate; /* an estimate of number of bytes actually in use */
// 当前待GC的数据大小,其实就是累加totalbytes和GCthreshold的差值
lu_mem gcdept; /* how much GC is `behind schedule' */
// 可以配置的一个值,不是计算出来的,根据这个计算GCthreshold,以此来控制下一次GC触发的时间
int gcpause; /* size of pause between successive GCs */
// 每次进行GC操作回收的数据比例,见lgc.c/luaC_step函数
int gcstepmul; /* GC `granularity' */
lua_CFunction panic; /* to be called in unprotected errors */
TValue l_registry;
struct lua_State *mainthread;
UpVal uvhead; /* head of double-linked list of all open upvalues */
struct Table *mt[NUM_TAGS]; /* metatables for basic types */
TString *tmname[TM_N]; /* array with tag-method names */
} global_State;
可以看到,在global_State中,存在rootgc与gray链表
双白色
解决刚刚创建的对象在标记完后创建,然后还没来得及引用就被回收的问题。
lua使用currentwhite与otherwhite 交替使用,第N次GC使用的是第一种白色,那么下一次就是另外一种。在回收的时候判断,如果某个对象的白色不是此次GC使用的白色状态,那么将不会认为是没有被引用的对象而回收,这样的白色将留在下一次GC中进行扫描。
参考:GC算法详解
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)