函数(进阶篇)

8.1 词法作用域

Lua 采用词法作用域(Lexical Scoping),也称为静态作用域。函数的上下文在定义时确定,而非调用时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local x = 10

function outer()
local x = 20

function inner()
return x -- 引用的是定义时的 x,值为 20
end

return inner
end

local fn = outer()
print(fn()) --> 20(而不是 10)

8.2 闭包(Closure)

闭包 = 函数 + 它引用的外部局部变量(upvalue)

计数器示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function make_counter()
local count = 0 -- 这个变量被闭包捕获,称为 upvalue

return function()
count = count + 1
return count
end
end

local counter1 = make_counter()
local counter2 = make_counter()

print(counter1()) --> 1
print(counter1()) --> 2
print(counter1()) --> 3

print(counter2()) --> 1(独立的计数器)
print(counter2()) --> 2

工厂函数

1
2
3
4
5
6
7
8
9
10
11
function make_power(exponent)
return function(base)
return base ^ exponent
end
end

local square = make_power(2)
local cube = make_power(3)

print(square(5)) --> 25
print(cube(5)) --> 125

闭包的实际用途:私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function create_account(initial_balance)
local balance = initial_balance or 0

return {
deposit = function(amount)
if amount > 0 then
balance = balance + amount
end
end,

withdraw = function(amount)
if amount > 0 and balance >= amount then
balance = balance - amount
return true
end
return false
end,

get_balance = function()
return balance
end
}
end

local acc = create_account(1000)
acc.deposit(500)
print(acc.get_balance()) --> 1500
acc.withdraw(200)
print(acc.get_balance()) --> 1300

-- 无法直接访问 balance
print(acc.balance) --> nil

闭包陷阱:循环变量的捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 经典陷阱
local funcs = {}
for i = 1, 5 do
table.insert(funcs, function()
return i
end)
end

print(funcs[1]()) --> 6?预期 1,实际是 6
-- 因为循环变量 i 在循环结束后变成 6

-- 解决方案:创建新的闭包来捕获当前值
local funcs2 = {}
for i = 1, 5 do
do
local current = i -- 每次迭代创建新的局部变量
table.insert(funcs2, function()
return current
end)
end
end

print(funcs2[1]()) --> 1(正确!)

8.3 非全局函数

表中的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local math_utils = {}

function math_utils.factorial(n)
if n <= 1 then return 1 end
return n * math_utils.factorial(n - 1)
end

-- 或者使用 self 引用(推荐)
function math_utils:factorial(n)
if n <= 1 then return 1 end
return n * self:factorial(n - 1) -- 用 self 而非表名
end

-- 匿名函数存储
math_utils.absolute = function(n)
return n >= 0 and n or -n
end

局部递归函数

直接递归时注意前向声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 错误写法
local factorial = function(n)
if n <= 1 then return 1 end
return n * factorial(n - 1) -- factorial 此时还是 nil
end

-- 正确写法一:先声明,再赋值
local factorial
factorial = function(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end

-- 正确写法二:使用 local function 语法(推荐)
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1)
end

相互递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 前向声明
local is_even, is_odd

is_even = function(n)
if n == 0 then return true end
return is_odd(n - 1)
end

is_odd = function(n)
if n == 0 then return false end
return is_even(n - 1)
end

print(is_even(4)) --> true
print(is_odd(4)) --> false

8.4 尾调用(Tail Call)

尾调用是函数最后一步执行的函数调用。Lua 会对尾调用进行优化——不增加调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 尾调用
function f(x)
return g(x) -- g 的返回值直接作为 f 的返回值
end

-- 不是尾调用(有额外的操作)
function f(x)
return g(x) + 1 -- 执行加法,需要保留栈帧
end

function f(x)
return g(x) or x -- 需要判断 or,不是尾调用
end

function f(x)
local y = g(x)
return y -- 返回变量,不是尾调用
end

尾调用消除递归栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 普通递归:深度一大就栈溢出
local function factorial(n)
if n <= 1 then return 1 end
return n * factorial(n - 1) -- 不是尾调用
end

-- 尾递归优化版本
local function factorial_tail(n, acc)
acc = acc or 1
if n <= 1 then return acc end
return factorial_tail(n - 1, n * acc) -- 尾调用!
end

print(factorial_tail(100)) --> 不会栈溢出

尾调用的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 必须满足:return func(args)
-- 其中 func 可以是任意返回函数的表达式

-- 合法的尾调用:
return g(x)
return (g(x)) -- 括号也可,但只取一个返回值
return tbl.func(x)

-- 不合法的:
return g(x) + 1 -- 有额外操作
return x and g(x) -- 有额外判断
return x, g(x) -- 多返回值不能优化
return g(x) or x -- or 操作不是尾调用
return g(x) .. "s" -- 连接操作

8.5 高阶函数

高阶函数是接收函数作为参数返回函数作为结果的函数。

map / filter / reduce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
-- map:对每个元素应用函数
local function map(arr, func)
local result = {}
for i, v in ipairs(arr) do
result[i] = func(v)
end
return result
end

-- filter:筛选满足条件的元素
local function filter(arr, pred)
local result = {}
for _, v in ipairs(arr) do
if pred(v) then
table.insert(result, v)
end
end
return result
end

-- reduce:归约
local function reduce(arr, func, initial)
local acc = initial
for _, v in ipairs(arr) do
acc = func(acc, v)
end
return acc
end

-- 使用示例
local numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

-- 将所有偶数乘以 2
local result = map(
filter(numbers, function(x) return x % 2 == 0 end),
function(x) return x * 2 end
)
-- result = {4, 8, 12, 16, 20}

-- 求和
local sum = reduce(numbers, function(a, b) return a + b end, 0)
print(sum) --> 55

table.sort 自定义排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local students = {
{name = "张三", score = 85},
{name = "李四", score = 92},
{name = "王五", score = 78},
{name = "赵六", score = 92},
}

-- 按分数降序,同分按名字升序
table.sort(students, function(a, b)
if a.score ~= b.score then
return a.score > b.score
end
return a.name < b.name
end)

装饰器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function with_logging(func)
return function(...)
print("调用函数,参数:", ...)
local result = func(...)
print("函数返回:", result)
return result
end
end

function with_timing(func)
return function(...)
local start = os.clock()
local result = func(...)
local elapsed = os.clock() - start
print(string.format("耗时:%.3f 秒", elapsed))
return result
end
end

-- 组合装饰器
local function fib(n)
if n <= 2 then return 1 end
return fib(n - 1) + fib(n - 2)
end

fib = with_timing(with_logging(fib))
fib(10)

8.6 本章小结

  • 词法作用域:函数上下文在定义时确定
  • 闭包:函数 + upvalue,可实现私有变量、工厂函数
  • 非全局函数:表中函数、局部递归函数
  • 尾调用return func(args) 不增加栈帧,可优化递归
  • 高阶函数:map、filter、reduce、装饰器

练习题

  1. 实现 memoize(f) 函数缓存,返回一个带记忆功能的版本(斐波那契数列)
  2. 用闭包实现一个”分页器”:create_paginator(data, page_size),返回 get_page(n) 函数
  3. 写一个 once(f) 函数,确保 f 只被调用一次,后续调用返回第一次的结果
  4. 用尾递归实现二分查找
  5. 实现 throttle(f, interval) 函数,限制函数在指定时间间隔内只执行一次
ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: