Unity游戏开发客户端面经——lua(初级)

前言:记录了总6w字的面经知识点,文章中的知识点若想深入了解,可以点击链接学习。由于文本太多,按类型分开。这一篇是lua常问问题总结,有帮助的可以收藏。


1.pairs与ipairs区别

        pairs会遍历所有key,对于key的类型没有要求,遇到nil时可以跳过,不会影响后面的遍历,既可以遍历数组部分,又能遍历哈希部分。

        ipairs只会从1开始,步进1,只能遍历数组部分, 中间不是数字的key忽略, 到第一个不连续的数字为止(不含),遍历时只能取key为整数值,遇到nil时终止遍历。

2.点和冒号区别

        点  :无法传递自身,需要显示传递

        冒号 :隐式传递自身

3.Lua如何实现面向对象

        Lua面向对象编程是基于元表metatable,元方法__index来实现的。

        通过元表的__index元方法,将一个table的__index元方法设置为另一个table,那么后者的方法就被前者继承。

        如果访问了lua表中不存在的元素时,就会触发lua的一套查找机制,也是凭借这个机制,才能够实现面向对象的。

        总结元表的查找步骤:

        在表中查找该元素,如果找到,返回该元素,找不到则判断该表是否有元表(setmetatable(A,B)     -- 把 B设为 A的元表 ),如果没有元表,返回nil,有元表则判断元表有没有__index方法不是直接看元表有该元素!),如果__index方法为nil,则返回nil;如果__index方法是一个表,则重复上述步骤;如果__index方法是一个函数,则调用该函数,并返回该函数的返回值。

 4._index 与 _newindex的区别

        注意:__index 和__newindex归为元表方法。

        __index:当子脚本不存在被访问的元素的时候,lua就会调用__index,去父脚本里面查找该元素。

        __newindex:当赋值时,如果赋值一个不存在的索引。

        如果__newindex是一个表,那么会把这个值赋值到newindex所指的表中(有这个索引就修改,没有就创建),不会修改自己;

        如果__newindex是一个函数,那么会调用这个函数。

代码:

        1.__newindex指定一个函数

        2.__newindex指向一个空表

3.__newindex指定一个已经定义的表。

        只能在元表中指定

5._rawset与_rawget的区别

        _rawget:访问时,不想从__index 对应的元方法中查询值

        _rawset:更新时,不想执行__newindex 对应的元方法

6.如何实现一个只读表

        利用__index 与 __newindex来实现。

   

详细请看:Lua中rawset 函数使用_zhaixh_89的博客-CSDN博客_lua rawset

7.Table

        为了提高table的插入查找效率,在table的设计上,采用了 array数组 和 hashtable(哈希表) 两种数据的结合。

        数组部分:从 1 开始做整数数字索引,这可以提供紧凑且高效的随机访问;数组部分存储在 TValue *array 中,其长度信息存储在 int sizearray 中。

        哈希表:存储在 Node *node,哈希表的大小用 lu_byte lsizenode 表示,lsizenode表示的是2的几次幂,而不是实际大小,因为哈希表的大小一定是 2 的整数次幂。哈希冲突后,采取开放定址法,应对 hash 碰撞。

        每个 Table 结构,最多会由三块连续内存构成:

               (1) 一个 table 结构

               (2) 一块存放了连续整数索引的数组

               (3) 一块大小为 2 的整数次幂的哈希表

       

 所以table会将部分整形key作为下标放在数组中, 其余的整形key和其他类型的key都放在hash表中。

        table中的hash表的实现结合了以上两种方法的一些特性:

                查找和插入等同链地址法(拉链法)复杂度。

                内存开销近似等同于开放定址法。

   详细请看:

【Lua 5.3源码】table实现分析_YzlCoder的博客-CSDN博客

8.string 类型

        string:底层实现对字符串做了分类处理。

        只要大于 40 字节的字符串,即使内容一样,也会在堆内存中,重新生成一个全新的字符串数据拷贝;

        当小于 40 字节时,使用全局 stringtable 进行管理,相同字符串只会有一份数据拷贝,每份相同的对象只是存在一个 hash 值,用来索引 stringtable。

