一文一起lua入门

153 阅读14分钟

image.png

  • 什么是lua:

    lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放(地址:github.com/zhyingkun/l… )。其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

  • lua特性:

    • 轻量级:

      使用标准C编写,编译后仅仅一百余K,方便嵌入别的程序中

      • 我们都知道Redis中通过运行lua脚本可以执行原子性任务,但为什么不是通过执行某个Java的方法来执行原子任务呢?因为Java太重了,redis的运行不可能把一部分内存再让给JVM;
      • 其他许多场景也正是由于lua对资源消耗少,例如游戏开发,独立应用脚本,Web应用脚本,扩展和数据库插件(如 MySQL Proxy,MySQL workbench)
    • 可扩展:

      lua提供了非常易于使用的扩展接口机制,由宿主语言(C或C++)提供

      • 我们Java程序员经常会有需要调用JNI本地代码的场景,直接调用C的代码可能有所不便,而通过lua间接调用C即可充分利用lua的可扩展特性
    • 支持面向过程 & 函数式编程

    • 自动内存管理,lua只提供一种通用类型的表(table),用他可以实现数组、哈希表、集合、对象。 使用table作为主要数据结构对内存管理很有好处:GC逻辑更简单,内存碎片更少导致GC时延更短...

    • 实现 闭包

      闭包:是一个由函数 和 该函数能够访问到的 非局部变量 组成的实体。

      举个例子:

          // 在没有闭包的调用栈中执行
       function parent() {
          let n = 1 // 局部变量n,基本数据类型1
          function son() { // 局部变量son
      
          // son执行时,parent已经出栈,变量n访问不到了
          // 报错!
          console.log(n);
        }
            return son  
        // parent出栈
      }
      
        let son = parent() // parent入栈
        son()// son入栈,
      
      

      问题的矛盾点是:

      函数式编程中函数是一等公民,但是函数和常见的基本数据类型或者引用数据类型还是有所不同的,这个不同点就导致无法直接将函数当作其他数据类型来看待。 代码中,函数son作为parent的返回值,被接着执行了,此时son 和 局部变量 n 的关系就很暧昧——理论上访问不到,但是希望它访问的到 —— 需要新技术 —— 闭包;

      闭包的具体实现:

      将闭包所需要的数据,都存储到堆上(Heap)。

      image.png

      参考博客:juejin.cn/post/708454…

    • 通过闭包和table可以方便地支持面向对象的一些关键机制,比如继承与重载,数据抽象,虚函数等。

    • Lua语法

      lua被设计成一款使用方便的解释型语言。此特征就导致其运行模式和语法都和python类似

      • 交互式编程 & 脚本式编程

        聊到这里就需要说明lua的运行模式;lua会首先将代码编译成一种中间状态(字节码),然后由解释器逐行执行。上层的调用只需要执行 lua example.lua 即可,底层实则优先由 luac 编译成 example.luac(或者在win下是example.out)文件,之后再解释执行;但是由于lua在设计上不鼓励直接运行.luac文件,所以解释执行的方案需要曲线救国:

        1. 编写启动脚本loader.lua
          package.loaded[...] = nil -- 清除可能存在的同名模块
          loadfile(arg[1])()     -- 加载并执行第一个命令行参数指定的文件
          
        2. 命令行中 通过启动脚本运行luac文件
          lua loader.lua example.luac
          

        为什么不鼓励?无法解释.luac那为什么要面向开发者提供luac编译器? 因为lua VM的实现方式没有考虑向后兼容,也没有考虑跨平台;也就是说编译得到的.luac文件如何理解,每个版本可能都有所不同。(小众语言,果然不如Java)

        详情见:cloud.tencent.com/developer/a…

      • 变量

        lua有三种类型的变量:全局变量、局部变量、表中的域。

        在默认情况下,变量总是全局的。除非用local显示声明为局部变量(局部变量的作用域为声明位置开始到所在语句块的结束位置)

        lua全局变量的实现

        在之前写Java程序时,全局变量的语义是用来描述整个jar程序中可见且唯一的一些变量,使用时往往由public static修饰,在类加载器加载.class文件时就将这个变量加载入堆内存中,时刻对外暴露;lua中实现也类似 —— 定义的全局变量会进入_G全局变量表(类似hashmap)中,通过_G[varname]即可访问


        不同的点在于:lua的全局变量并非真正的全局变量,其设计遵循环境与模块的隔离

        我们先来梳理下声明和使用全局变量的流程:

        • 声明很简单,a = 10即可

        • 使用:

          一个很简单的想法是:全局变量这么多,很容易冲突啊?lua通过环境进行了隔离

          对于任意自由名称(没有关联到显式声明上的名称),例如下面的x,y

          local z = 10
          x = y + z
          

          编译器将代码中所有自由名称转化成_ENV.x,因此代码变成

          local z = 10 
          _ENV.x = _ENV.y + z
          

          那么_ENV又是什么?

          它可以是任意的表,这样的表就被称为一个环境。 通过加载不同的表,声明程序运行的不同环境,从而做到全局变量一定程度上的隔离。

          参考博客:cloud.tencent.com/developer/a…

      • table

        table在lua中用来实现模块、包和对象,本身的抽象程度很高,同时也用来当作数组、字典这种简单的数据结构

        基础操作

        -- 简单的 table
          mytable = {}
          print("mytable 的类型是 ",type(mytable))
        
          mytable[1]= "Lua"
          mytable["wow"] = "修改前"
          print("mytable 索引为 1 的元素是 ", mytable[1])
          print("mytable 索引为 wow 的元素是 ", mytable["wow"])
        
          -- alternatetable和mytable的是指同一个 table
          alternatetable = mytable
        
          print("alternatetable 索引为 1 的元素是 ", alternatetable[1])
          print("alternatetable 索引为 wow 的元素是 ", alternatetable["wow"])
        
          alternatetable["wow"] = "修改后"
        
          print("mytable 索引为 wow 的元素是 ", mytable["wow"])
        
          -- 释放变量
          alternatetable = nil
          print("alternatetable 是 ", alternatetable)
        
          -- mytable 仍然可以访问
          print("mytable 索引为 wow 的元素是 ", mytable["wow"])
        
          mytable = nil
          print("mytable 是 ", mytable)
          
          fruits = {"banana","orange","apple"}
          -- 返回 table 连接后的字符串
          print("连接后的字符串 ",table.concat(fruits))
        
          -- 指定连接字符
          print("连接后的字符串 ",table.concat(fruits,", "))
        
          -- 指定索引来连接 table
          print("连接后的字符串 ",table.concat(fruits,", ", 2,3))
          
          fruits = {"banana","orange","apple"}
        
          -- 在末尾插入
          table.insert(fruits,"mango")
          print("索引为 4 的元素为 ",fruits[4])
        
          -- 在索引为 2 的键处插入
          table.insert(fruits,2,"grapes")
          print("索引为 2 的元素为 ",fruits[2])
        
          print("最后一个元素为 ",fruits[5])
          table.remove(fruits)
          print("移除后最后一个元素为 ",fruits[5])
        
      • 模块:

        模块就是一个封装库,lua由变量、函数等元素组成一个table,这个table只要暴露出去,就作为了一个模块。于是声明和导入其实就是基于table的

          -- 文件名为 module.lua
          -- 定义一个名为 module 的模块
          module = {}
        
          -- 定义一个常量
          module.constant = "这是一个常量"
        
          -- 定义一个函数
          function module.func1()
              io.write("这是一个公有函数!\n")
          end
           
          local function func2()
              print("这是一个私有函数!")
          end
           
          function module.func3()
              func2()
          end
           
          return module
        
        -- test_module.lua 文件
          -- module 模块为上文提到到 module.lua
          require("module")
        
          print(module.constant)
        
          module.func3()
        

        与C结合:

        Lua可以通过loadlib函数轻松加载由C而来的共享对象库

          local path = "/usr/local/lua/lib/libluasocket.so"
          local f = loadlib(path, "luaopen_socket")
        
      • 元表(Metadata)

        对表本身的操作就是元方法,元方法就存储在元表中

        lua中有两个重要函数用来处理元表

        • setmetatable(table,metatable)
        • getmetatable(table)

        常见的元方法:

        1. _index

        用来给table设置基础数据

        当你通过键来访问 table 的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的__index 键。如果__index包含一个表格,Lua会在表格中查找相应的键。

        由于面向函数编程的自由性, _index可能是一个表,也可能是一个function,对于function而已,默认接受table名和key

          Lua 5.3.0  Copyright (C) 1994-2015 Lua.org, PUC-Rio
          > other = { foo = 3 } 
          > t = setmetatable({}, { __index = other }) 
          > t.foo
          3
          > t.bar
          nil
        
          mytable = setmetatable({key1 = "value1"}, {
       __index = function(mytable, key)
         if key == "key2" then
           return "metatablevalue"
         else
           return nil
         end
       end
      })
      
      print(mytable.key1,mytable.key2)
      
      1. _newindex

      用来给table绑定异常处理的handler

      当你给表的一个缺少的索引赋值,解释器就会查找__newindex 元方法:如果存在则调用这个函数而不进行赋值操作。

      1. 为表添加操作符

      用来实现表层面的向量操作,通常会定义_add,_sub,_mod等操作

      实例代码(剽窃自菜鸟教程)

        -- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
        -- 自定义计算表中最大键值函数 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
        
        -- 两表相加操作
        mytable = setmetatable({ 1, 2, 3 }, {
          __add = function(mytable, newtable)
            for i = 1, table_maxn(newtable) do
              table.insert(mytable, table_maxn(mytable)+1,newtable[i])
            end
            return mytable
          end
        })
        
        secondtable = {4,5,6}
        
        mytable = mytable + secondtable
                for k,v in ipairs(mytable) do
        print(k,v)
        end
      
      1. _call

      使得一个table可以像函数一样被调用

      当给table传入一个参数(而不是下标时),触发call方法;使得table类似也具有function特性

      示例代码

       -- 计算表中最大值,table.maxn在Lua5.2以上版本中已无法使用
       -- 自定义计算表中最大键值函数 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))
      

      table确实很灵活,后续面向对象的思想完美契合table;其中或许和面向函数编程的思想暗合 —— 一个table只需要具有常规数据类型的属性和函数类型的属性 就是一个对象。

      • 协同程序 & 多线程

        lua本身并不支持多线程,单个lua虚拟机只能运行在一个线程下。如果想并行处理一些操作,必须为每个线程部署到独立的lua虚拟机上。而现场的成熟多线程库有:lanses和Effil,他们都试图隐藏多虚拟机的细节,并且实现不同虚拟机间的相互调用。

        而线程与协同程序的主要区别在于:一个具有多个线程的程序可以同时运行几个线程,而每一时刻只能有一个协同程序在运行。协同程序有点像同步了的多线程,或者等待同一把锁的多线程。

        • Question1:Lua虚拟机为何如此设计:

          设计为一个虚拟机默认只能在一个线程中运行,不符合我们传统Java程序的理解,但是符合脚本程序执行的需求 —— lua往往作为程序执行的下游(最后一公里),因此多线程通常由上游调用方完成;另一方面,lua设计为轻量级,易嵌入,因此虚拟机设计从简单出发,同时避免线程切换的复杂性,多线程下内存分配和回收的复杂性等。

      • Question2:不同的lua多线程的实现方式:

        1. 多线程库 lanses & Effil

          思路是启动多个虚拟机,对上层隐藏多虚拟机的细节

        2. 云风的skynet 通过框架支持多线程 //此处不懂如何实现的

      • lua垃圾回收

        自动化内存管理大致有两种方式:

        1. 引用计数
          • 标记每个对象的被引用数量,当降为0后自动清理
        2. 垃圾收集
          • 由垃圾回收器在某时刻完成对垃圾对象的收集

        引用计数不管在哪里,第一个问题都是需要解决循环引用,众所周知python GC使用的就是引用计数,它解决的方式是引入部分标记清除(或者是是可达性分析方法)

        • python里操作如下: 当内存不足时,触发标记清除 —— 由栈上变量引用的对象作为root,开始可达性分析,无法到达的对象即为要淘汰的对象。
        • 具体操作过程:维护一个双向链表(我觉得可以叫脏对象链表),链表A用来存放可能存在循环引用的对象(那些容器类型,自定义类型有可能有循环引用,而基本类型,String不会有)。在标记阶段通过可达性分析将可达的对象都标记,在清理阶段,遍历上述链表,将没有标记过的清除这里很不确定,网上资料很乱很杂,还有很多谜语人,感觉需要找一本权威的书来

        而lua没有使用引用计数,选择垃圾收集,官方给出的原因是:对于动态类型语言,使用引用计数会带来overhead的压力;

        • 我对此的理解是:使用引用计数法天生的问题是 需要随着代码执行实时更新对象的引用,而代码的逻辑可能导致对象的引用数变化特别频繁;静态类型语言在编译期做了许多优化来去掉不必要的计数调整(例如C++使用许多inline函数消除掉RAII中的冗余操作),但是动态类型语言只能在运行时确定数据类型,这就导致重载状态下的函数无法推测到底要内联哪个函数(下面细聊);于是动态类型在运行时无可避免要承受大量计数调整的操作
        • 具体来说:对于如下代码:
        inline int add(Student a, Student b) {
               return a.age + b.age;
           }
           int main() {
               int result = add(alice, bob); 
               return 0;
           }
        
        

        C++这类静态类型在编译时可能会通过内联转化成

        int main() {
               int result = alice.age + bob.age;  
               return 0;
           }
        

        代码是随便写的,大佬别在意细节

        对于上面的代码,进入add后对alice和bob的引用增加1,调用age时又增加1,跳出add后将原本调用age的减去; 对于下面的代码,只需要在调用age时增加1的引用计数即可

        换句话说:内联避免了函数调用的成本 而静态类型无法在编译时期内联优化,因为如果有多个重载的add,根据参数类型不同选择执行不同的代码,但动态类型语言在运行前做不到。

        接下来就是对标记清除法的优化历程了

        1. Lua 5.0 之前,垃圾回收的逻辑简单直接 —— 每创建一个对象就垃圾回收一次,每次垃圾回收都一次性执行完成,过程中通过双色标记法。
        2. Lua 5.0 针对垃圾回收频率做了调整,当内存分配超过了上次GC后的两倍,就跑一次全量GC。
        3. Lua 5.1 支持了渐进式垃圾回收,原理就是三色扫描替换双色扫描
        4. Lua 5.2 推出分代GC 优化GC效果,但在5.3又删除了
        5. Lua 5.4 真正推出分代GC
      • Lua 面向对象

        据leader所说:lua中万物皆table,原因在于lua使用table来描述对象的属性

        直接上示例代码

        -- 元类  
          Rectangle = {area = 0, length = 0, breadth = 0}  
            
          -- 派生类的方法 new  
          function Rectangle:new (o,length,breadth)  
            o = o or {}  
            setmetatable(o, self)  
            self.__index = self  
            self.length = length or 0  
            self.breadth = breadth or 0  
            self.area = length*breadth;  
            return o  
          end  
        
          -- 派生类的方法 printArea  
          function Rectangle:printArea ()  
            print("矩形面积为 ",self.area)  
          end
        

        思想:类就是对象

        • 在lua中,类的定义就是一个table,其实就是具有默认值的一个对象。
        • 如何根据这个类创建对象?需要从table层面提供一种方法 —— table的元方法 —— 给这个原始table设置一个new方法,由其返回一个新的tableB
        • 如何确定tableB是原始table类产生的对象?需要在table层面表明这种关系的一种方式 —— table的元方法 —— 给tableB的_index绑定原始table,实现的效果就是tableB是原始table的实现/重写...

        之前无法理解晖哥说 Java是一门很中庸的语言,如今看来 Java之外天外有天