Lua闭包,Lua基础学习一

1.什么是Lua闭包

词法定界:当一个函数内嵌套另一个函数的时候,内函数可以访问外部函数的局部变量,这种特征叫做词法定界。

    table.sort(names,functin (n1,n2)
        return grades[n1]>grades[n2]
    end)
    //内部匿名函数可以访问外部函数的grades数组

第一类值: 是可以作为对象传递的,可以作为函数返回的值。

lua当中函数是一个值,他可以存在变量中,可以作为函数参数,可以作为返回值。

upvalue (外部局部变量):

function test()
        local i=0
        return function()
            i++
            ...
        end
    end
    //函数作为返回值,这里的i也叫外部局部变量,就是lua中的upvalue

闭包:通过调用含有一个内部函数加上该外部函数持有的外部局部变量(upvalue)的外部函数(就是工厂)产生的一个实例函数

闭包组成:外部函数+外部函数创建的upvalue+内部函数(闭包函数)

function test()
        local i=0
        return function()//尾调用
            i+=1
            return i
        end
    end
    c1=test()
    c2=test()//c1,c2是建立在同一个函数,同一个局部变量的不同实例上面的两个不同的闭包
            //闭包中的upvalue各自独立,调用一次test()就会产生一个新的闭包
    print(c1()) -->1
    print(c1()) -->2//重复调用时每一个调用都会记住上一次调用后的值,就是说i=1了已经
    print(c2())    -->1//闭包不同所以upvalue不同    
    print(c2()) -->2


2.为什么要有Lua闭包

闭包是被总结的一种现象。


3.如何使用Lua闭包,使用Lua闭包的不同方案比较

 3.1 闭包在迭代器中的运用

迭代器需要保留上一次调用的状态和下一次成功调用的状态,刚好可以使用闭包的机制来实现

创建迭代器:(一定要注意迭代器只是一个生成器,他自己本身不带循环)

function list_iter(t)
            local i=0
            local n=table.getn(t)
            return function()
                i=i+1
                if i<=n then return t[i] end
            end
        end
    //这里的list_iter是一个工厂,每次调用都会产生一个新的闭包该闭包内部包括了upvalue(t,i,n)
    //因此每调用一次该函数产生的闭包那么该闭包就会根据记录上一次的状态,以及返回list的下一个

使用迭代器:

while中使用:
            t={10,20,90}
            iter=list_iter(t)//调用迭代器产生一个闭包
            while true do
                local element=iter()
                if element==nil then break end
                print(element)
                end
            end
        泛型for使用:
            t={10,0,29}
            for element in list_iter(t) do//这里的list_iter()工厂函数只会被调用一次产生一个闭包函数,后面的每一次迭代都是用该闭包函数,而不是工厂函数
                print(element)
            end

4.源码实现

Lua闭包结构如下:(重点)

// Lua闭包
typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;    // 函数原型
  UpVal *upvals[1];  /* list of upvalues */   // upvalue列表
} LClosure;

通过sizeLclosure宏可获得Lua闭包的大小,其内存布局如下:

| LClosure | UpVal* | .. | UpVal* |

UpVal是对upvalue的间接引用,它的结构这样:(单个upvalue的结构)

struct UpVal {
  // 引用的值,该值可能在栈上(open),也可能是下面的TValue(close)
  TValue *v;  /* points to stack or to its own value */
  // 引用计数
  lu_mem refcount;  /* reference counter */
  union {
    // 当v指向栈上时,open有用,next指向下一个,挂在L->openupval上
    struct {  /* (when open) */
      UpVal *next;  /* linked list */
      int touched;  /* mark to avoid cycles with dead threads */
    } open;     //这个就是open参数
    // 当v指向自己时,这个值就在这里
    TValue value;  /* the value (when closed) */
  } u;
};

LClosure中记录的是UpVal指针,这说明一个UpVal可能会被多个Lua闭包引用,refcount就是这个引用计数。UpVal长成这个样子,完全是因为它要解决作用域的问题。比如下面代码:

function add (x)
    return function (y)
        return x+y
    end
end

local add2 = add(2)
print(add2(5))

 add函数调用完之后,参数x就超出作用域了,它本来在栈上,函数返回后它也会从栈中删除掉,但是add返回的函数对象还引用着这个x,这该怎么办呢?Lua是这样处理的。

UpVal有两种状态:

  • open状态 在这种情况下,其字段v指向的是栈中的值,换句话说它的外层函数还在活动中,因此那些外部的局部变量仍然活在栈上。
  • close状态 当外层函数返回时,就像上面代码(上层函数没了)那样,add2函数中的UpVal会变成关闭状态,即v字段指向自己的TValue,这样v就不依赖于外层局部变量了。

lua_State的openupval字段维护着一个open的链表,当创建一个Lua闭包时,调用luaF_findupval尝试从openupval链表中找到一个UpVal(根据函数原型的Upvaldesc信息),如果找得到就记录它并增加引用计数,如果找不到就创建一个新的UpVal,并加入openupval链表,原码如下:

