Lua闭包和上值深入解析

深入理解Lua闭包和上值(upvalue)机制,掌握函数式编程范式、状态保持和高级抽象技巧。

闭包的本质

在 Lua 中,函数是第一类值(first-class value)。当一个函数引用了其外部作用域的局部变量时,就形成了一个闭包(closure)。闭包可以访问并修改这些外部变量(称为上值/upvalue),即使外部函数已经返回。

function counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local c = counter()
print(c())  -- 1
print(c())  -- 2
print(c())  -- 3

这里 count 是闭包的上值。每次调用 counter() 都会创建一个新的闭包实例,拥有独立的 count 变量。

上值的工作原理

上值是闭包捕获的外部局部变量。Lua 通过特殊的机制来管理上值:

function make_adder(x)
    -- x 是这个闭包的上值
    return function(y)
        return x + y
    end
end

local add5 = make_adder(5)
local add10 = make_adder(10)

print(add5(3))   -- 8  (5 + 3)
print(add10(3))  -- 13 (10 + 3)

当外部函数返回后,局部变量本应被销毁。但如果闭包引用了它,Lua 会将该变量从栈上迁移到堆上(这个过程称为 “closing”),使闭包可以继续访问它。

闭包共享上值

多个闭包可以共享同一个上值,这是实现私有状态的关键:

function make_account(balance)
    local function deposit(amount)
        balance = balance + amount
        return balance
    end

    local function withdraw(amount)
        if amount > balance then
            error("余额不足")
        end
        balance = balance - amount
        return balance
    end

    local function get_balance()
        return balance
    end

    return {
        deposit = deposit,
        withdraw = withdraw,
        balance = get_balance
    }
end

local account = make_account(1000)
account.deposit(500)
print(account.balance())   -- 1500
account.withdraw(200)
print(account.balance())   -- 1300

三个闭包(depositwithdrawget_balance)共享同一个 balance 上值,实现了数据封装。

闭包作为迭代器

Lua 标准库中的 ipairspairs 都是闭包的典型应用:

-- 自定义迭代器:range
function range(start, stop, step)
    step = step or 1
    local i = start - step
    return function()
        i = i + step
        if i <= stop then
            return i
        end
    end
end

for v in range(1, 10, 2) do
    print(v)  -- 1, 3, 5, 7, 9
end

-- 自定义迭代器:zip
function zip(t1, t2)
    local i = 0
    return function()
        i = i + 1
        if t1[i] and t2[i] then
            return t1[i], t2[i]
        end
    end
end

for a, b in zip({1,2,3}, {"a","b","c"}) do
    print(a, b)  -- 1 a, 2 b, 3 c
end

高阶函数

闭包使 Lua 可以实现常见的高阶函数模式:

map / filter / reduce

function map(t, func)
    local result = {}
    for i, v in ipairs(t) do
        result[i] = func(v)
    end
    return result
end

function filter(t, func)
    local result = {}
    for _, v in ipairs(t) do
        if func(v) then
            result[#result + 1] = v
        end
    end
    return result
end

function reduce(t, func, init)
    local acc = init
    for _, v in ipairs(t) do
        acc = func(acc, v)
    end
    return acc
end

local numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

-- 链式调用
local result = reduce(
    filter(
        map(numbers, function(x) return x * x end),
        function(x) return x % 2 == 0 end
    ),
    function(acc, x) return acc + x end,
    0
)
print(result)  -- 220 (4+16+36+64+100)

函数组合

function compose(...)
    local funcs = {...}
    return function(...)
        local result = {...}
        for i = #funcs, 1, -1 do
            result = {funcs[i](table.unpack(result))}
        end
        return table.unpack(result)
    end
end

local double = function(x) return x * 2 end
local add_one = function(x) return x + 1 end
local square = function(x) return x * x end

local transform = compose(square, add_one, double)
print(transform(3))  -- (3*2 + 1)^2 = 49

柯里化

function curry(func)
    return function(a)
        return function(b)
            return func(a, b)
        end
    end
end

local add = curry(function(a, b) return a + b end)
local add5 = add(5)
print(add5(3))   -- 8
print(add5(10))  -- 15

闭包实现记忆化

闭包是实现函数记忆化(memoization)的最佳方式:

function memoize(func)
    local cache = {}
    return function(...)
        local key = table.concat({...}, ",")
        if cache[key] == nil then
            cache[key] = func(...)
        end
        return cache[key]
    end
end

-- 记忆化斐波那契
local fib = memoize(function(n)
    if n <= 1 then return n end
    return fib(n - 1) + fib(n - 2)
end)

print(fib(50))  -- 12586269025 (瞬间返回)

闭包与面向对象

闭包可以替代元表实现面向对象编程,提供更强的封装性:

function new_person(name, age)
    -- 私有属性
    local self = {
        name = name,
        age = age,
        logs = {}
    }

    -- 私有方法
    local function log(msg)
        table.insert(self.logs, os.date() .. ": " .. msg)
    end

    -- 公开接口
    return {
        get_name = function() return self.name end,
        get_age = function() return self.age end,
        set_age = function(new_age)
            log(string.format("年龄从 %d 改为 %d", self.age, new_age))
            self.age = new_age
        end,
        greet = function()
            return string.format("你好,我是 %s,今年 %d 岁", self.name, self.age)
        end,
        get_logs = function()
            return self.logs
        end
    }
end

local person = new_person("张三", 25)
print(person.greet())         -- 你好,我是 张三,今年 25 岁
person.set_age(26)
print(person.get_logs()[1])   -- 包含年龄修改日志
-- person.name 无法直接访问,实现了真正的私有

延迟求值和惰性序列

闭包可以实现延迟求值,构建惰性序列:

function lazy_seq(gen, init)
    local state = init
    local computed = false
    local value

    return {
        head = function()
            if not computed then
                value, state = gen(state)
                computed = true
            end
            return value
        end,
        tail = function()
            if not computed then
                value, state = gen(state)
                computed = true
            end
            return lazy_seq(gen, state)
        end
    }
end

-- 惰性自然数序列
local function naturals_gen(n)
    return n, n + 1
end

local naturals = lazy_seq(naturals_gen, 1)
print(naturals.head())              -- 1
print(naturals.tail().head())       -- 2
print(naturals.tail().tail().head()) -- 3

使用 debug 库查看上值

Lua 的 debug 库提供了检查和修改闭包上值的能力:

function make_counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local c = make_counter()
c()
c()
c()

-- 查看上值
local name, value = debug.getupvalue(c, 1)
print(name, value)  -- count  3

-- 修改上值
debug.setupvalue(c, 1, 100)
print(c())  -- 101

-- 获取闭包的上值数量
local info = debug.getinfo(c)
print(info.nups)  -- 1

注意事项

闭包的使用需要注意以下方面:

  • 每个闭包实例都有独立的上值副本,不会互相干扰
  • 闭包会持有上值的引用,可能影响垃圾回收,注意避免循环引用
  • 在循环中创建闭包时,确保上值被正确捕获而非共享意外变量
  • debug.getupvaluedebug.setupvalue 可用于调试,但不应用于生产代码
  • Lua 5.2+ 中 <close> 变量提供了一种资源管理替代方案
  • 大量闭包创建时注意内存占用,特别是在热路径上

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页