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>映射到普通classstruct
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",       -- 整数除法 (//)
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