// 查找栈上的uv。
UpVal *luaF_findupval (lua_State *L, StkId level) {
  UpVal **pp = &L->openupval;
  UpVal *p;
  UpVal *uv;
  lua_assert(isintwups(L) || L->openupval == NULL);
  // 查找open的uv, open的uv由L->openupval串起来一个链表
  while (*pp != NULL && (p = *pp)->v >= level) {
    lua_assert(upisopen(p));
    if (p->v == level)  /* found a corresponding upvalue? */
      return p;  /* return it */
    pp = &p->u.open.next;
  }
  /* not found: create a new upvalue */
  // 如果未找到,创建一个新的加入链表
  uv = luaM_new(L, UpVal);
  uv->refcount = 0;
  uv->u.open.next = *pp;  /* link it to list of open upvalues */
  uv->u.open.touched = 1;
  *pp = uv;
  uv->v = level;  /* current value lives in the stack */
  if (!isintwups(L)) {  /* thread not in list of threads with upvalues? */
    L->twups = G(L)->twups;  /* link it to the list */
    G(L)->twups = L;
  }
  return uv;
}

比如下面这段Lua代码:

local x = 1
local y = 2
local z = 3

local function f1()
    return x + 1
end

local function f2()
    return x + 2
end

执行到f1声明时,创建一个Lua闭包,并创建一个UpVal挂到openupval链表上,接着执行到f2声明,此时从openupval可以到过UpVal,就直接引用它。

外层函数执行完毕的时候,会调用luaF_close将openupval中的一些UpVal关闭,代码如下:

