函数和控制流

这一页集中说明 Lux 在 Lua 之上增加的日常语法。它们的目标不是把 Lua 变成另一种 完全不同的语言,而是把 GLua 里最常见的样板写法压缩掉,同时保留可预测的编译结果。

下面的“编译后 Lua”展示的是语义等价的生成形态。实际输出里的临时变量名可能不同, 但求值顺序、nil 判断和返回行为应保持一致。

fn 函数

fn 是 Lux 的函数声明语法。简单名字的顶层 fn 会创建模块私有绑定,不会写入 全局表,也不会自动导出。

单表达式函数
fn sum(a, b) = a + b

块函数使用花括号。函数体最后一个没有分号的表达式会作为隐式返回值。

块函数与隐式返回
fn tier(exp) {
  if exp >= 25 {
    "veteran"
  } else {
    "visitor"
  }
}

简单名字的 fn 会被绑定提升,所以同一作用域内可以前向调用,也可以互相递归。提升 的是函数绑定,不是普通语句的执行顺序。

函数绑定提升
fn isEven(n) = n == 0 or isOdd(n - 1)
fn isOdd(n) = n ~= 0 and isEven(n - 1)

默认参数

默认参数只在对应实参是 nil 或没有传入时触发。false0、空字符串都不会触发 默认值。这一点不能用 Lua 的 or 直接模拟。

默认参数是 nil-only
fn visible(value = true) = value

visible(false)
visible(nil)

默认值按参数顺序处理,后面的默认值可以读取前面已经处理过的参数。反过来读取后面的 参数虽然按普通词法查找处理,但可读性很差,不建议这样写。

默认值按参数顺序求值
fn clamp(value, min = 0, max = min + 100) {
  value < min then min else value > max then max else value
}

GLua 方法声明

Lux 保留 GLua 惯用的点号和冒号方法声明,方便给 ENTSWEPPANEL 这类已有表 挂方法。

冒号方法
fn PANEL:Paint(w, h) {
  drawPanel(self, w, h);
}
点号函数字段
fn SWEP.PrimaryAttack() {
  fire(self);
}

这些声明是对已有表路径的赋值,不是模块私有绑定,也不会自动导出。普通模块 API 应 使用简单名字,再用 export 暴露。

隐式返回和分号

函数体、if 分支块和 do 表达式都使用同一套块值规则:最后一个没有分号的表达式 就是块的值。函数体走到这个尾表达式时,会返回它。

尾表达式返回
fn displayName(player) {
  local nick = player?:Nick() ?? "unknown"
  `[${player:SteamID64()}] ${nick}`
}

如果最后一行只是副作用,用分号压制隐式返回。GMod 的 PaintThink、输入回调 通常都应这样写,除非你明确要返回值。

分号压制隐式返回
fn PANEL:Think() {
  updateLayout(self);
}

显式 return 始终优先,适合 Lua 多返回值、提前返回和回调转发。

显式 return 保留多返回值
fn passthrough(...) {
  return ...
}

条件表达式

if { ... } else { ... } 可以作为表达式使用。每个分支的值来自分支块的尾表达式。

if 作为表达式
local label = if score > 10 {
  "high"
} else {
  "low"
}

短条件写成 cond then yes else no。它是语言级条件表达式,不是 Lua 的 and/or 技巧,所以 yesfalse 时也不会被错误替换成 no

短条件表达式
local visible = alpha > 0 then false else true

Guard 语句

stopif 用于提前返回。它的基本形式是:

stopif 基本形式
stopif 条件, 返回值

省略返回值时就是普通 returnstopifn 是反向形式,条件不成立时返回。

stopif 和 stopifn
fn playerName(player) {
  stopif player == nil, "unknown"
  stopifn player:IsValid(), "invalid"
  player:Nick()
}

循环内可以使用 continueifbreakif。这两个语句只在循环体内合法。

循环 guard
fn sumUntilNegative(values) {
  local total = 0

  for i = 1, #values {
    local value = values[i]
    continueif value == nil
    breakif value < 0
    total += value
  }

  total
}

箭头函数

=> 创建普通匿名函数,适合传给数组工具、hook 包装、一次性回调。

普通箭头函数
arr.map(players, (player, index) => `${index}: ${player:Nick()}`)

