1.3 八种值类型与变量作用域


文档摘要

1.3 八种值类型与变量作用域 本节摘要:Lua 是动态类型语言,全部值只有八种类型——nil、boolean、number、string、function、userdata、thread、table。变量没有类型,值有类型。本节逐一看八种值的脾气,分清值语义与引用语义,再讲透变量:全局变量在实现上只是环境表 ENV 里的一个字段,local 才是嵌入风格的正统选择。 值有类型,变量没有 先建立一个正确的心理模型:Lua 里变量是名字,不是盒子。一个名字此刻可以指字符串,下一行指函数,语言不做任何约束: 是类型探针,返回类型名字符串。这种自由在嵌入式场景是特性而非缺陷:宿主往脚本环境里注入的对象类型经常随版本变化,动态类型让胶水层不必为每个版本重写声明。

1.3 八种值类型与变量作用域

本节摘要:Lua 是动态类型语言,全部值只有八种类型——nil、boolean、number、string、function、userdata、thread、table。变量没有类型,值有类型。本节逐一看八种值的脾气,分清值语义与引用语义,再讲透变量:全局变量在实现上只是环境表 _ENV 里的一个字段,local 才是嵌入风格的正统选择。

值有类型,变量没有

先建立一个正确的心理模型:Lua 里变量是名字,不是盒子。一个名字此刻可以指字符串,下一行指函数,语言不做任何约束:

> x = "gateway" > print(type(x)) string > x = 80 > print(type(x)) number > x = print > print(type(x)) function

type 是类型探针,返回类型名字符串。这种自由在嵌入式场景是特性而非缺陷:宿主往脚本环境里注入的对象类型经常随版本变化,动态类型让胶水层不必为每个版本重写声明。

八种类型按"谁拥有内存"可以分成两栏。前四种是值语义的轻量值,赋值与传参是拷贝;后四种是引用语义的重值,赋值传的是引用,垃圾回收器盯着它们:

01-03-fig01

轻值四种:nil、boolean、number、string

nil 只有一个值,就是 nil。它表示"没有":不存在的键读出来是 nil,没赋值的变量是 nil,把变量设成 nil 等于删掉它。判断表里有没有某项,用 rawget 或与 nil 比较,别用 t[k] == false——false 是"明确否定",nil 是"根本没提",两者在配置语义上要分开:宿主解析配置时,"未填写"与"显式关掉"是两码事。

boolean 只有 true 和 false。但条件判断不只认它们——Lua 的规则全语言最简:nil 和 false 为假,其余一切为真。注意这和 C、JavaScript 都不同:

> if 0 then print("0 是真") end 0 是真 > if "" then print("空串也是真") end 空串也是真

为什么 0 为真?因为 nil 已经承担了"缺席"语义,再让 0 也表示假就会引入两套"没有",脚本和 C 边界转换时徒增心智负担。少一条特例,宿主就少一类坑。

number 在 5.3 之后是两种内部表示:64 位整数与双精度浮点,对用户呈现为同一个 number 类型:

