lua知识点

1 闭包 

Lua 中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。
第一类值指:在 Lua 中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。
词法定界指:被嵌套的函数可以访问他外部函数中的变量。这一特性给 Lua 提供了强大的编程能力。
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。
简单的说闭包是一个函数加上它可以正确访问的 upvalues

 

2 元表

Lua 中的每个值都可以用一个 metatable。这个 metatable 就是一个原始的 Lua table ,它用来定义原始值在特定操作下的行为。你可以通过在 metatable 中的特定域设一些值来改变拥有这个 metatable 的值的指定操作之行为

metatable 中的键名为 事件 (event) ,把其中的值叫作 元方法 (metamethod)

可通过函数getmetatable查询任何值的元表。函数setmetatable替换表的元表(debug库不可以)

__index:我们访问一个表中的元素不存在时,则会触发去寻找__index元方法,如果不存在,则返回nil,如果存在,则返回结果。

 __newindex:当给你的表中不存在的值进行赋值时,lua解释器则会寻找__newindex元方法,发现存在该方法,则执行该方法进行赋值,注意,是使用rawset来进行赋值。

 rawget (table, index)

 功能:获取表中指定索引的值,此函数不会调用任何元表的方法,成功返回相应的值,当索引不存在时返回nil

 注:本函数只能用于以数字索引访问的表 如:t={"1","cash"}
 rawset (table, index, value)
 功能:设置表中指定索引的值,此函数不会调用任何元表的方法,此函数将返回table

 

3 协程

 协程采用的主动让出控制权的方式,因而协程中的的指令顺序是可以预计的;相反线程由于采用的是抢占式,导致线程的执行顺序不可知,因此存在同步的问题

Lua语言实现的协程是一种非对称式(asymmetric)协程,或称半对称式(semi-symmetric)协程,又或干脆就叫半协程(semi-coroutine)。这种协程机制之所以被称为非对称的,是因为它提供了两种传递程序控制权的操作:一种是(重)调用协程(通过coroutine.resume);另一种是挂起协程并将程序控制权返回给协程的调用者(通过coroutine.yield)。

Lua 协程有三个状态:挂起态(suspended)、运行态(running)、停止态(dead)。可以通过coroutine.status来查看协程状态

创建一个协程需要调用coroutine.create 。它只接收单个参数,这个参数是 coroutine 的主函数。 create 函数仅仅创建一个新的coroutine 然后返回一个类型为thread的对象,并不会启动 coroutine 的运行。

 lua所支持的协程全称被称作协同式多线程(collaborative multithreading)。Lua为每个coroutine提供一个独立的运行线路。然而和多线程不同的地方就是,coroutine只有在显式调用yield函数后才被挂起,同一时间内只有一个协程正在运行。

 

4 模块 

 Lua提供简易的加载及创建模块的方法,由require、module方法及package表组成

    1、module (name [, ···])

  功能:建立一个模块。

  当package.loaded[name]中存在时,当中的表作为module;

  当在全局表中存在name指定的表时,此表作为module;

  当以前两种情况都不存表name时,将新建一个表,并使其作为全局名name的值,并package.loaded[name],而且设t._NAME为name,t._M为module,t._PACKAGE为包的全名(模块名-组件a.b.c);最后把此module设t作为当前函数的新环境表和package.loaded[name]的新值(也就是说,旧的环境表将不能访问,除了加上package.seeall参数外),以被require使用

  module(name)后的可选参数为接收module名的函数,如package.seeall 

   2、require (modname)

  功能:加载指定的模块。

  此函数先检测package.loaded表中是否存在modname,存在则直接返回当中的值,没有则通过自定义的加载器加载modname。

  查找加载器顺序:

  (1)检测package.preload表是否存在modname,有则加载

  (2)通过Lua Loader加载,通过查找存放于package.path的路径加载,有则加载

  (3)通过C Loader加载,通过查找存放于package.cpath的路径加载,有则加载

  (4)通过all-in-one Loader加载:

  通过查找modname.dll并查找当中的luaopen_

  其中XXXX为载块名-后的字符用_替换.后的字符:如:a.v1-b.c 当函数名为luaopen_b_c

  当require查找的不是一个Lua库或C库,它就会调用all-in-one loader,此加载器是用C路径作为载块的目录,

  当查找到合适的加载器时,require就会加载其中的模块,当加载器有返回值,将会存放于package.loaded[modname]表。最后返回package.loaded[modname]表

  当加载失败时,require将触发错误   

      require只认文件名,不认路径名。要加入路径名信息的话,就要写成父模块子模块的形式。

     3、package.cpath

  功能:用于require C loader的搜索路径

  可以通过修改LUA_CPATH变量(luaconf.h)修改此值

  4、package.loaded

  功能:一个用于让require知道哪些模块已加载的记录表,如果package.loaded已经有require要的值,则直接返回此值

  5、package.loadlib (libname, funcname)

  功能:通过动态连接C函数库方式加载Lua扩展库

  libname为库文件名,funcname为入口函数(此函数必须为纯C接口函数 c++则需用 extern "C" {} 进行限制)

  6、package.path

  功能:用于require Lua loader的搜索路径

  可以通过修改LUA_PATH变量(luaconf.h)修改此值

  7、package.preload

  功能:一个用于保存特殊模块加载器的表

  8、package.seeall(module)

  功能:为module设置一个元表,此元表的__index字段的值为全局环境_G。所以module可以访问全局环境

   