-> 创建带隐式 self 参数的匿名函数。它适合直接写 GLua 风格的面板回调。-> 内部的 self 是新参数,会遮蔽外层同名变量。

隐式 self 箭头函数
panel.Paint = (w, h) -> {
  drawPanel(self, w, h);
}

可选访问

Lux 提供三种直接段可选访问:

  • obj?.field:安全读取字段
  • obj?.fn(args):安全点号调用,不注入 self
  • obj?:method(args):安全冒号调用,会注入 self
  • tbl?.[key]:安全索引

可选访问只保护被标记的那一段,不会自动保护后续普通访问或调用。

安全冒号调用
local name = player?:Nick()

安全索引会先检查接收者,再求值索引表达式。因此 tbl?.[makeKey()]tblnil 时不会调用 makeKey()

安全索引延迟求值
local value = cache?.[makeKey()]

不要把 factory?.Make()() 误认为整条链都安全。它只保护 factory?.Make(), 最后的 () 仍是普通调用。

?? nil 合并

?? 只在左侧结果为 nil 时使用右侧。它不会把 false 当成空值。

nil 合并不能用 or 代替
local alive = player?:Alive() ?? false

?? 和比较运算混用时要写括号,避免语义不清。

?? 与比较运算显式分组
(player?:GetNWInt("exp") ?? 0) > 10

关系比较遇到可选链产生的 nil 会得到 false,不会对 nil 做大小比较。

可选链参与关系比较
if player?:GetNWInt("exp") > 10 {
  print("veteran")
}

解构绑定

对象解构读取字段,数组解构读取 Lua 的一基数组位置。默认值同样只在取到 nil 时 使用。

对象解构
const { name, hp, armor = 0 } = playerState
数组解构
local [x, y, z = 0] = point

右侧表达式只求值一次。解构不会自动保护 nil,需要接受空值时先兜底:

解构前先兜底
const { name } = maybePlayer ?? {}

表展开

表构造支持 ...table 展开。展开使用 pairs 拷贝键值,后面的字段覆盖前面的字段。 nil 展开源会被忽略。

表展开按顺序覆盖
local props = {
  ...defaults,
  ...overrides,
  visible = true,
}

表展开只复制可枚举键值,不复制 metatable。如果 metatable 有意义,需要手动处理。

复合赋值

Lux 支持 +=-=*=/=%=^=..=。复合赋值是语句,不是表达式。

简单复合赋值
state.count += 1
state.label ..= "!"

复杂左值只求值一次,避免 getPlayer().score += 1 这种写法重复调用左侧表达式。

复杂左值只求值一次
getPlayer().score += amount

管道

管道 |> 使用显式占位符 %。左侧值只求值一次,并替换右侧当前步骤里的 %。 Lux 不会隐式把左侧塞进第一个参数;右侧必须出现 %

显式占位符管道
players
  |> arr.filter(%, (player) => player:Alive())
  |> arr.map(%, (player, index) => `${index}. ${player:Nick()}`)

% 的作用域很浅,不会进入嵌套函数。下面写法是错误的:

管道占位符不会进入嵌套函数
xs |> arr.map(%, (x) => x + %)

需要在嵌套函数中读取管道值时,先把管道值绑定到局部变量。

do 表达式

do { ... } 可以出现在表达式位置,用来把一段局部计算收拢成一个值。

do 表达式
local next = do {
  local current = state.count ?? 0
  current + 1
}

没有尾表达式时,do 表达式的值是 nil。在语句位置,do { ... } 只是普通的 局部作用域。

模板字符串

反引号字符串支持 ${...} 插值。嵌入表达式会用 tostring 转成字符串,静态文本 原样保留。

模板字符串
local message = `HP ${hp}/${maxHp}`

如果要输出字面量 ${...},使用转义形式 \${...}

多返回值和可变参数

Lux 保留 Lua 多返回值语义。普通函数调用在 Lua 敏感位置会保留多返回值,例如 return f()、局部多重绑定、调用参数列表的最后一项、表数组项的最后一项。

多返回值转发
fn wrap(...) {
  log("call")
  return run(...)
}
局部多重绑定仍按 Lua 规则
local id, name = readPlayer()
call("player", readPlayer())

在普通表达式位置,调用结果仍像 Lua 一样折叠成单值。需要保留多个结果时,把它放在 Lua 本来就会展开的位置。