> print(type(3), type(3.0), type(3e2)) number number number > print(math.type(3), math.type(3.0)) integer float > print(3 == 3.0) true > print(7 / 2) 3.5 > print(7 // 2, 7 % 2) 3 1

数学上相等则相等(3 == 3.0),除法恒返回浮点,// 是向下取整除。5.3 之前只有双精度浮点一种,做位运算要先转整数再转回来,游戏与设备场景叫苦不迭——这就是 1.1 节版本线里"5.3 补整数"的由来。整数溢出会回绕(模 2 的 64 次幂),做哈希或编码时要心里有数。

整浮共存后有一个显示层面的细节必须现在说透,因为它是脚本拼参数时的常驻事故:浮点值显示时总带小数点

> print(tostring(8), tostring(8.0)) 8 8.0 > print(string.format("%d", 8.5)) input:1: number has no integer representation

tostring 对整数与浮点给出不同文本;%d 遇到无整数表示的浮点直接报错。需要"数值进字符串出、看起来干净"的场合(拼 SQL、拼 URL、拼协议帧),一律用 string.format%d%.0f 收口,不要用 .. 硬拼。判断一个数是不是整数,math.type(v) == "integer"v % 1 == 0 更直白。

数值与字符串之间还有一对官方转换函数:tonumbertostringtonumber 转不成功返回 nil 而不是抛错,这让它成为输入校验的标准件:

> print(tonumber("99"), tonumber("0x1f"), tonumber("3e2")) 99 31 300.0 > print(tonumber("width=8")) nil

解析外部输入(配置、协议、用户提交)的第一行防御几乎总是 local n = tonumber(raw); if not n then ... endtostring 则把任何值转成字符串:nil 得 "nil"、数字按上面规则、函数与表得地址文本。

string 是不可变的字节序列。不存"字符"只存"字节",编码由使用者约定(现代项目统一 UTF-8)。不可变意味着"修改"字符串必然产生新串:

> local s = "redis" > s = s:upper() > print(s) REDIS

字符串可以相乘吗?不行,但可以用 string.rep 复制,用 .. 连接。长度用 #s 取字节数,中文按 UTF-8 通常是每字三字节,所以 #"脚本" 是 6——按字节数不是按字数,处理多语言文本时这一点会在第 4 章再算账。不可变带来的最大红利是驻留:内容相同的字符串在虚拟机里共享一份,比较是指针级,作为表的键效率极高——又是典型的"为宿主省成本"设计。

重值四种:function、userdata、thread、table

function 是一等公民:可以存进变量、放进表里、当参数传、当返回值回。宿主把 C 函数包装后注册进脚本环境,脚本看到的就是 function 类型的值,调它就是跨边界进 C。print 出来是地址样的 function: 0x...。第 2 章整章讲它。

userdata 是宿主对象的门牌。宿主想在脚本世界里暴露一个 C 对象(一个 socket、一个 redis 连接、一个引擎里的实体),就造一个 userdata 值:脚本侧只能持有它、传它、把它交给宿主的 API,不能(默认)窥视内部。配合元表可以给 userdata 挂上方法与运算符,让 C 对象用起来像本地对象。你在 OpenResty 里看到的 ngx.req 相关句柄、游戏脚本里的 Entity,底子多半是 userdata。

thread 是协程的正式类型名。注意与操作系统线程无关——它只是"一段可暂停恢复的执行状态"。全册第 7 章专门讲。

table 是唯一复合结构。数组、哈希、对象、模块、环境,全是它。引用语义的代表作:

> local a = { 1, 2, 3 } > local b = a > b[1] = 99 > print(a[1]) 99

ba 是同一个表的两个名字。判断两个表是否同一个用 ==(引用相等);想判"内容相同"得自己写深比较。第 3 章整章讲它,这里只需记住:宿主与脚本交换结构化数据,最终都以表的形式过栈——表是两种语言之间唯一的复合数据公约。

顺带一个引用语义的经典面试题:把表传进函数,函数内改它的字段,外面看得见吗?看得见——传的是引用,两边操作同一张表。但如果函数内做 t = {} 整体替换,外面看不见——替换只改了形参这个名字的指向,原表纹丝不动。"改内容"与"换对象"是两种完全不同的操作,这一区分在函数一节还会以参数传递的形式再出现一次。

变量:全局的真相与 local 的规矩

现在讲变量。Lua 的变量分三种:全局变量、局部变量(local)、表字段。第三种显然是表的事,前两种在这里说透。

全局变量的真相:从 5.2 起,一个"全局变量"在编译器眼里其实是"当前环境表 _ENV 里的字段"。写 x = 1 等价于 _ENV.x = 1,读 x 等价于 _ENV.x_ENV 本身只是一个普通的局部可见名字,于是"改变环境"变成"换一张表指过去"——沙箱实现因此轻而易举:

> version = "1.0" > print(_ENV.version) 1.0 > print(_G.version) -- _G 是环境表对自己的引用 1.0

这套机制的妙处要到第 6 章沙箱一节才完全展开:宿主给不同插件分配不同 _ENV,插件之间的全局变量就互不可见;想收权,换一张白表即可。

顺着 _ENV 再往深看一层,能看到全局变量在 5.1 与 5.2 之后的两副面孔。5.1 的实现里每个函数带一个"环境表",全局名查它,setfenv 函数可以换掉某个函数的环境;5.2 之后取消 setfenv,把环境变成编译器可见的上值 _ENV——语义相同,实现从"隐藏字段"变成"普通名字",于是你可以用普通 Lua 代码操纵它:

-- 给一小段脚本换个干净环境再执行(沙箱雏形) local sandbox = { print = print, math = math } local chunk = load("return 1 + 1", "demo", "t", sandbox) print(chunk()) --> 2 -- 沙箱里没有 io,尝试访问得 nil local bad = load("return io.open", "demo2", "t", sandbox) print(bad()) --> nil

load 的第四个参数就是给这块代码指定 _ENV。三行代码搭出一个能力白名单,这是 1.2 节"宿主式脚本"的机制内核,也是 Redis、OpenResty 敢收第三方脚本的底气。中间这段代码你现在不必逐字理解,只需记住结论:环境的切换是表级别的操作,便宜且可组合

全局变量贵在哪:每次读写都是一次哈希查找,编译器无法像对 local 那样把它优化成寄存器访问;名字全局共享,两个脚本库都定义 utils 就会互相覆盖;全局表被垃圾回收视为根,里面的值永远可达,忘了清理就常驻内存。所以在嵌入现场,风格检查的第一条几乎都是"禁止裸全局"。

local 的规矩

local a = 1 -- 当前块(chunk)可见 do local b = 2 -- 只在这个 do 块可见 print(a + b) end -- print(b) -- 此处 b 不存在,读到的是全局 b,即 nil

local 的作用域从声明语句延伸到所在块结束。控制结构的体(if/for/while 的体)各自是块;函数体是块。for 的循环变量在 5.4 里是每次迭代新造的局部变量,闭包捕获不会串值——细节留到第 2 章。

一个常见疑问:交互解释器里每行是一个独立块,所以上一行的 local 下一行就看不见:

> local n = 10 > print(n) -- n 已经出了作用域,这是全局 n nil

想跨行保持,要么不加 local(用全局,练习时无妨),要么把整段塞进 do ... end 一次敲完。这个差异坑过很多人,值得单独记一笔记。

local 与性能:local 在寄存器里,访问是指令级;全局是哈希表查找加函数调用语义。热点循环里把全局缓存成 local 是 Lua 优化的第一课:

local sqrt = math.sqrt -- 把表字段查找提到循环外 for i = 1, 1000000 do local x = sqrt(i) end

遮蔽与命名:内层 local 与外层同名时,内层遮蔽外层,出块后外层"复活":

local tag = "outer" do local tag = "inner" print(tag) --> inner end print(tag) --> outer

遮蔽有时是坑(改错了一层),有时是刻意为之(循环里复用短名)。工程上的折中惯例:作用域差很大的同名要避免,紧邻的遮蔽可以接受。另外 local function flocal f = function 有个微妙差别——前者允许函数体里递归调用 f(声明先于函数体),后者不行,递归函数请固定用 local function 写法。

清空与回收:把 local 置 nil 不必要(出块自动消失),但把表里的键置 nil 是正操作(删键、帮 GC 断引用)。长生命周期的缓存表里存大对象时,记得用 t[k] = nil 显式断开,只把局部变量丢给 GC 不够——只要表还引用着它,它就活着。这条规则在游戏脚本的对象池、网关的连接缓存里天天用到。

⚠️ 常见坑:宿主环境里裸赋值 foo = ... 会悄悄污染全局表。若两个插件都定义全局 callback,后加载的会覆盖先加载的,故障表现是"偶发丢逻辑"。规范做法是每个模块顶部声明 local,模块对外只暴露一个表。

💡 关键直觉:把全局表想成"房东的公告板",把 local 想成"房客自带的抽屉"。公告板上贴东西人人可见也人人可撕;重要家当进抽屉,用完(块结束)自动清走。

本节要点回顾

  • 八种类型一分俩:nil、boolean、number、string 是值语义轻值;function、userdata、thread、table 是引用语义重值,由 GC 追踪;
  • 真值规则全语言最简:只有 nil 与 false 为假,0 与空串皆真,减少 C 边界转换的特例;
  • number 内部整数浮点并存(5.3 起),math.type 可辨,除法恒浮点、// 向下取整、整数溢出回绕;
  • string 不可变且驻留,按字节计长,# 数的是字节数;
  • userdata 是宿主对象的门牌,thread 即协程,table 是唯一复合结构、跨边界的数据公约;
  • 全局变量即 _ENV 字段,改环境等于换表,沙箱的基石;local 进寄存器、作用域到块尾,嵌入风格第一守则是"能 local 就 local"。

下一节把表达式与控制流补齐,语言的"入住培训"就全部完成。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U