5 环境
 全局变量table
        lua把所有的全局变量存在一个table里,并把这个table赋值给一个全局变量_G
        _G也在这个全局变量的table里,它就是一个普通的全局变量
        可以用这种方法遍历所有全局变量 for k, v in pairs(_G) do print(k,v) end
        可以使用_G["全局变量名"]来访问全局变量,与直接访问一样效果
        可以通过对_G设置元表来控制对全局变量的访问行为
        _G只是个全局变量,对其重新赋值则它就是别的东西了,这种情况只能通过getfenv找回全局table了
 非全局的环境
        lua中每个函数都可以有自己的环境
        lua中加载每个程序块其实就是把这个程序块看作是一个函数来调用一下
        每个程序块最后都可以有一行return xxx
        如果有require后返回的就是这个xxx
        所以可以想象一个程序文件被加载就是一个函数被调用了,这个文件的内容整个就是一个大函数
        因为每个函数都可以有自己的环境,所以每个程序块都有自己的环境
        这也正是为什么会命名为setfenv、getfenv了
       setfenv(f, table)
            1.第一个参数如果是函数则改变这个函数的环境为第二个参数的table
            2.第一个参数如果是数字,则1代表当前函数,2代表调用自己的函数,以此类推
       getfenv(f)
            1.参数f的使用方法与setfenv一样
            2.返回当前环境的table
        setfenv(1, {})如果这样调用则所有环境变量都将无法访问,包括如print函数,因为环境table是空的
        继承环境环境:
            envt = {}
            setmetatable(envt, {__index = _G})
            setfenv(1, envt)
            print("hello")
        在程序块中调用setfenv(1,table)就会修改当前程序块的环境
        每个新创建的函数(包括闭包)的环境都默认继承了创建者的环境
        当改变当前环境后,后续创建的函数的环境都将随之改变
        利用默认继承的功能方便的实现命名空间之类的功能
        可以来回切换多个环境来实现多套变量定义和值内容
        函数在查找要访问的变量时总是以这种顺序,局部->全局->环境table元表的__index来查找
        环境主要用于加载模块等环节,另外的一个主要用途就是当作一个独立的数据存储集
 
 
7 热更新

  Lua5.1.4代码分析 -如何实现Lua代码的热更新       
  2013-07-08 11:00 
  能很好的支持代码热更新机制,是大部分选择要嵌入脚本语言的原因之一。好处很简单,脚本代码可以热更新的话,调试和线上解决问题都可以不用重启程序了,对开发效率有很大的帮助。

  今天就来谈谈Lua代码如何实现热更新。

  先简单回顾之前提过的模块和require机制。Lua内部提供了一个require函数,来实现模块的加载,它做的事情主要是以下几个:

  在registry["_LOADED"]表中判断该模块是否已经加载过了,如果是则返回,避免重复加载某个模块代码。
  依次调用注册的loader来加载模块
  将加载过的模块赋值给registry["_LOADED"]表。
  而如果要实现Lua的代码热更新,其实也就是需要重新加载某个模块,因此就要想办法让Lua认为它之前没有加载过。查看Lua代码发现,registry["_LOADED"]表,实际上对应的是package.loaded表,这在以下函数中有体现:

  627 LUALIB_API int luaopen_package (lua_State *L) {
  /....
  655   /* set field `loaded' */
  656   luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 2);
  657   lua_setfield(L, -2, "loaded");
  因此事情就很简单了,需要提供一个require_ex函数,可以理解为require的增强版,使用这个函数可以动态更新某个模块的代码,这个函数可以在我的开源项目qnode的script/util.lua中找到:

  function require_ex( _mname )
      qlog( string.format("require_ex = %s", _mname) )
       if package.loaded[_mname] then
          qlog( string.format("require_ex module[%s] reload", _mname))
      end
        package.loaded[_mname] = nil
      require( _mname )
  end
  这个函数做的事情一目了然。首先判断是否曾经加载过这个模块,如果有则打印一条日志表示需要重新加载某个模块,然后将该模块原来在表中注册的值赋空,然后再次调用require进行模块的加载和注册。

  以上了解了Lua代码热更新的原理,但是还有一些细节需要提醒一下。

  第一点,如何组织你的项目中的Lua代码?我在qnode中使用的方式是,单独使用一个叫main.lua的文件调用require_ex函数来加载需要用到的lua模块,而lua虚拟机创建之后执行的是这个文件,这样的话,当你需要热更新项目中的lua代码时,只需要重新执行这  个main.lua就行了。如何通知热更新代码呢?我在qnode中使用的信号机制,当服务器收到USR1信号时,通知所有工作进程,由工作进程来重新对main.lua进行重新加载,这样就完成了lua代码的热更新,为此我写了一个简单的脚本reload.sh,就是根据当前qnode的服务器进程ID来对其发送USR1信号量的。

  第二点,一般热更新的都是函数的实现,所以需要对全局变量做一些保护。比如当前某全局变量为100,表示某个操作已经进行了100次,它不能因为热更新重新置0,所以要对这些不能改变的全局变量做一个保护,最简单的方式就是这样:

a = a or 0 很单的原理,只有当前a这个变量没有初始值的时候才会赋值为0,而后面不管这个Lua文件被加载多少次,a都不会因为重新加载了Lua代码发生改变了。

posted on 2014-03-12 17:55  kangbry  阅读(747)  评论(0编辑  收藏  举报

导航