Lua 面试题
Lua面试题
1.Lua 是如何进行热更的?
由于 Lua 不需要编译,因此 Lua 代码可以直接在 Lua 虚拟机里运行。而C#代码在开始运行之前,都会一起装在到内存的代码段,没有办法更新新的代码。
动态装载:app + 内置脚本解释器,由这个脚本解释器动态的执行脚本代码
Lua = Lua解释器 + Lua脚本
2.Lua有哪些数据类型
nil(空类型),boolean,number(数字类型,不细分),string,function,userdata,thread,table
Lua 条件判断时把 false 和 nil 看作是 false,其他都是 true,数字 0 也是 true
3.Lua 中的 string
Lua 的字符串分为短字符串和长字符串。
短字符串: 长度小于等于40为短字符串。相同字符的短字符串是共用同一个的。
长字符串: 长度大于40为长字符串。相同的两个长字符串是两个独立的字符串。
4.Lua 中 table 的内部数据结构
typedef struct Table
{
TValue *array; // 数组指针
int sizearray; // 数组大小
Node *node; // 哈希表指针
lu_byte lsizenode; // 哈希表大小(2^lsizenode)
Node *lastfree; // 空闲节点指针
// ...(元表、GC等字段)
} Table;
Lua table 实际内部是包含一个数组 (array) 和 hashtable 来共同存储所有数据的。其中 array 用于存储键值为 1 ~ n 的数据,且 1 ~ n 必须连续。而其他的数据,会被存放在 hashtable 内。注意 Lua 数组要求 key 必须是连续的整数(1 ~ n),如果中间有空洞,那么可能出现的情况是后面的数据会被放到 hashtable 存储。
除此之外,如果数字键值对的数量过大,也会存储到 hash 表上(简单的说就是数组容量不够了)
5.Lua 中 table 如何解决冲突
开放定址法
6.Lua 中 GC
标记清除式(Mark and Sweep)GC算法,每次 GC 的时候,对所有对象进行一次扫描,如果该对象不存在引用,则被回收。从 Lua 5.1 开始,采用三色增量标记清除算法。好处:它不必再要求 GC 一次性扫描所有的对象,这个 GC 过程可以是增量的,可以被中断再恢复并继续进行的。3种颜色分类如下:
白色:表示对象还没有被标记过,这也是任何一个对象创建后的初始状态。换言之,如果一个对象在结束扫描过程后仍然是白色,则说明该对象没有被系统中的任何一个对象所引用,可以回收其空间了。(白色又分为白1、白2)。
灰色:当前对象为待扫描状态,表示对象已经被访问过,但是该对象引用的其他对象还没有被访问到。
黑色:当前对象为已扫描状态,表示对象及该对象引用的其他对象都被访问过了。
备注:白色分为白1和白2。原因:在 GC 标记阶段结束而清除阶段尚未开始时,如果新建一个对象,由于其未被发现引用关系,原则上应该被标记为白色,于是之后的清除阶段就会按照白色被清除的规则将新建的对象清除,这是不合理的。于是 lua 用两种白色进行标识,如果发生上述情况,lua依然会将新建对象标识为白色,不过是“当前白”(比如白1)。而 lua 在清扫阶段只会清扫“旧白”(比如白2),在清扫结束之后,则会更新“当前白”,即将白2作为当前白。
GC 实现随版本不同:
Lua 5.0:stop-the-world GC,有明显卡顿
Lua 5.1+:增量 GC(默认),GC 可以被分成多个小步骤执行以降低停顿
Lua 5.4:引入了分代策略,年轻代对象更频繁回收,老年代较少回收,以提高性能。
7.pairs 和 ipairs 的区别?
pairs 遍历 table 里的所有元素,ipairs 只遍历前面连续的元素
--下面lua代码运行的结果是
local a = { [1]=2,[2]=3,[4]=a,[5]=c }
for i,v in ipairs(a) do
print(v)
end
结果
2
3
ipairs顺序遍历数组的部分,遍历到3找不到就自动退出了
8.怎么获取 table 的长度?
如果 table 内容是连续的可以用#,存在 key-value 形式的就需要遍历计数
9.table 的排序
按照 value 值来排序,使用 table.sort(needSortTable, func) 即可,可以根据自己的需要重写 func,否则会根据默认来,默认的情形之下,如果表内既有 string,number 类型,则会因为两个类型直接 compare 而出错,所以需要自己写 func 来转换一下。
local test_table = {2,1,3,"SORT","sort"}
table.sort(test_table , function(a , b)
return tostring(a) > tostring(b)
end)
10.如何拼接字符串?
使用 table.concat 或者…
“…”每次拼接都会产生一个新的字符串,而在lua中每产生一个新的字符串都需要将该字符串存放在全局状态表(global_State)的 strt 域中,随着拼接次数增大,就会需要更大的空间存储新的字符串,当达到一定大小时,旧的字符串就需要 GC,伴随着不断的开辟新空间和GC,就导致性能降低。
table.concat 将 table 中的元素转换成字符串,再用分隔符连接起来。table.concat 没有频繁申请内存,只有当写满一个8192的 BUFF 时,才会生成一个 TString,最后生成多个 TString 时,会有一次内存申请并合并。在大规模字符串合并时,应尽量选择这种方式。
11.如何判断一个 table 是否为 nil 或者空表
--next查找下一个值,默认从第一个值开始
function IsTableEmpty(t)
return t == nil or next(t) == nil
end
12.__index,__newindex,__call,__tostring 方法用法?
local mytable = {1, 2, 3}
local mymetatable = {
__index = {b = 6},
__newindex = function(table, key)
print(__newindex ..key)
end
,
__call = function(self, newtable)
sum = #newtable
return sum
end
,
__tostring = function(self)
return #self
end
}
--设置元表
mytable = setmetatable(mytable, mymetatable)
--获取对象元表
getmetatable(mytable)
--元表设置了__index方法,mytable找不到b元素(也可以是方法)就会去元表的__index中找
print(mytable.b)
--表里添加新元素时会调用__newindex方法
mytable.c = 3
--当表作为函数形式被调用时,进入__call方法
newtable = {1,2,3}
print(mytable(newtable))
--直接调用mytable,进入__tostring方法
print(mytable)
13.Lua 如何设置只读表
--在元表的__newindex中抛出异常
function table_read_only(t)
local temp= t or {}
local mt =
{
__index = function(t,k)
return temp[k]
end,
__newindex = function(t, k, v)
error("attempt to update a read-only table!")
end
}
setmetatable(temp, mt)
return temp
end
--用法:
local ta = {1,2,3}
local tb = table_read_only(ta) --tb为只读
tb[5] = 1 --对表进行更新,会报错:attempt to update a read-only table!
14.__rawset 和 __rawget 有什么区别?
如果对一个表进行查找的时候,若表中不存在该值,则会查找该表的元表访问其元表__index字段来解决。而若对表输入一个新值,则会查找该表的元表访问其元表 __newindex 字段来解决。而 rawset & rawget 则是绕过元表这一过程,把操作这个表的结果直接输出。
rawset(table, key, value),为 table 加入一对 key,value
key 必须为字符串格式,输入其他格式会报错,添加时不会触发元表的 __newindex
rawget(table, index)
获取 table[index],不会触发元表的 __index
15.Lua 和 C# 底层如何交互
C# 与 Lua 进行交互主要通过虚拟栈实现,栈的索引分为正数与负数,若果索引为正数,则1表示栈底,若果索引为负数,则-1表示栈顶。
C# Call Lua:C# 把请求或数据放在栈顶,然后 lua 从栈顶取出该数据,在 lua 中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后 C# 再从栈顶取出 lua 处理完的数据,完成交互
Lua Call C#:(1)Wrap 方式:首先生成C#源文件所对应的 Wrap 文件,由 Lua 文件调用 Wrap 文件,再由 Wrap 文件调用 C# 文件。
(2)反射方式:当索引系统 API、dll 库或者第三方库时,如果无法将代码的具体实现进行代码生成,可采用此方式实现交互。缺点:执行效率低。
16.运行 Lua 的三种方式
1.执行字符串
void Start()
{
//lua环境对象,一个LuaEnv实例对应Lua虚拟机,出于开销的考虑,建议全局唯一。
LuaEnv luaenv = new LuaEnv();
//注意这里hello world使用单引号,这样符合C#规则
luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");
//调用lua的print方法,前面会有LUA: 标识
luaenv.DoString("print('hello world2')");
luaenv.Dispose();
}
2.加载lua脚本运行
void Start()
{
LuaEnv luaenv = new LuaEnv();
//加载lua脚本,文件放在Resources下,因为Resource只支持有限的后缀,
//放Resources下的lua文件得加上txt后缀,如LuaTestScript.lua.txt
//TextAsset ta = Resources.Load<TextAsset>("LuaTestScript.lua");
//luaenv.DoString(ta.text);
//require加载文件必须放在Resources下,可以不用写.lua后缀
//require实际上是调一个个loader去加载,有一个成功就不再往下,
//全失败则报文件找不到。
luaenv.DoString("require 'LuaTestScript'");
luaenv.Dispose();
}
3.通过自定义loader方式加载文件
void Start()
{
LuaEnv luaenv = new LuaEnv();
//自定义loader加载的lua脚本可以放在任意合法路径,且不需要添加.txt后缀
luaenv.AddLoader(MyLoader);
//调用require时会优先使用自定义的MyLoader加载文件
//如果自定义loader中有返回值,就不会再去执行系统的loader调用
luaenv.DoString("require 'Test'");
luaenv.Dispose();
}
private byte[] MyLoader(ref string fileName)
{
//加载streamingAssets目录下的文件
string absPath = Application.streamingAssetsPath + "/" + fileName + ".lua";
return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
}
17.C# 调用 lua
1.获取一个全局基本数据类型
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
//获取lua文件中的全局变量
int a = luaenv.Global.Get<int>("a");
Debug.Log(a);
luaenv.Dispose();
}
2.访问一个全局的table
<1>映射到普通class或struct
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
//xLua会帮你new一个实例,并把对应的字段赋值过去。
//table的属性可以多于或者少于class的属性。可以嵌套其它复杂类型。
//要注意的是,这个过程是值拷贝,如果class比较复杂代价会比较大。
Person p = luaenv.Global.Get<Person>("person");
print(p.name + "-" + p.age);
//修改class的字段值不会同步到table,反过来也不会。
p.name = "haha";
luaenv.DoString("print(person.name)");
luaenv.Dispose();
}
class Person
{
public string name; //必须是public且名称一致
public int age;
}
/* Lua中的表
person = {
name = "siki",
age = 100,
Eat = function(self, a, b)
print(a + b)
end
}
*/
<2>利用接口做映射,推荐使用
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
//这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),
//代码生成器会生成这个interface的实例。如果get一个属性,生成代码会get对应的table字段,
//如果set属性也会设置对应的字段。也可以通过interface的方法访问lua的函数。
IPerson p1 = luaenv.Global.Get<IPerson>("person");
print(p1.name + "-" + p1.age);
//通过接口访问,相当于引用,会修改lua中的值
p1.name = "heihei";
luaenv.DoString("print(person.name)");
p1.Eat(1, 2);
luaenv.Dispose();
}
[CSharpCallLua]
public interface IPerson
{
string name { get; set; }
int age { get; set; }
void Eat(int a, int b);
}
<3>通过Dictionary,List做映射
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
//Dictionary只能接受表里键值对形式的
Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("person");
foreach (string key in dict.Keys)
{
print(key + "-" + dict[key]);
}
print("-----------");
//List只能接受表里非键值对形式的
List<object> list = luaenv.Global.Get<List<object>>("person");
foreach (object obj in list)
{
print(obj);
}
luaenv.Dispose();
}
<4>通过LuaTable获取,速度慢,不常用
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
LuaTable tab = luaenv.Global.Get<LuaTable>("person");
print(tab.Get<string>("name"));
print(tab.Get<int>("age"));
luaenv.Dispose();
}
3.访问一个全局的function
<1>映射到delegate,性能好,类型安全,推荐使用
[CSharpCallLua]
public delegate int Add(int a, int b, out int sub);
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
//要生成代码(否则会抛InvalidCastException异常)
Add add = luaenv.Global.Get<Add>("add");
int sub;
int res = add(12, 24, out sub);
print(res);
print(sub);
//引用不置空,Dispose时会报错
add = null;
luaenv.Dispose();
}
/* Lua中的方法
function add(a, b)
print(a + b)
return a + b, a - b
end
*/
<2>映射到LuaFunction,性能差
void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaTestScript'");
LuaFunction func = luaenv.Global.Get<LuaFunction>("add");
object[] os = func.Call(12, 24);
foreach (object o in os)
{
print(o);
}
luaenv.Dispose();
}
18.Lua 调用 C#
1.通过CS.访问不同命名空间下构造函数,静态属性,方法
local newGameObj = CS.UnityEngine.GameObject()
--读静态属性
CS.UnityEngine.Time.deltaTime
--写静态属性
CS.UnityEngine.Time.timeScale = 0.5
--调用静态方法
CS.UnityEngine.GameObject.Find('helloworld')
--调用成员方法,建议用冒号语法糖
testobj:DMFunc(100)
--xlua只一定程度上支持重载函数的调用,C#的int,float,double都对应于lua的number,
--上面例子中DMFunc如果有这些重载参数,如DMFunc(int x) DMFunc(float x)将无法区分开来,
--只能调用生成代码中排前面的
2.将table映射到参数为结构体,类,接口,List,Dictionary的方法
--[[
C#中定义如下结构体,class和接口也支持,接口需要打标签[CSharpCallLua]
public struct A
{
public int a;
}
public struct B
{
public A b;
public double c;
}
成员函数如下:
public void Foo(B b)
--]]
--在lua可以这么调用
obj:Foo({b = {a = 100}, c = 200})
3.将function映射到委托,需要打标签
--[[
C#中定义如下委托
[CSharpCallLua]
public delegate void MyDelegate(int num);
成员函数
public void Foo(MyDelegate p)
--]]
myDelegate = function(num)
print("lua中对应的委托方法"..num)
end
obj:foo(myDelegate)
19.Lua 如何实现面向对象?
Lua 中的类通过 table 和 function 模拟出来,继承通过 __index 和 setmetatable 模拟出来,多态通过重写父类的方法模拟。
-- 定义基类
Base = { name = "base", age = 99 }
function Base:showInfo()
print(self.name .." 年龄 "..self.age)
end
-- 定义基类的new方法,用于构造子类
function Base:new()
local sub = {}
--写法1
--setmetatable(sub, { __index = self })
--写法2
self.__index = self
setmetatable(sub, self)
return sub
end
-- 创建对象
Sub = Base:new()
Sub:showInfo()
-- 多态
function Sub:showInfo()
print(self.name)
end
Sub:showInfo()
Sub 调用 showInfo 方法,在 Sub 中找不到该方法,就会去元表(Base)的 __index 中找,而 Base 的 __index 指向自身,也就是会在 Base 中找该方法
20.点调用和冒号调用区别?
person = { name = "aaa", age = 10}
person.sleep = function()
print("在睡觉")
end
function person:eat()
print(self.name.."在吃饭")
end
--没用到self的方法,冒号调用和点调用都一样
person.sleep()
person:sleep()
--用到self的方法,通过点调用第一个参数为当前的table
--冒号是语法糖,通过冒号调用系统会自动传递当前的table给self
person.eat(person)
person:eat()
21. 怎么对 Table 进行深拷贝?
递归地遍历表的每一个元素,并且在遇到子表时,对子表也进行深拷贝。
function clone(object)
--记录已经拷贝过的表,防止循环引用
--有可能表自己引用自己,如果不记录有可能无限递归
local lookup_table = {}
--递归拷贝函数
local function _copy(object)
--如果遇到的不是表,则直接返回这个值(比如 nil 或基本类型)
if type(object) ~= "table" then
return object
--如果在拷贝过的表中找到该对象,说明已经拷贝过,直接返回拷贝过的表
elseif lookup_table[object] then
return lookup_table[object]
end
--创建新表
local new_table = { }
--记录这个新表,表明 objcet 这个表已经拷贝过来
lookup_table[object] = new_table
--递归拷贝每个键值对,对每个健值对都调用 _copy 拷贝
--之所以对键也要调用 _copy,是因为键也有可能是一个表
for key, value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
--为拷贝表设量与原来的表相同的元表
return setmetatable(new_table, getmetatable(object))
end
return _copy(object)
end
22.多人开发怎么避免全局变量泛滥?
- 尽量使用局部变量
- 使用模块封装设计
--myModule.lua
local myModule = {}
function myModule.Test()
--body
end
return myModule
--test.lua
local myModule = require("myModule")
myModule.Test()
- 使用表模拟命名空间
myNamespace = {}
myNamespace.value = 10
function myNamespace.TestFun()
--body
end
- 制定命名约定,使用有意义的命名约定来避免命名冲突,并且清晰的标记处哪些变量是全局的,比如:全局标识_脚本名_变量名(G_MyLuaTest_age)
23.所有的元方法
-- 所有可用的元方法
local all_metamethods = {
"__index", -- 索引访问
"__newindex", -- 索引赋值
"__call", -- 函数调用
"__tostring", -- 转换为字符串
"__concat", -- 连接操作 (..)
"__len", -- 长度操作 (#)
"__add", -- 加法 (+)
"__sub", -- 减法 (-)
"__mul", -- 乘法 (*)
"__div", -- 除法 (/)
"__mod", -- 取模 (%)
"__pow", -- 幂运算 (^)
"__unm", -- 一元负号 (-)
"__eq", -- 等于 (==)
"__lt", -- 小于 (<)
"__le", -- 小于等于 (<=)
"__gc", -- 垃圾回收
"__mode", -- 弱引用模式
"__pairs", -- pairs() 迭代器 (Lua 5.2+)
"__ipairs", -- ipairs() 迭代器 (Lua 5.2+)
-- 位运算 (Lua 5.3+)
"__band", -- 按位与 (&)
"__bor", -- 按位或 (|)
"__bxor", -- 按位异或 (~)
"__bnot", -- 按位取反 (~)
"__shl", -- 左移 (<<)
"__shr", -- 右移 (>>)
-- 整数除法 (Lua 5.3+)
"__idiv", -- 整数除法 (//)
}
更多推荐

所有评论(0)