vmcase(OP_RETURN) {
    int b = GETARG_B(i);
    if (cl->p->sizep > 0) luaF_close(L, base);
...

// 关闭栈中的upvalues,从level往后的upvalue,如果引用计数为0释放之,否则拷贝到UpVal自己身上
void luaF_close (lua_State *L, StkId level) {
  UpVal *uv;
  while (L->openupval != NULL && (uv = L->openupval)->v >= level) {
    lua_assert(upisopen(uv));
    L->openupval = uv->u.open.next;  /* remove from 'open' list */
    if (uv->refcount == 0)  /* no references? */
      luaM_free(L, uv);  /* free upvalue */
    else {
      setobj(L, &uv->u.value, uv->v);  /* move value to upvalue slot */
      uv->v = &uv->u.value;  /* now current value lives here */
      luaC_upvalbarrier(L, uv);
    }
  }
}

luaF_close还会在其他地方执行,只要任何情况下留在栈中的局部变量被删除出栈,就会调这个函数。调完之后,UpVal本身就把局变量的值保存在自己身上了,这个过程对于函数是透明的,因为它总是间接的引用upvalue。

下图表示open和close的UpVal状态:

 opending vars就是保存到L->openupval的链表

5.Lua闭包关键点,重要节点,疑难杂症场景

5.1.函数原型

每个Lua函数都有一个原型,这是一个由GC管理的对象,它挂靠在函数上,为函数提供必要的信息,比如这个函数的操作码(opcodes),常量信息,本地变量信息,upvalue信息,和调试信息等等。

因为Lua函数中可以内嵌函数,所以原型对象里面也有一个内嵌原型的列表,由此形成一个函数原型的树。

原型结构是这样的:

typedef struct Proto {
  CommonHeader;
  // 固定参数的数量
  lu_byte numparams;  /* number of fixed parameters */
  // 是否有可变参数
  lu_byte is_vararg;
  // 该函数需要的栈大小
  lu_byte maxstacksize;  /* number of registers needed by this function */
  // upvalues数量
  int sizeupvalues;  /* size of 'upvalues' */
  // 常量数量
  int sizek;  /* size of 'k' */
  // 指令数量
  int sizecode;
  // 行信息数量
  int sizelineinfo;
  // 内嵌原型数量
  int sizep;  /* size of 'p' */
  // 本地变量的数量
  int sizelocvars;
  // 函数进入的行
  int linedefined;  /* debug information  */
  // 函数返回的行
  int lastlinedefined;  /* debug information  */
  // 常量数量
  TValue *k;  /* constants used by the function */
  // 指令数组
  Instruction *code;  /* opcodes */
  // 内嵌函数原型
  struct Proto **p;  /* functions defined inside the function */
  // 行信息
  int *lineinfo;  /* map from opcodes to source lines (debug information) */
  // 本地变量信息
  LocVar *locvars;  /* information about local variables (debug information) */
  // Upvalue信息
  Upvaldesc *upvalues;  /* upvalue information */
  // 使用该原型创建的最后闭包(缓存)
  struct LClosure *cache;  /* last-created closure with this prototype */
  // 源代码文件
  TString  *source;  /* used for debug information */
  // 灰对象列表,最后由g->gray串连起来
  GCObject *gclist;
} Proto;

5.2.常量

函数中的常量就是那些字面量,比如下面代码:

local function fun()
    local x = 1
    local s = "ok"
    local b = true
end

1, ok true这些就是常量,Lua把所有的值都统一为TValue,常量也不例外,由TValue *k;保存。

而且常量只能是数字,布尔值,字符串,和nil这些基本类型,其他GC对象不可以是常量。由于常量不可变(和string一样,是不可变的,改变他们就是创建新的),所以直接保存在原型对象上就可以了。

5.3 局部变量

函数中的固定参数,可变参数,和本地变量,都是局部变量,这些变量都存在函数关联的栈中,而栈中的元素就称为“寄存器”,maxstacksize指定该函数需要多少个寄存器,在创建Lua函数时就会在栈上预留这么多空间。因为可变参数的实际数量只有调用者才知道,所以maxstacksize不包含可变参数的数量。

locvars(local vars)是一个局部变量的信息结构,主要用于调试的:

//  本地变量的信息
typedef struct LocVar {
  // 本地变量名
  TString *varname;   
  int startpc;  /* first point where variable is active */
  int endpc;    /* first point where variable is dead */
} LocVar;

5.4 子函数原型 

struct Proto **p保存着内嵌函数的原型列表,比如下面的代码:

function func()
    local function sf1()
    end
    local function sf2()
    end
end

sf1和sf2就是内嵌函数,所以func的函数原型就有两个子原型。 

5.5 upvalue

upvalue其实就是外部函数的局部变量,upvalues是这些upvalue的信息列表,Upvaldesc结构如下:

typedef struct Upvaldesc {
  // 名字
  TString *name;  /* upvalue name (for debug information) */
  lu_byte instack;  /* whether it is in stack (register) */
  lu_byte idx;  /* index of upvalue (in stack or in outer function's list) */
} Upvaldesc;

instack指明这个upvalue会存在哪里,有两种情况要考虑:

  • upvalue如果是上一层函数的局部变量,且这个上层函数还在活动中,那么该局部变量一定还在上层函数的栈中。此时,instack为1,表明它在栈中,idx指定在栈中的索引,相对于上层函数的栈基址。
  • uv如果是上一层函数之外的局部变量,就像下面代码这样:
local x = 1
local function func()
    local function innerfunc()
        return x + 1
    end
end

x在上两层函数之外声明,Lua是这样解决这个问题的:首先func会把x当成upvalue记录下来,然后innerfunc再从func的upvalue数组寻找。所以这种情况下,instack为0,则idx表示上层函数uv列表的索引。

实际的upvalue引用是在函数对象中的,这里只是一个描述信息,函数对象要根据这个信息才能引用到upvalue。

5.6 C闭包

Lua在执行到fucntion ... end表达式时,会创建一个函数对象,其结构如下:

typedef union Closure {
  CClosure c;
  LClosure l;
} Closure;

正好对应了C闭包和Lua闭包,C闭包结构如下:

// nupvalues upvalue数量
// gclist为灰对象列表,最后由g->gray串连起来
#define ClosureHeader \
    CommonHeader; lu_byte nupvalues; GCObject *gclist
// C闭包
typedef struct CClosure {
  ClosureHeader;       //闭包头
  lua_CFunction f;    // C函数指针
  TValue upvalue[1];  /* list of upvalues */    // update数组
} CClosure;

 因为C函数相应简单,没有外层函数,所以upvalue其实就是保存在CClosure中的一个TValue数组。一个CClosure的实际大小通过sizeLclosure计算出,其内存布局如下:

| CClosure | TValue[0] | .. | TValue[nupvalues-1] |

因为CClosure的upvalue数组包含了一个元素,所以后面跟着的长度为nupvalues-1。通过luaF_newCclosure生成一个新的C闭包,实际应用中一般用lua_pushcclosure向栈顶压入一个新的C闭包,同时栈顶要装备好upvalue。函数实现如下: 

// 生成一个C闭包并压入栈顶, n表示当前栈顶有多少个upvalue要与闭包关联
LUA_API void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n) {
  lua_lock(L);
  if (n == 0) {
    // 没有upvalue,它是轻量级C函数
    setfvalue(L->top, fn);
    api_incr_top(L);
  }
  else {
    // 有upvalue,它是一个C闭包
    CClosure *cl;
    api_checknelems(L, n);
    api_check(L, n <= MAXUPVAL, "upvalue index too large");
    // 新建C闭包
    cl = luaF_newCclosure(L, n);
    cl->f = fn;
    L->top -= n;
    // 保存upvalue
    while (n--) {
      setobj2n(L, &cl->upvalue[n], L->top + n);
      /* does not need barrier because closure is white */
    }
    setclCvalue(L, L->top, cl);
    api_incr_top(L);
    luaC_checkGC(L);
  }
  lua_unlock(L);
}

 

6.引用

Lua的闭包详解(终于搞懂了) - 风雨缠舟 - 博客园

深入Lua:函数和闭包 - 知乎

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值