Lua语言程序设计3:闭包、模式匹配、日期和时间
文章目录
- 第九章:闭包
- 9.1 函数是第一类值
- 9.1.1 第一类值的特性
- 9.1.2 函数是匿名的
- 9.1.3 高阶函数
- 9.2 非全局函数
- 9.2.1 表字段中的函数
- 9.2.2 局部函数
- 9.2.3 递归局部函数的特殊处理
- 9.3 词法作用域(Lexical Scoping)
- 9.3.1 词法作用域
- 9.3.2 闭包(Closure)
- 9.3.1 闭包的概念
- 9.3.2 函数重写
- 9.3.3 创建安全沙盒(Sandbox)
- 9.3.4 回调函数(Callback)
- 9.3.5 总结
- 9.4 函数式编程
- 十、模式匹配
- 10.1 模式匹配函数
- 10.2 模式语法
- 10.2.1 字符分类
- 10.2.2自定义字符集
- 10.2.2 魔法字符、连字符、脱字符
- 10.2.3 重复/可选修饰符
- 10.2.4 锚点:`^` 匹配开头,`$` 匹配结尾
- 10.2.5 平衡匹配
- 10.2.6 边界匹配
- 10.3 捕获
- 10.3.1 基本捕获语法
- 10.3.2 引用捕获(略)
- 10.3.3 空捕获
- 10.4 替换
- 10.4.1 字符串替换
- 10.4.2 表替换
- 10.4.3 函数替换
- 10.4.4 嵌套命令处理
- 10.4.5 URL编码解码(略)
- 10.4.6 制表符展开(略)
- 10.5 技巧(略)
- 十一、统计文本中的高频词
- 11.1 核心思路与数据结构
- 11.2 实现步骤详解
- 11.3 程序运行示例
- 11.4 练习与扩展
- 十二、日期与时间
- 12.1 os.time
- 12.2 函数 os.date
- 12.3 日期时间处理
- 12.3.1 **os.time 的归一化特性**
- 12.3.2 os.difftime
第九章:闭包
在Lua语言中,函数是严格遵循词法定界(lexical scoping
)的第一类值(first-class value
)。“词法定界”意味着Lua语言中的函数可以访问包含其自身的外部函数中的变量(这意味着Lua语言完全支持Lambda 演算)。“第一类值”意味着Lua语言中的函数与其他常见类型的值(例如数值和字符串)具有同等权限:
- 函数可以被保存到变量中(全局或局部)或表中
- 可以将某个函数作为参数传递给其他函数,也可以作为其他函数的返回值返回。
上述两个特性联合起来为Lua语言带来了极大的灵活性,允许我们在Lua语言中使很多函数式语言(functional-language)的强大编程技巧。
9.1 函数是第一类值
9.1.1 第一类值的特性
a = {p = print} --> 创建一个表 a,其字段 p存储的是全局函数_G.print
a.p("Hello World") --> 调用原始的print函数
print = math.sin --> 将print变量指向math.sin函数,即_G.print → _G.math.sin
a.p(print(1)) --> print(1)计算1的正弦值,a.p调用原始的print函数输出这个值
math.sin = a.p -- 将 math.sin赋值为 a.p(_G.math.sin → a.p → 最初的 _G.print)
math.sin(10,20) --> 现在 math.sin指向的是 print,结果是10 20
-
第一类值:Lua 的函数是第一类值,可以像普通变量一样被赋值、传递和修改。 例如:
a = {p = print}
把print
函数存储到表a
中。 -
动态绑定:Lua 的函数调用是动态查找的,函数名可以随时指向不同的函数。例如:
-
print = math.sin
让print
指向sin
函数。 -
math.sin = a.p
让math.sin
指向print
函数。
-
-
引用机制: Lua 的函数变量存储的是引用,而不是函数本身。
-
a.p
存储的是print
的引用,即使print
被修改,a.p
仍然指向原来的print
函数。 -
但如果修改
a.p
本身(如a.p = math.cos
),那它就会指向新的函数。
-
-
表(table)作为命名空间 :Lua 没有真正的模块系统,但可以用表来模拟命名空间。例如
a = {p = print}
相当于创建了一个“模块”a
,其中p
是print
的别名。
9.1.2 函数是匿名的
如果函数也是值的话,那么是否有创建函数的表达式呢?答案是肯定的。Lua语言中常见的函数定义方式如下:
-- 1. 常规写法
function foo(x) return 2*x end
这其实是另一种方式的语法糖:
-- 2. 赋值语句
foo = function(x) return 2*x end
赋值语句右侧的表达式就是函数构造器,类似于表构造器{}
,因此,函数定义实际上就是创建类型为"function"的值并把它赋值给一个变量的语句。
在 Lua 中,函数本质上是匿名值,它们没有固有的名称,通常通过变量来存储和引用。当讨论函数名时(例如print
),实际上指的是保存该函数的变量名,从而看似给函数起了一个名字,但在很多场景下,仍然会保留函数的匿名性,比如table.sort
函数,该函数以一个表为参数并对其中的元素排序 。
这种函数必须支持各种各样的排序方式 : 升序或降序 、 按数值按字母排序等 。sort函数 并没有试图穷尽所有的排序方式,而是提供了一个可选的参数,也就是所谓的排序函数——接收两个参数并根据第一个元素是否应排在第二个元素之前返回不同的值。 假设有一个如下所示的表:
network = {{name = "grauna", IP = "210.26.30.34"},{name = "arraial", IP = "210.26.30.23"},{name = "lua", IP = "210.26.23.12"},{name = "derain", IP = "210.26.23.20"},
}
如果想针对 name 宇段 、按字母顺序逆序对这个表排序 ,只需使用如下语句:
table.sort(network, function (a,b) return (a.name > b.name) end)
这里的排序规则(一个接收两个参数 a和 b并返回比较结果的函数)是直接作为表达式(一个匿名函数) 传递给 table.sort
的。它没有预先赋给一个变量,也没有特定的名称。匿名函数的优势在于:
- 即用即定义:无需为了一个仅在排序时使用的逻辑专门去定义一个命名函数,使代码更紧凑,逻辑更清晰;
- 灵活性高:排序规则可以非常方便地根据具体需求现场改变。例如,如果想按 IP字段排序,只需修改匿名函数内部的比较逻辑即可,无需影响其他代码。
匿名函数的用途不仅限于此,它还常见于以下情况:
-
作为回调函数 (Callback):在事件驱动编程中,匿名函数非常适合作为一次性或特定情境下的回调处理程序。例如,为某个按钮的点击事件临时定义一个行为,或者响应一个网络请求的返回。
-
实现闭包 (Closure):匿名函数经常用于创建闭包,捕获并“记住”其定义时的上下文环境。这在需要维持状态的场景中非常有用,例如生成计数器。
-
作为函数返回值:**高阶函数(Higher-order function)**可以将匿名函数作为返回值,这使得我们可以动态地生成行为各异的函数。
对于复杂或需要复用的逻辑,定义一个命名函数通常是更好的选择,这能提高代码的可读性和可维护性。过度使用匿名函数,尤其是在嵌套较深时,可能会让代码变得难以调试
9.1.3 高阶函数
像sort函数 这样以另一个函数为参数的函数,我们称之为高阶函数,利用匿名函数作为参数正是其灵活性的主要来源 。让我们再来实现一个常见的高阶函数——导数。根据通常的定义,函数 f
的导数 f'
定义为:
f′(x)=(f(x+d)−f(x))/df'(x) = (f(x + d) - f(x)) / df′(x)=(f(x+d)−f(x))/d
其中 d
是一个无限小的值。根据这个定义,我们可以计算导数的近似值。
function derivative(f, delta)delta = delta or 1e-4 -- 默认使用 0.0001 作为 delta 值return function (x)return (f(x + delta) - f(x)) / deltaend
end
-- 计算正弦函数的导数
c = derivative(math.sin)-- 比较数学库的余弦函数与我们计算的导数近似值
print(math.cos(5.2), c(5.2))
--> 0.46851667130038 0.46856084325086print(math.cos(10), c(10))
--> -0.83907152907645 -0.83904432662041
此实现为导数计算的近似方法,精度取决于 delta 的大小。较小的 delta 值会提高精度,但可能增加数值计算的不稳定性。默认使用1e-4 (0.0001) 作为 delta 值,这是一个常用的折中选择。
9.2 非全局函数
函数作为第一类值,不仅可以存储在全局变量中,还可以存储在表字段和局部变量中。大部分 Lua 语言的库就采用了这种将函数存储在表字段中的机制(例如 io.read 和 math.sin ),这种机制是 Lua 面向对象编程(21章)的关键组成部分。
9.2.1 表字段中的函数
结合以前的知识,要创建表字段中的函数,有几种方式:
-
直接赋值
Lib = {} Lib.foo = function (x, y) return x + y end Lib.goo = function (x, y) return x - y end
-
使用构造函数
Lib = {foo = function (x, y) return x + y end,goo = function (x, y) return x - y end }
-
专用语法(推荐)
Lib = {} function Lib.foo(x, y) return x + y end function Lib.goo(x, y) return x - y end
使用示例:
print(Lib.foo(2, 3), Lib.goo(2, 3)) --> 5 -1
9.2.2 局部函数
当函数存储在局部变量中时,称为局部函数,其作用域受限于给定的范围,而不是全局可见。对于局部函数,Lua 语言提供了一种语法糖的写法(推荐):
local function f(params) -- 函数体body
end
一般写法是:
local f -- 先声明局部变量
f = function(params) -- 再赋值函数-- 函数体body
end
在 Lua 中,一个代码块可以是:
-
一个完整的 Lua 文件
-
一段在交互模式中输入的代码
-
一个被 load 函数加载的字符串
Lua 将每个代码块都当作一个函数来处理,所以在一段程序中声明的函数就是局部函数,这些局部函数只在该程序段中可见 (程序段中的其他函数可以使用这些局部函数),这一特性对于包( package )而言尤其有用。
- 示例 1:基本用法
-- 这是一个代码块(比如一个.lua文件)
local function add(x, y) -- 局部函数,只在当前文件可见return x + y
endlocal function multiply(x, y) -- 另一个局部函数return x * y
end-- 这两个函数可以互相调用,因为它们在同一个代码块中
local function calculate(a, b)return add(a, b) + multiply(a, b)
endprint(calculate(2, 3)) -- 输出: 11 (2+3 + 2*3 = 5 + 6 = 11)
- 示例 2:在不同文件中
-- 文件1: math_utils.lua
local function internal_helper(x) -- 这个函数只在当前文件可见return x * 2
endfunction public_function(x) -- 全局函数,其他文件可以访问return internal_helper(x) + 1
end
-- 文件2: main.lua
require "math_utils"print(public_function(5)) -- 输出: 11 (5*2 + 1 = 11)
-- print(internal_helper(5)) -- 错误!这个函数在math_utils.lua外不可见
这样设计的优点是:
- 封装:隐藏内部实现细节,只暴露必要的接口
- 安全:外部代码无法直接调用内部函数
- 模块化:便于代码的组织和维护
- 避免命名冲突:局部函数不会污染全局命名空间
面向对象编程的三要素是封装、继承和多态,在Python和Lua中 的实现方式不同:
特性 | Python | Lua |
---|---|---|
封装 | 通过命名约定和名称修饰实现封装(_ 和__ 约定) | 通过词法作用域实现封装(局部变量/函数) |
继承 | 类继承 | 元表和原型 |
多态 | 方法重写 | 函数和表的多态 |
核心机制 | 类和对象 | 表和元表 |
9.2.3 递归局部函数的特殊处理
直接定义递归局部函数时会出现问题:
local fact = function(n) -- 错误的方式if n == 0 then return 1else return n * fact(n - 1) -- 错误:此时局部变量 fact 尚未定义end
end
当 Lua 语言编译函数体中的 fact(n-1 ) 调用时,局部的 fact 尚未定义 。 因此,这个表达式会尝试调用全局的 fact 而非局部的 fact 。 我们可以通过先定义局部变量再定义函数的方式来解决这个问题 :
-- 1. 先声明后定义
local fact -- 先声明局部变量
fact = function(n) -- 再定义函数if n == 0 then return 1else return n * fact(n - 1) -- 正确:引用局部变量 factend
end
如果使用局部函数的语法糖定义,Lua 会自动展开为正确的形式:
local function foo (params) body end
--自动展开为:
local foo; foo = function (params) body end
所以上式可写成:
-- 2. 使用函数语法糖定义(推荐)
local function fact(n) -- Lua 会自动展开为if n == 0 then return 1else return n * fact(n - 1)end
end
但是对于间接递归函数,需要显式的前向声明:
local f -- 前向声明local function g()-- 某些代码f() -- 调用 f-- 某些代码
endfunction f() -- 注意:不要加 local 关键字-- 某些代码g() -- 调用 g-- 某些代码
end
重要提醒:在定义
f
时不要使用local
关键字,否则会创建新的局部变量,导致g
中引用的f
变为未定义状态。
函数类型 | 定义方式 | 作用域 | 适用场景 |
---|---|---|---|
全局函数 | function foo() end | 全局可见 | 通用工具函数 |
表字段函数 | function Lib.foo() end | 通过表访问 | 面向对象编程、库函数 |
局部函数 | local function foo() end | 局部作用域 | 模块内部、私有函数 |
9.3 词法作用域(Lexical Scoping)
当函数 B
包含函数 A
时,内部函数 A
可以访问外部函数 B
的所有局部变量,我们将这种特性称为词法作用域( lexical scoping ) 。 词法作用域外加嵌套的第一类值函数可以为编程语言提供强大的功能,很多编程语言并不支持将这两者组合使用 。
9.3.1 词法作用域
排序示例:基于分数对学生姓名排序,分数高者在前
names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}-- 匿名函数访问外部变量 grades
table.sort(names, function (n1, n2)return grades[n1] > grades[n2] -- 比较成绩
end)
如果要将其封装魏函数,则可写作:
function sortbygrade(names, grades)table.sort(names, function (n1, n2)return grades[n1] > grades[n2] -- 内部匿名函数可访问外部函数sortbygrade的参数 gradesend)
end
在这个例子中,grades 既不是全局变量也不是局部变量,而是我们所说的非局部变量(non-local variable) (由于历史原因,在 Lua语言中非局部变量也被称为上值)。
9.3.2 闭包(Closure)
9.3.1 闭包的概念
闭包是一个函数加上该函数能访问的所有非局部变量(non-local variables) 的组合。下面是一个典型的示例:
function newCounter()local count = 0return function () -- 返回一个匿名函数(闭包)count = count + 1return countend
endc1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
在 newCounter 中,count
是一个 局部变量,理论上newCounter()
执行完毕后,count
变量应该被销毁。但是通过词法作用域,内部函数(此处是返回的匿名函数)还在引用 count
变量,这些变量对于闭包来说是上值(upvalue)。因此 ,Lua 会保留这个变量而不是立即销毁,count 的生命周期被延长到与闭包相同。
--普通变量生命周期:
[newCounter开始] --- count存在 --- [newCounter结束] → count被销毁--闭包变量生命周期:
[newCounter开始] --- count存在 --- [newCounter结束] --- count继续存在 --- [c1被垃圾回收] → count被销毁
如果没有生命周期延长,那么闭包试图访问已销毁的变量, 程序就会崩溃或出错。这就是闭包强大之处——它打破了变量生命周期的常规规则,让函数能够"记住"自己的创建环境!
步骤分解:
c1 = newCounter()
:- 在
newCounter()
内部创建局部变量count = 0
; - 创建匿名函数,这个函数捕获了外部的
count
变量 newCounter()
返回这个匿名函数(现在是一个闭包)并赋值给c1
- 在
print(c1())
:第一次调用c1()
,此时闭包内部的count
值变为1
print(c1())
:再次调用同一个闭包,由于闭包访问的是同一个count
变量,所以值再次+1
。
这里面的关键在于闭包捕获变量,每次调用闭包时,操作的是同一个 count
变量(状态保持)。被捕获的变量 count
的生命周期与闭包相同,只有当所有引用这个上值的闭包都被销毁后,count
才会被回收。
c1 = newCounter() -- count创建,生命周期开始
print(c1()) --> 1 (count还存在!)
print(c1()) --> 2 (count依然存在!)c1 = nil -- 闭包被丢弃,没有引用指向count了
-- 下次垃圾回收时,count才会被真正销毁
每次调用 newCounter
都会创建一个新的闭包:
-- 第二次调用 newCounter() 时,又创建了一个新的 count 变量和新的闭包(赋值给 c2)
-- c1 和 c2 是两个独立的闭包,它们各自拥有自己的 count 实例。c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
在 Lua 中,所有的函数本质上都是闭包,只是有些函数没有捕获任何外部变量(即没有“上值”),这种情况下它们就是“纯净”的函数(也称为 “原型函数”)。但为了简单起见,我们通常还是称它们为“函数”,除非需要强调闭包的特性。
总结:
- 在 Lua 中,函数就是闭包,只是有些闭包没有捕获外部变量。
- 闭包允许函数“记住”并访问其定义时的环境(即使那个环境已经不再活跃)。
- 每次调用外部函数(如
newCounter
)都会创建一个新的闭包和新的上值(如count
),彼此独立。
另外,Lua允许直接在return语句中定义匿名函数(函数是第一类值), 这是Lua中非常常见和优雅的写法。也可以写成其它形式:
-- 先定义再返回
function newCounter()local count = 0local function increment() -- 先给函数命名count = count + 1return countendreturn increment -- 再返回这个函数
end
-- 赋值给局部变量
function newCounter()local count = 0local increment = function () -- 赋值给变量count = count + 1return countendreturn increment
end
这三种写法在功能上完全等价,但第一种写法(直接在return中定义)是最简洁和常见的。
9.3.2 函数重写
通过捕获原函数,在不修改源码的情况下增加新功能(日志、验证、转换等)。这是面向切面编程(AOP) 的一种简单实现。比如下面的代码重新定义 math.sin
,使其接收角度而非弧度。
dolocal oldSin = math.sin -- 捕获原函数local k = math.pi / 180 -- 捕获转换系数math.sin = function (x) -- 新函数(闭包)开始return oldSin(x * k) -- 这里访问了外部的 oldSin 和 kend -- 新函数(闭包)结束
end
-- 此时,局部变量 oldSin 和 k 理论上已超出作用域, 但它们被 math.sin 这个闭包所引用,所以不会被销毁。
- 信息隐藏:使用
do...end
代码块将oldSin
和k
定义为局部变量,实现了函数封装,外部代码无法直接访问到原始的 oldSin,防止了意外修改或绕过你的新函数。 - 避免命名冲突:临时变量不会污染全局命名空间
现在,任何代码调用
math.sin(90)
,都会先转换成弧度(90 * (π/180) = π/2)
,然后调用原始的math.sin
函数计算sin(π/2)
,结果是 1。
9.3.3 创建安全沙盒(Sandbox)
闭包可用于构建安全的运行时环境,限制未受信任代码的访问系统资源(如文件、网络、全局变量)保证主机系统的安全。下面的示例通过重定义 io.open
来限制文件访问。
dolocal oldOpen = io.open -- 捕获原始的文件打开函数 local access_OK = function(filename, mode) -- 定义一个权限检查函数(这里只是示意,实际更复杂) return filename:sub(1,5) == "/tmp/" -- 例如:只允许读取 /tmp/ 目录下的文件endio.open = function(filename, mode) -- 重写全局的 io.open 函数if access_OK(filename, mode) then return oldOpen(filename, mode) -- 权限通过,调用原始函数完成实际操作else return nil, "access denied" -- 权限拒绝,返回错误endend
end
原始的、危险的 oldOpen
被隐藏在了闭包的私有上下文中。沙盒内运行的不可信代码只能接触到被重写后的、安全的 io.open
(文件操作都会先经过权限检查access_OK
),而不能直接调用到 oldOpen
( oldOpen
是一个外部无法访问的局部变量。)
你可以在重写函数里做任何你想做的控制:记录日志、限制访问频率、模拟文件系统(比如让程序认为它在某个虚拟目录下)等。Lua没有提供一个大而全的安全模型,而是通过闭包这个简单的特性,灵活地自行解决这些问题。这充分体现了Lua语言设计的简洁性和强大性。
9.3.4 回调函数(Callback)
在事件驱动编程中,尤其是GUI开发中,你经常需要预先定义一些函数(回调函数),这些函数会在未来的某个时间点被调用(如按钮点击、定时器到期、网络请求返回)。
当回调函数被调用时,创建它的那个环境可能早已不存在。如何让回调函数记住它被创建时的上下文信息?答案就是闭包。比如创建一个数字按钮的回调:
function digitButton (digit) -- 参数 digit 是局部变量return Button{label = tostring(digit),action = function () -- 这个匿名函数就是闭包!add_to_display(digit) -- 它记住了外部的变量 digitend -- 即使 digitButton 函数返回,这个闭包依然持有对 digit 的引用}
end-- 创建按钮0到9
for i = 0, 9 dobutton = digitButton(i) -- 每次循环调用 digitButton(i):-- 1. 创建一个新的闭包,该闭包捕获当前的 i 值。-- 2. 将这个闭包设置为按钮的回调。
end
如果不用闭包,你可能需要将数字 i
存在某个全局变量或按钮对象的某个属性里,然后在回调函数中去查找。这样做:
-
丑陋且繁琐:增加了不必要的代码。
-
容易出错:如果全局变量被意外修改,回调行为就会出错。
-
无法并发:如果两个按钮共用一个全局变量,会产生竞争条件。
闭包的优势:
-
简洁自然:每个回调函数自动且独立地“记住”了它所需的数据(digit)。
-
数据封装:digit 变量被安全地封装在闭包内部,不会被其他代码修改。
-
完美匹配:闭包的“创建时上下文”特性与回调函数的“延迟执行”需求是天作之合。
9.3.5 总结
闭包通过捕获并持久化外部变量,实现了高度灵活和安全的编程模式:
-
函数增强与包装:通过捕获原函数,在不修改源码的情况下增加新功能(日志、验证、转换等)。这是面向切面编程(AOP) 的一种简单实现。
-
环境隔离与安全:通过捕获并隐藏核心资源,构建一个受控的执行环境(沙盒)。这是实现系统安全和模块隔离的基石。
-
状态保持与回调:通过捕获创建时的上下文,为异步回调函数提供持久化的、私有的状态。这是事件驱动编程和函数式编程的核心模式。
9.4 函数式编程
本节将介绍如何利用函数式编程的特性构建一个简单的几何区域系统,可以表示简单的几何图形,且可以通过多种方式( 旋转 、变换、并集等)组合和修改这些图形 。核心概念是用特征函数表示几何区域。
- 几何区域被定义为点的集合。
- 每个区域可以用一个特征函数表示:输入一个点 (x, y),返回该点是否在区域内(true/false)
基本图形的定义:
-- 1. 通过圆心和半径定义圆
function disk(cx, cy, r)return function(x, y)return (x - cx)^2 + (y - cy)^2 <= r^2end
end-- 2. 通过左右上下边界定义矩形
function rect(left, right, bottom, up)return function(x, y)return left <= x and x <= right and bottom <= y and y <= upend
end
按照这种方式,使用正确的特征函数,可以创建诸如三角形或非轴对称矩形等其它基本图形。
区域的组合与变换:
-- 1. 创建区域的补集(反向区域)
-- @param r: 原始区域函数
-- @return: 返回一个新函数,表示不在原始区域内的点
function complement(r)return function(x, y)return not r(x, y)end
end-- 2. 创建两个区域的并集
-- @param r1, r2: 两个区域函数
-- @return: 返回一个新函数,表示在任一区域内的点
function union(r1, r2)return function(x, y)return r1(x, y) or r2(x, y)end
end-- 3. 创建两个区域的交集
-- @param r1, r2: 两个区域函数
-- @return: 返回一个新函数,表示同时在两个区域内的点
function intersection(r1, r2)return function(x, y)return r1(x, y) and r2(x, y)end
end-- 4.创建两个区域的差集 (r1 - r2)
-- @param r1, r2: 两个区域函数
-- @return: 返回一个新函数,表示在r1中但不在r2中的点
function difference(r1, r2)return function(x, y)return r1(x, y) and not r2(x, y)end
end-- 5. 平移区域
-- @param r: 原始区域函数
-- @param dx, dy: x和y方向的平移量
-- @return: 返回平移后的新区域函数
function translate(r, dx, dy)return function(x, y)return r(x - dx, y - dy) -- 通过坐标反向平移来实现end
end
为了使一个区域可视化,我们接下来写一个函数来生成一个 PBM (portable bitmap,可移植位图)格式的文件来绘制指定的区域。
- 使用
plot(r, M, N)
函数将区域r
绘制成 PBM 格式的图像。 - 将逻辑坐标
[-1,1]x[-1,1]
映射到像素坐标[1,M]x[1,N]
。 - 区域内像素输出为
"1"
(黑色),区域外为"0"
(白色)。
-- 将区域绘制为PBM格式文件
-- @param r: 区域函数
-- @param M, N: 图像的宽度和高度(像素)
-- 将逻辑坐标[-1,1]x[-1,1]映射到像素坐标[1,M]x[1,N]
function plot(r, M, N)io.write("P1\n", M, " ", N, "\n") -- PBM文件头for i = 1, N do -- 遍历每一行local y = (N - i * 2) / N -- 计算当前行的y坐标for j = 1, M do -- 遍历每一列local x = (j * 2 - M) / M -- 计算当前列的x坐标io.write(r(x, y) and "1" or "0") -- 区域内为1(黑),外为0(白)endio.write("\n")end
end
以下代码绘制了一个南半球可见的渐盈凸月:
c1 = disk(0, 0, 1)
moon = difference(c1, translate(c1, 0.3, 0))
plot(moon, 500, 500)
这种实现方式展示了函数式编程的几个关键优势:
-
高阶函数:函数可以作为参数和返回值,使得组合和变换变得简单。
disk, rect, complement, union
等所有函数都是高阶函数,因为它们都返回了一个新的函数。complement, union, translate, plot
这些函数,它们都接收一个或多个函数作为参数。
-
词法作用域:闭包可以捕获创建时的环境,保持状态。比如
disk
函数返回的匿名函数捕获了外部函数的参数cx, cy, r
。即使disk
函数已经执行完毕,这些变量的值依然被内部函数保持着。function disk (cx, cy, r) -- cx, cy, r 是外部函数的局部变量return function (x, y) -- 内部函数-- 这里访问了外部变量 cx, cy, r-- 这三个变量被“捕获”了,形成了闭包return (x - cx)^2 + (y - cy)^2 <= r^2end endlocal my_disk = disk(10, 20, 5) -- 此时,my_disk 这个函数不仅包含计算逻辑,还包含了一个指向 (cx=10, cy=20, r=5) 的环境引用。 --当你调用 my_disk(12, 20) 时,它使用的就是这些被记住的值。
-
组合性:通过简单的函数组合可以构建复杂的行为。比如
difference(translate(c1, 0.3, 0), c2)
会创建多层的闭包,每一层都记住了自己所需的环境。c1 = disk(0, 0, 1) -- 创建一个闭包,记住圆心(0,0),半径1-- 分解来看: local translated_c1 = translate(c1, 0.3, 0) -- `translate` 创建了一个新闭包,这个新闭包: -- 1. 记住了它接收到的函数 `c1` -- 2. 记住了位移量 dx=0.3, dy=0 -- 它的逻辑是:return c1(x - 0.3, y - 0)local moon = difference(c1, translated_c1) -- `difference` 创建了一个新闭包,这个新闭包: -- 1. 记住了第一个区域函数 `c1` -- 2. 记住了第二个区域函数 `translated_c1` -- 它的逻辑是:return c1(x, y) and not translated_c1(x, y)plot(moon, 500, 500) -- 当 plot 循环中调用 moon(x, y) 时,会触发一系列链式调用, -- 每一层调用都访问着它被创建时“记住”的那些变量和函数。
十、模式匹配
不同于许多其他脚本语言,Lua 并未采用 POSIX 或 Perl 风格的正则表达式,而是设计了一套独有的模式匹配机制。这一选择主要出于代码体积的考虑:一个典型的 POSIX 正则表达式实现需要超过 4000 行代码,而 Lua 的整个模式匹配系统仅用不到 600 行实现。尽管功能上不如完整的 POSIX 实现丰富,Lua 的模式匹配仍然非常强大,并且具备一些标准 POSIX 实现难以提供的特性。
10.1 模式匹配函数
Lua 的字符串库提供了四个基于模式的函数:find
、match
、gsub
和 gmatch
。
-
string.find
:用于在字符串中查找模式,返回匹配的起止位置。若未找到,返回nil
。s = "hello world" i, j = string.find(s, "hello") --> 1, 5 print(string.sub(s, i, j)) --> hello
- 第三个参数(可选):表示查找的起始位置
- 第四个参数(可选):是否启用简单查找模式(必须先传入第三个参数)
> string.find("a [word]", "[") stdin:1: malformed pattern (missing ']') > string.find("a [word]", "[", 1, true) --> 3 3
由于
'['
在模式中具有特殊含义,因此第1个函数调用会报错。在第2个函数调用中,函数只是把'['
当作简单字符串,所以才能查找成功。 -
string.match
:返回匹配到的子字符串,适用于提取符合模式的内容。date = "Today is 17/7/1990" d = string.match(date, "%d+/%d+/%d+") --> "17/7/1990"
-
string.gsub
:全局替换函数,3 个必选参数 分别是目标字符串、模式和替换字符串。最简单的用法是将匹配到的模式替换为指定字符串、函数或表的值。s = string.gsub("Lua is cute", "cute", "great") --> "Lua is great"
第 4 个参数是可选的,用于限制替换的次数
s = string.gsub("all lii", "l", "x", 1) print(s) --> axl lii s = string.gsub("all lii", "l", "x", 2) print(s) --> axx lii
-
string.gmatch
:返回一个迭代器,用于遍历所有匹配项。例如 ,以下示例可以找出指定字符串 s 中 出现的所有单词 :s = "some string" words = {} for w in string.gmatch(s, "%a+") dowords[#words + 1] = w end
在下一节后续我们马上会学习到,模式 ’%a+’ 会匹配一个或多个字母组成的序列( 也就是单词) 。因此 ,
for
循环会遍历所有目标字符串中的单词,然后把它们保存到列表words
中 。
10.2 模式语法
由于反斜杠/
是 Lua 语言中的转义符,为避免冲突,Lua模式使用百分号 %
作为转义字符。
10.2.1 字符分类
字符分类,就是模式中能够与一个特定集合中的任意字符相匹配的一项 。 例如,分类%d
匹配的是任意数字 。 因此 ,可以使用模式d%d%/d%d%/d%d%d%
来匹配 dd/mm/yyyy
格式的日期:
s = "Deadline is 30/05/1999, firm"
date = "%d%d/%d%d/%d%d%d%d"
print(string.match(s, date)) --> 30/05/1999
下表列出了所有预置的字符分类及其对应的含义:
类1 | 含义1 | 类2 | 含义2 | 类3 | 含义3 |
---|---|---|---|---|---|
. | 所有字符 | %a | 字母 | %c | 控制字符 |
%d | 数字 | %g | 非空格可打印字符 | %l | 小写字母 |
%p | 标点符号 | %s | 空白字符 | %u | 大写字母 |
%w | 字母数字 | %x | 十六进制数字 |
大写形式表示补集,如 %A
表示任意非字母字符。
print((string.gsub("hello, up-down!", "%A", "."))) --> hello..up.down.
10.2.2自定义字符集
字符集允许我们创建自定义的字符类,通过将单个字符和字符类组合在方括号内来实现。例如:
-
字符集
[%w_]
可匹配字母数字字符和下划线; -
[01]
可匹配二进制数字; -
[%[%]]
则可匹配方括号本身。 -
统计文本中的元音字母数量:
_, nvow = string.gsub(text, "[AEIOUaeiou]", "")
10.2.2 魔法字符、连字符、脱字符
-
魔法字符:以下字符在模式中具有特殊含义,需用
%
转义:( ) . % + - * ? [ ] ^ $
因此,
'%?'
匹配1个问号,'%%'
匹配1个百分号。 -
连字符:字符集支持使用连字符
“-”
指定字符范围,例如[0-7]
表示所有八进制数字。但这一功能并不常用,因为大多数常用范围已有预定义类别,例如:-
%d
等价于[0-9]
(数字) -
%x
等价于[0-9a-fA-F]
(十六进制数字)
-
-
脱字符:若需匹配某个字符集的补集,可在开头使用脱字符“^”。例如:
-
[^0-7]
匹配非八进制数字字符; -
[^\n]
匹配非换行符。
需要注意的是,简单字符类的补集可直接通过其大写形式表示,例如使用
%S
(非空白字符)比[^%s]
更简洁。 -
10.2.3 重复/可选修饰符
-
+
总是获取与模式相匹配的最长序列 。修饰符 含义 修饰符 含义 +
1 次或多次 *
0 次或多次(贪婪) -
0 次或多次(懒惰) ?
0 次或 1 次 例如,模式
’%a+’
代表一个或多个字母(即一个单词):print((string.gsub("one, and two; and three", "%a+", "word"))) --> word, word word; word word
模式
'%d+'
匹配一个或多个数字(一个整数):print(string.match("the number 1298 is even", "%d+")) --> 1298
-
星号
*
:贪婪匹配零次或多次- 匹配最长可能的序列
- 接受零次出现(即可以匹配空序列)
- 典型应用:匹配可选空格
-- 匹配括号对,允许中间有空格 pattern = '%(%s*%)' -- 匹配 () 或 ( )
-
减号
-
: 懒惰匹配零次或多次- 匹配最短可能的序列
- 同样接受零次出现
- 用于非贪婪匹配,避免过度匹配
-- 正确匹配C语言注释(非贪婪方式) pattern = '/%*.-%*/' -- 匹配 /* 注释内容 */
-
问号
?
: 可选匹配零次或一次- 匹配零次或一次出现
- 用于表示可选内容
-- 匹配可能带符号的整数 pattern = '[+-]?%d+' -- 匹配 -12, 23, +1009
贪婪 vs 懒惰匹配的对比
test = "int x; /* x */ int y; /* y */"-- 贪婪匹配(错误结果)
print(string.gsub(test, "/%*.*%*/", "")) --> int x;-- 懒惰匹配(正确结果)
print(string.gsub(test, "/%*.-%*/", "")) --> int x; int y;
标识符匹配示例
-- 正确匹配Lua标识符(贪婪匹配)
pattern = '[_%a][_%w]*' -- 匹配 identifier, _var, name123-- 错误用法(懒惰匹配会过早结束)
pattern = '[_%a][_%w]-' -- 只匹配第一个字符
限制:修饰符只能应用于字符类
- 不能对分组模式使用修饰符
- 无法直接匹配"可选单词"(除非单词只有一个字母)
10.2.4 锚点:^
匹配开头,$
匹配结尾
if string.find(s, "^%d") then ... -- 检查字符串 s 是否以数字开头
if string.find(s, "^[+-]?%d+$") then ... -- 检查字符串是否为一个没有多余前缀字符和后缀字符的整数
^
和$
字符只有位于模式的开头和结尾时才具有特殊含义;否则 ,它们仅仅就是与其自身相匹配的普通字符 。
10.2.5 平衡匹配
平衡匹配 %bxy
:匹配成对的字符。其中,x 和 y 必须是两个不同的字符,分别表示开始字符和结束字符。例如 %b()
匹配括号内的内容:
s = "a (enclosed (in) parentheses) line"
print((string.gsub(s, "%b()", ""))) --> a line-- 提取花括号内的JSON内容
json_string = '{"name": "John", "age": 30}'
content = string.match(json_string, "%b{}")-- 嵌套匹配HTML标签内容(简单情况)
html = "<div><p>text</p></div>"
tag_content = string.match(html, "%b<>")
10.2.6 边界匹配
%f[char-set]
用于匹配字符集边界的位置,边界模式在以下条件下匹配成功:
- 前一个字符不属于
char-set
- 下一个字符属于
char-set
常用边界模式:
%f[%w]
:从非字母数字到字母数字的边界%f[%W]
: 从字母数字到非字母数字的边界%f[%a]
:从非字母到字母的边界%f[%A]
:从字母到非字母的边界
-
单词边界检测
-- 只匹配完整的单词 "the" text = "theater the theme" result = string.gsub(text, "%f[%w]the%f[%W]", "ONE") --> theater ONE theme
-
标识符提取
-- 提取字符串中的所有单词 for word in string.gmatch(text, "%f[%w]%a+%f[%W]") doprint(word) end
-
格式验证
-- 检查字符串是否以字母开头 if string.find(text, "^%f[%w]") thenprint("以字母数字开头") end
10.3 捕获
捕获是Lua模式匹配中的核心功能之一,它允许我们从匹配的字符串中提取特定部分。通过在模式中使用圆括号()
,我们可以标记需要捕获的子模式,这些被标记的部分会在匹配成功后被单独返回。
10.3.1 基本捕获语法
在模式中使用圆括号来指定捕获内容:
pair = "name = Anna"
key, value = string.match(pair, "(%a+)%s*=%s*(%a+)")
print(key, value) --> name Anna
(%a+)
:捕获一个或多个字母%s*
:匹配零个或多个空格=
:匹配等号(%a+)
:捕获一个或多个字母
多捕获示例:使用三个独立的捕获来分别获取日、月、年。
date = "Today is 17/7/1990"
d, m, y = string.match(date, "(%d+)/(%d+)/(%d+)")
print(d, m, y) --> 17 7 1990
10.3.2 引用捕获(略)
10.3.3 空捕获
空捕获 ()
捕获位置信息而非内容:
print(string.match("hello", "()ll()")) --> 3 5
10.4 替换
Lua的string.gsub
函数不仅支持简单的字符串替换,还提供了两种高级替换方式:函数替换和表替换,极大地增强了模式匹配的灵活性和表达能力。string.gsub
基本语法为:
string.gsub(主体字符串, 模式, 替换内容[, 替换次数限制])
第三个参数替换内容可以是字符串、表或者函数,即Lua的替换机制提供了三个层次的解决方案,可以混合使用来处理复杂的文本转换需求:
- 简单替换:直接字符串替换
- 映射替换:使用表进行键值映射
- 逻辑替换:通过函数实现复杂替换逻辑
这种设计使得string.gsub
成为一个极其强大的文本处理工具,能够优雅地处理从简单到复杂的各种文本转换需求,特别是在模板渲染、数据编码、格式转换等场景中表现出色。
10.4.1 字符串替换
s = string.gsub("Lua is cute", "cute", "great")
print(s) --> Lua is great
10.4.2 表替换
使用第一个捕获作为键查找表,对应的值作为替换内容。表中没有这个键时,或者键对应值为nil
,那么函数 gsub
不改变这个匹配 。表替换适合键值对形式的简单映射。
-
基本表替换,处理缺失键
-- 安全表替换函数 function safeTableReplace(template, data)return string.gsub(template, "$(%w+)", function(key)return data[key] or ("[" .. key .. "未设置]")end) end-- 不完整的数据 local partialData = {product = "Lua编程书",price = "68.00",-- author 键缺失 }local productTemplate = "产品: $product, 价格: $price, 作者: $author"print(safeTableReplace(productTemplate, partialData)) --> 产品: Lua编程书, 价格: 68.00, 作者: [author未设置]
-
多行模板:
-- 用户信息表 local userInfo = {username = "luauser",email = "luauser@example.com",level = "VIP",points = 1500,join_date = "2023-01-15" }-- 多行模板 local emailTemplate = [[ 尊敬的 $username ($level会员):感谢您使用我们的服务! 您的积分:$points 注册日期:$join_date如有问题,请联系:$email祝好, 客服团队 ]]-- 执行表替换 local personalizedEmail = string.gsub(emailTemplate, "$(%w+)", userInfo)print(personalizedEmail)
输出: 尊敬的 luauser (VIP会员):感谢您使用我们的服务! 您的积分:1500 注册日期:2023-01-15如有问题,请联系:luauser@example.com祝好, 客服团队
表替换的优势:
- 简洁高效:一行代码完成复杂替换,性能比多次函数调用更高效
- 可维护性:数据与模板分离,易于管理
- 灵活性:支持动态数据源
10.4.3 函数替换
每次匹配时调用函数,函数返回值作为替换内容。下述函数用于变量展开,它会把字符串中所有出现的 $varname
替换为全局变量 varname
的值 :
-- 变量扩展功能
function expand (s)return (string.gsub(s, "$(%w+)", _G))
endname = "Lua"; status = "great"
print(expand("$name is $status, isn't it?")) --> Lua is great, isn't it?
_G
是预先定义 的包括所有全局变量的表,对于每个与'$(%w+)'
匹配的地方( $
符号后紧跟一个字),函数gsub
都会在全局表 _G
中查找捕获到的名字,并用找到 的结果替换字符串中相匹配的部分;如果表中没有对应的键, 则不进行替换 。
如果不确定是否指定变量具有字符串值 ,那么可 以对它们的值调用 函数 tostring
。 在这种情况下,可以用一个函数来返回要替换的值 :
-- 变量扩展功能
function expand(s)return string.gsub(s, "$(%w+)", function(n)return tostring(_G[n] or "")end)
endname = "Lua"
status = "great"
print(expand("$name is $status")) --> Lua is great
示例二:条件替换
-- 只替换存在的变量
function safeExpand(s)return string.gsub(s, "$(%w+)", function(n)return _G[n] and tostring(_G[n]) or "$"..nend)
end
10.4.4 嵌套命令处理
function toxml(s)s = string.gsub(s, "\\(%a+)(%b{})", function(tag, body)body = string.sub(body, 2, -2) -- 去除花括号body = toxml(body) -- 递归处理嵌套内容return string.format("<%s>%s</%s>", tag, body, tag)end)return s
endprint(toxml("\\title{The \\bold{big} example}"))
--> <title>The <bold>big</bold> example</title>
相比多次调用string.match
和手动拼接,使用gsub
的单次遍历更高效。
10.4.5 URL编码解码(略)
10.4.6 制表符展开(略)
10.5 技巧(略)
十一、统计文本中的高频词
本篇将介绍一个实用的 Lua 程序——统计文本中的高频词。这个程序巧妙运用了迭代器、匿名函数等 Lua 特性,是一个很好的学习案例。
11.1 核心思路与数据结构
程序的核心目标是:统计词频 -> 排序 -> 输出结果。
其核心数据结构是一个 table
,它充当词频计数器(Counter):
- 键(Key):单词(字符串)
- 值(Value):该单词在文本中出现的次数(数字)
11.2 实现步骤详解
-
统计词频:程序首先需要读取文本并填充词频计数器。我们通过两层迭代器来实现:
- 第一层:使用
io.lines()
迭代器逐行读取输入。 - 第二层:使用
string.gmatch
迭代器,利用模式"%w+"
匹配每一行中的每一个字母数字序列(即我们定义的“单词”)。
对于每个匹配到的单词,我们使用
(counter[word] or 0) + 1
这种惯用法来初始化或递增其计数器。local counter = {} for line in io.lines() dofor word in string.gmatch(line, "%w+") docounter[word] = (counter[word] or 0) + 1end end
- 第一层:使用
-
创建并排序单词列表
counter
表的键是单词,但我们无法直接对键进行排序。因此,我们需要将所有的键(即单词)提取到一个新的数组words
中。local words = {} -- 存放所有单词的列表 for w in pairs(counter) dowords[#words + 1] = w end
-
使用
table.sort
进行排序。这里的关键在于自定义的排序函数。该函数是一个匿名函数,定义了复杂的排序规则:- 首要规则:频率高的单词排在前面 (
counter[w1] > counter[w2]
)。 - 次要规则:如果频率相同,则按字母顺序升序排列 (
w1 < w2
)。
table.sort(words, function(w1, w2)return counter[w1] > counter[w2] or(counter[w1] == counter[w2] and w1 < w2) end)
- 首要规则:频率高的单词排在前面 (
-
输出结果:程序输出前 N 个高频词及其次数。
程序通过命令行参数arg[1]
获取用户想要打印的单词数量n
。如果用户没有提供参数,则默认打印所有单词。-- 确定要打印的单词数量(取用户输入和单词总数的最小值) local n = math.min(tonumber(arg[1]) or math.huge, #words)for i = 1, n doio.write(words[i], "\t", counter[words[i]], "\n") end
这个高频词统计程序是一个经典的例子,它展示了 Lua 如何以简洁的代码处理复杂任务:
- 使用
pairs
和gmatch
进行高效遍历。 - 使用 Table 作为核心数据结构来映射键值关系。
- 使用匿名函数实现自定义的复杂排序逻辑。
- 通过命令行参数
arg
表实现用户交互。
11.3 程序运行示例
-- 引入(隐式)arg 表:arg[1] 存放命令行第一个参数,即“要打印的单词个数”-- 1. 统计阶段 ----------------------------------------------------------
local counter = {} -- counter[word] 记录单词出现次数
for line in io.lines() do -- 逐行读标准输入(重定向文件或手工输入)-- %w+ 匹配“连续字母/数字/下划线”,即把每行拆成单词for word in string.gmatch(line, "%w+") docounter[word] = (counter[word] or 0) + 1end
end-- 2. 整理阶段 ----------------------------------------------------------
local words = {} -- 用来存放“出现过”的所有单词(无重复)
for w in pairs(counter) do -- 遍历哈希表,把单词收集到数组里words[#words + 1] = w
end-- 3. 排序阶段 ----------------------------------------------------------
-- 自定义排序函数:先按出现次数降序;次数相同则按字典序升序
table.sort(words, function (w1, w2)return counter[w1] > counter[w2] or(counter[w1] == counter[w2] and w1 < w2)
end)-- 4. 输出阶段 ----------------------------------------------------------
-- 决定打印多少个:若命令行给了数字就用它,否则打印全部
local n = math.min(tonumber(arg[1]) or math.huge, #words)-- 依次输出 Top-N 的“单词+制表符+出现次数”
for i = 1, n doio.write(words[i], "\t", counter[words[i]], "\n")
end
将程序保存为 wordcount.lua
,统计当前目录下 book.txt
文件中出现频率最高的前 10
个单词:
$ lua wordcount.lua 10 < book.txt
the 5996
a 3942
to 2560
is 1907
of 1898
in 1674
we 1496
function 1478
and 1424
x 1266
从结果可以看出,高频词通常是 "the", "a", "to"
这类功能性的短词,它们本身携带的信息量较少。
11.4 练习与扩展
-
修改程序,使其忽略长度小于 4 个字母的单词。
思路:在统计词频的循环内,添加一个条件判断
if #word >= 4 then ... end
,只对长单词进行计数。 -
从一个文本文件中读取“停用词”列表(如
"the", "a", "in"
),并忽略这些单词。思路:在程序开始时,读取一个停用词文件并将其中的单词存入一个集合表(
ignore_list = {the=true, a=true, ...}
)。在统计时,检查当前单词是否不在这个忽略列表中 (if not ignore_list[word] then ... end
)。
十二、日期与时间
Lua 处理时间有两种方式:
- 数值表示(时间戳):一个数字,通常表示从纪元(Epoch) 开始所经过的秒数。在大多数系统(如 POSIX 和 Windows)上,纪元是指 1970 年 1 月 1 日 0:00 UTC。
- 表表示(日期表):一个包含时间细节的 Lua 表。关键字段包括:
year
,month
,day
,hour
,min
,sec
(整数)wday
:星期几(1 代表星期日)yday
:一年中的第几天(1 代表 1 月 1 日)isdst
:布尔值,表示是否处于夏令时
注意:日期表不包含时区信息,时区解释需要由程序自行处理。
12.1 os.time
os.time
函数用于获取当前时间戳,或将日期表转换为时间戳。
-
获取当前时间戳(不带任何参数)
local timestamp = os.time() print(timestamp) -- 输出例如:1439653520
-
将日期表转换为时间戳
调用os.time(table)
,传入一个日期表。year
,month
,day
字段是必须的,hour
,min
,sec
没有提供则默认为12:00:00
。local t = os.time({year=2015, month=8, day=15, hour=12, min=45, sec=20}) print(t) --> 1439653520-- 注意时区的影响:1970-01-01 00:00:00 在不同时区下会得到不同的时间戳 local t1 = os.time({year=1970, month=1, day=1, hour=0}) print(t1) --> 可能输出 10800(表示东一区 UTC+1)
关键特性:当传入的日期表字段值“超出范围”时(例如
day=40
),os.time
会接受它并进行规范化(Normalization)。这为日期计算提供了巨大便利。
12.2 函数 os.date
os.date
函数是 os.time
的逆操作,它将一个时间戳转换为更易读的字符串或日期表形式。
-
格式化为字符串:第一个参数是格式字符串,包含以
%
开头的指令。第二个参数是时间戳,默认为当前时间。-- 格式化为常见的日期字符串 print(os.date("%Y-%m-%d", 906000490)) --> 1998-09-16 print(os.date("%d/%m/%Y", 906000490)) --> 16/09/1998 print(os.date("%A in %B", 906000490)) --> Tuesday in September (依赖本地化设置)-- 生成 ISO 8601 格式 local t = 906000490 print(os.date("%Y-%m-%dT%H:%M:%S", t)) --> 1998-09-16T23:48:10
指示符 含义 示例 指示符 含义 示例 指示符 含义 示例 %a
星期几简写 Wed %A
星期几全名 Wednesday %b
月份简写 Sep %B
月份全名 September %c
日期时间 09/16/98 23:48:10 %d
月内第几天 16 %H
24时小时 23 %I
12时小时 11 %j
年内第几天 259 %m
月份 09 %M
分钟 48 %p
AM/PM pm %S
秒 10 %w
星期 3 %W
年内第几周 37 %x
日期 09/16/98 %X
时间 23:48:10 %y
两位数年份 98 %Y
完整年份 1998 %z
时区 -0300 %%
百分号 % -
转换为日期表:使用格式字符串
"*t"
,os.date
会返回一个日期表。local date_table = os.date("*t", 906000490) -- date_table 内容为: -- { year=1998, month=9, day=16, yday=259, wday=4, -- hour=23, min=48, sec=10, isdst=false }
重要等式:
os.time(os.date("*t", t)) == t
对任何有效时间戳t
都成立。 -
处理 UTC 时间:在格式字符串前加上感叹号
!
,可以强制使用 UTC 时间而非本地时间。-- 查看纪元的 UTC 时间 print(os.date("!%c", 0)) --> Thu Jan 1 00:00:00 1970
如果不带任何参数调用函数
os.date
,那么该函数会使用格式%c
,即以一种合理的格式表示日期和时间信息。请注意,%x、%X 和&c 会根据不同的区域和系统而发生变化。如果需要诸如dd/mm/yyyy
这样的固定表示形式,那么就必须显式地使用诸如"%d/%m/%Y"
这样的格式化字符串。
12.3 日期时间处理
12.3.1 os.time 的归一化特性
当你将一个日期表传递给 os.time
时,表中的字段值不需要是规范的。系统会自动处理溢出或负值,并将其转换为一个有效、规范的时间戳,这种方法优雅地规避了不同月份天数不同、闰年等复杂日历问题。
-
计算 40 天后的日期
-- 1. 获取代表当前时间的日期表 local t = os.date("*t")-- 2. 直接修改表中的天数 t.day = t.day + 40-- 3. 通过 os.time 将修改后的表转换为时间戳(自动归一化发生在此刻) -- 4. 再用 os.date 将时间戳格式化为易读的字符串 print(os.date("%Y/%m/%d", os.time(t))) -- 输出可能为:2015/09/27(从 2015/08/18 开始算)
可见,我们只需创建一个表示当前(或某个基准)时间的日期表,然后直接对其字段(如
day
,month
,year
)进行数学运算,最后让os.time
帮我们计算出正确的未来或过去时间。 -
计算 6 个月后的日期:直接操作秒数来计算月份是非常困难的(因为每个月秒数不同),但使用归一化方法则非常简单:
local t = os.date("*t") print(os.date("%Y/%m/%d", os.time(t))) -- 输出当前日期,例如:2015/08/18t.month = t.month + 6 -- 增加 6 个月print(os.date("%Y/%m/%d", os.time(t))) -- 输出:2016/02/18
-
处理负值(计算40天前的日期):即使将天数减到负数,归一化过程也能正确回溯到上一个月。
local t = os.date("*t") print(t.day, t.month) -- 输出当前日/月,例如:26, 2 (2月26日)t.day = t.day - 40 print(t.day, t.month) -- 表内的值变为:-14, 2-- 关键步骤:归一化处理 t = os.date("*t", os.time(t)) print(t.day, t.month) -- 输出规范后的结果:17, 1 (1月17日)
-
月末的一个月后:归一化逻辑是直观的,但你必须意识到其结果是严格遵循公历规则的。这有时会产生一些反直觉的结果。
-- 假设当前日期是 3月31日 local t = {year=2023, month=3, day=31} t.month = t.month + 1 -- 增加到 4月31日 -- 4月只有30天,因此归一化后会变成 5月1日-- 如果再从 5月1日 减一个月: t.month = t.month - 1 -- 结果将是 4月1日,而非最初的 3月31日
这并非 Lua 的 Bug,日历机制导致的结果,因为从 3月31日 开始,增加“一个月”这个操作在日历上没有唯一的定义。
12.3.2 os.difftime
os.difftime
函数在任何系统上都返回两个时间戳之间以秒为单位的差值,其可靠性优于直接相减:
-
基础示例
-- 计算 Lua 5.3 和 5.2 发布日期间隔了多少天 local t5_3 = os.time({year=2015, month=1, day=12}) local t5_2 = os.time({year=2011, month=12, day=16}) local d = os.difftime(t5_3, t5_2) -- 使用 difftime 获取秒数差 print(d // (24 * 3600)) -- 将秒转换为天 --> 1123.0
核心建议:对于大多数的日期推算(如“几天后”、“几个月前”),首选方法是修改日期表字段并利用
os.time
归一化。这是 Lua 标准库中处理日期时间最优雅和强大的技巧。 -
将任意秒数转换为具体日期:
-- 1. 定义自定义纪元(2000年1月1日 00:00:00) local T = {year = 2000, month = 1, day = 1, hour = 0} -- 2. 设置从纪元开始经过的秒数 T.sec = 501336000 -- 3. & 4. 转换并格式化输出 local converted_timestamp = os.time(T) print(os.date("%d/%m/%Y", converted_timestamp)) --> 20/11/2015
-
使用
os.clock
测量代码运行时间:虽然os.difftime
可以计算真实世界的时间间隔,但测量代码段的运行时间 时,更推荐使用os.clock
函数。- 返回程序所消耗的 CPU 时间,而不是挂钟时间。这使其更能准确反映代码本身的性能,不受系统其他进程负载的影响。
os.clock
通常返回浮点数(float),具有亚秒级精度。
local start_time = os.clock() -- 记录开始时的CPU时间local s = 0 for i = 1, 100000 dos = s + i endlocal elapsed_time = os.clock() - start_time -- 计算消耗的CPU时间 print(string.format("elapsed CPU time: %.2f seconds\n", elapsed_time)) --格式化输出结果 -- 示例输出:elapsed CPU time: 0.02 seconds