9.lua的GC算法

     LuaGC 采用三色增量标记清除算法

     详细请看 Lua设计与实现--GC篇 - 知乎

lua的GC原理_LJY_rookie的博客-CSDN博客

10.C# 与 xlua交互

        10.1 交互原理

        1、在C#中将需要热更的类标记(标签,静态列表,动态列表)。

        2、生成函数连接器来连接lua脚本和c#函数。

        3、对编译生成的dll进行修改。

        4、把代码的执行路径修改到lua脚本中(如果lua中执行了对应的热修复函数,则把il中对应的函数替换为对应的lua函数)。

        10.2 在Lua中,已知GameObject obj。那如下代码在Lua侧调用,如下代码会发生什么底层交互obj.transform.setactive( false )

        Lua本身提供了C_API,让我们Push一个值到Lua虚拟栈上,Lua可以通过访问Lua虚拟栈来访问这个对象,Lua虚拟栈是Lua和其他语言交换数据的中介。

        Xlua对这个接口进行了封装,并提供了一系列push方法,让我们可以把一个C#对象push到lua的虚拟栈上。

        10.3 xLua是如何记录C#或者Unity引擎对象的

        引用存储

        10.4 如何在Lua里,避免上述的这些问题

        注意对象缓存

        10.5 C#和xlua如何互相交互?

        1. xlua/tolua都是借助的lua虚拟机完成的跨语言调用的。(看虚拟机怎么完成跨语言调用的)

        2. xlua/tolua对虚拟机做了上层封装,可以便捷访问Lua侧table或函数。(看下虚拟机上层接口封装这块的代码)

        3. 其次就是反射和warp了。Lua在调用C#的时候,实际上传递过来的是字符串,为了避免纯反射,其实xlua/tolua封装了一个keyvalue的字典,这个key是个字符串,就是引用类型的字符串(Lua传递过来的),通过这个字符串就能拿到这个typeofvalue,通过这个方式,避免了反射。

        4. 综述:

         Lua虚拟机,实现了跨语言交互。

        xLua或者toLua正是因为用了这个Lua虚拟机,才具备了跨语言调用的能力;其次,xLua或者tolua对Lua虚拟机进行了一定程度的封装,这样你在xLua或者toLua中,才能便的使用一些接口,比如获取Lua的变量或者table或者函数之类的功能。从这一点上来说,xLua或者toLua只是个上层应用,而Lua虚拟机是底层实现。

        xLua或者tolua不仅封装应用了Lua虚拟机,还额外优化了性能,体现在刚才聊到的容器,存储keyvalue映射,这样就不用反射了。

11.Lua 如何与 C# 交互

        C#与Lua进行交互主要通过虚拟栈实现,栈的索引分为正数与负数,若索引为正数,则1表示栈底,若果索引为负数,则-1表示栈顶。

        C#调用 Lua 实际上是:由 C# 先把数据放入栈中,由 Lua 去栈中取数据,然后返回数据对应的值到栈顶,再由栈顶返回 C#。 

        Lua 调 C# 也一样:先编写自己的 C模块,然后注册函数到 Lua 解释器中,然后由 Lua 去调用这个模块的函数。

        详细请看:C#与Lua交互过程及原理_钢与铁的博客-CSDN博客_lua 调用c#原理

12.Lua中的类型

        1.nil         2. number 整数         3.table 表         4.string 字符

        5.userdata 自定义类         6.function 函数         7.bool 布尔         8.therad 线程

13.Lua中的循环

循环类型

描述

while 循环

在条件为 true 时,让程序重复地执行某些语句。执行语句前会先检查条件是否为 true。

for 循环

重复执行指定语句,重复次数可在 for 语句中控制。

repeat...until

重复执行循环,直到 指定的条件为真时为止

循环嵌套

可以在循环内嵌套一个或多个循环语句(while do ... end;for ... do ... end;repeat ... until;)

泛型循环

pairs会遍历全部的值ipairs会从1开始遍历遍历到非连续位置停止,上面代码改成ipairs最后的999将不会被打印

14.闭包

   14.1 闭包函数

          声明在一个函数中的函数,叫做闭包函数。

   14.2 闭包概念

        内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。

   14.3 闭包构成的条件

        在函数嵌套(函数里面定义另一个函数)的前提下;

        内部函数使用了外部函数的变量(也包括外部函数的参数);

        外部函数返回了内部函数。

    14.4 闭包的作用

        闭包可以保存外部函数内的变量,可以根据外部函数内的变量来得到不同的结果,不会随着外部函数调用完而销毁,当闭包执行完成后,仍可以保持当前的运行环境,执行结果依赖于该函数上一次的运行结果。

        但是由于闭包引用了外部函数的变量,则外部函数的变量没有及时释放,这样会比较消耗内存。

        14.5 闭包实例        

function add()

    local x = 0

    return function ()

        x = x + 1

        return x

    end

end





add1 = add()

print(add1()) --1

print(add1()) --2



add2 = add()

print(add2()) --1

        当函数add1执行时,函数add 已经返回,add1的局部变量x已经在栈中退出,但是add1却能访问x。这是因为x是函数add的upvalue。

        而add2函数执行的结果表明add1和add2并没有共享upvalue,而是单独有一份自己的upvalue。

详细请看:

https://blog.csdn.net/qq_30585525/article/details/90898377?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166144088916782388055238%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=166144088916782388055238&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-90898377-null-null.142^v42^new_blog_pos_by_title,185^v2^control&utm_term=闭包unity&spm=1018.2226.3001.4187https://blog.csdn.net/qq_30585525/article/details/90898377?ops_request_misc=%7B%22request_id%22%3A%22166144088916782388055238%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=166144088916782388055238&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-2-90898377-null-null.142%5Ev42%5Enew_blog_pos_by_title,185%5Ev2%5Econtrol&utm_term=%E9%97%AD%E5%8C%85unity&spm=1018.2226.3001.4187

15. LoadFile、loadstring、dofile、require

      Loadfile

        只编译,不运行,它只会加载文件,编译代码,不会运行文件里的代码。

      dofile(filename) 

        用于加载一个指定路径filename的代码块,并运行。

      require(modname)

        对于同一个代码块只会加载一次(加载过会保存起来),和dofile类似,加载编译后也会自动运行。

        require和dofile有点像,不过又很不一样,require在第一次加载文件的时候,会执行里面的代码。但是,第二次之后,再次加载文件,则不会重复执行了。换句话说,它会保存已经加载过的文件,不会重复加载。require的返回值会被存储cache起来,所以一个文件最多只会执行一次,即使被require很多次。

      Load

        用于加载字符串代码块

详细请看:

https://blog.csdn.net/qq_44918090/article/details/126021457?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166221363916781683983292%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=166221363916781683983292&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-126021457-null-null.142^v46^new_blog_pos_by_title&utm_term=loadfile、loadstring、dofile和require的区别&spm=1018.2226.3001.4187https://blog.csdn.net/qq_44918090/article/details/126021457?ops_request_misc=%7B%22request_id%22%3A%22166221363916781683983292%22%2C%22scm%22%3A%2220140713.130102334.pc_all.%22%7D&request_id=166221363916781683983292&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-2-126021457-null-null.142%5Ev46%5Enew_blog_pos_by_title&utm_term=loadfile%E3%80%81loadstring%E3%80%81dofile%E5%92%8Crequire%E7%9A%84%E5%8C%BA%E5%88%AB&spm=1018.2226.3001.4187

16. C# 内存管理与 lua的内存管理有什么区别?C++呢?

        两个都有垃圾回收机制,不过c#有clr提供更多的支持,c++没有垃圾回收,要自己调用析构函数,手动释放。

17.lua怎么实现C#委托的功能

        利用元表的__index方法,多个元表层级嵌套,利用多个__index实现。

        利用lua实现 观察者模式

        详细请看:观察者模式--lua实现_ljf551的博客-CSDN博客


希望此篇文章可以帮助到更多的同学

猜你喜欢

转载自blog.csdn.net/Sea3752/article/details/127539068