Lua 面试题

1.Lua是如何进行热更的?

由于Lua不需要编译,因此Lua代码可以直接在Lua虚拟机里运行。而C#代码在开始运行之前,都会一起装在到内存的代码段,没有办法更新新的代码。
动态装载:app + 内置脚本解释器,由这个脚本解释器动态的执行脚本代码
Lua = Lua解释器 + Lua脚本


2.Lua有哪些数据类型

nil(空类型),boolean,number(数字类型,不细分),string,function,userdata,thread,table


3.Lua中的string

Lua的字符串分为短字符串和长字符串。
短字符串: 长度小于等于40为短字符串。相同字符的短字符串是共用同一个的。
长字符串: 长度大于40为长字符串。相同的两个长字符串是两个独立的字符串。


4.Lua中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种颜色分类如下:
白色:表示对象还没有被GC标记过,这也是任何一个对象创建后的初始状态。换言之,如果一个对象在结束GC扫描过程后仍然是白色,则说明该对象没有被系统中的任何一个对象所引用,可以回收其空间了。(白色又分为白1、白2)。
灰色:当前对象为待扫描状态,表示对象已经被GC访问过,但是该对象引用的其他对象还没有被访问到。
黑色:当前对象为已扫描状态,表示对象已经被GC访问过,并且该对象引用的其他对象也被访问过了。
备注:白色分为白1和白2。原因:在GC标记阶段结束而清除阶段尚未开始时,如果新建一个对象,由于其未被发现引用关系,原则上应该被标记为白色,于是之后的清除阶段就会按照白色被清除的规则将新建的对象清除,这是不合理的。于是lua用两种白色进行标识,如果发生上述情况,lua依然会将新建对象标识为白色,不过是“当前白”(比如白1)。而lua在清扫阶段只会清扫“旧白”(比如白2),在清扫结束之后,则会更新“当前白”,即将白2作为当前白。


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的长度?

如果tabl内容是连续的可以用#,存在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()

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()
  • 4
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值