笔记 - Lua 程序设计(第 2 版) Ch04~06 语句、函数、深入函数

04 语句

赋值

-- Lua 允许多重赋值
a, b = 10, 2 * 8

-- 并且先对等号右边所有元素求值后再赋值
-- swap可以这样实现
a, b = b, a

-- 如果右边个数多于左边,多出来的值会被丢弃
a, b = 10, 2 * 8, 133, 42
-- 如果右边个数少于左边,左边多余的变量会被赋为nil
a, b = 1

一般很少会为几个没有关联的变量使用多重赋值。有时会用到:

  • 交换两个变量
  • 收集函数的多个返回值

局部变量与块

j = 10		-- 全局变量
local i = 1	-- 局部变量

局部变量的作用域仅限于声明它们的块

如果需要严格控制局部变量的作用域,使用 do 块

a = 10
do
  local a = 2;
  a = a + 1
end
print(a)

一种习惯写法:

local foo = foo

创建了局部变量 foo,用全局变量 foo 的值初始化它。作用:

  • 防止全局变量的值被改变
  • 加速在当前作用域对 foo 的访问

控制结构

if

if … then … end

if … then … else … end

if … then … elseif … then… elseif … then … else … end

while

while … do … end

repeat

repeat … until …

类似 C 中的 do while。注意,在 repeat 块中的局部变量,在 until 条件中依然可见。

数字 for

-- i从2变化到5(闭区间),步长为2
-- 步长可选,若不指定,默认为1
for i = 2, 5, 1 do
  print(i)
end

-- 如果不想设置上限
for i = 1, math.huge do
  print(i)
end

for 的三个表达式在循环开始前一次性求值

循环变量自动声明为局部变量

不要在循环过程中修改控制变量的值

泛型 for

通过迭代器函数来遍历

a = {"A", "B", "C"}
-- 基础库提供的迭代器函数
-- ipairs迭代数组
for key, val in ipairs(a) do
  print(key, val)
end
-- pairs迭代table
for val in pairs(a) do
  print(val)
end
-- io,lines迭代文件中每行
-- string.gmatch迭代字符串中单词

循环变量自动声明为局部变量

不要在循环过程中修改控制变量的值

例:构造逆向 table

days = {"Sun", "Mon", "Tue", 
  "Wed", "Thu", "Fri", "Sat"}
revdays = {}
for key, val in pairs(days) do
  revdays[val] = key
end
for key, val in pairs(revdays) do
  print(key, val)
end

break 和 return

用于跳出当前的块

如果函数没有 return,结尾处会有隐式的 return

可以这样在调试的时候忽略 return

function foo ()
    do return end	-- 这样return就失去本来作用
    -- ...
end

05 函数

函数调用都要将参数放入圆括号,没有参数也要写空括号。以下两种例外可以没有圆括号:

  • 仅有一个字面字符串参数
  • 仅有一个 table 构造式

为面向对象的调用提供了冒号操作符,详见 16 章

实参数量会被自动调整,匹配参数表的要求,调整和多重赋值一样。

多重返回值

b, e = string.find("Hello my friend", "my");
print(string.sub("Hello my friend", b, e))	-->my

只需在编写函数的时候,在 return 后面用逗号分隔,列出所有的返回值即可。

同样类似于多重赋值,Lua 会调整返回值数量来适应不同的调用情况。详见 P37

如果把函数调用放入一对圆括号,将迫使它只返回一个结果

通过 unpack 函数来把数组中下标 1 开始的所有元素作为函数参数

f = string.find
a = {"hello", "ll"}
print(f(unpack(a)))

变长参数

-- ...表示函数接受不同数量的实参
function add(...)
  local s = 0
  for i, v in ipairs{...} do
    s = s + v
  end
  return s
end

print(add(3,4,5,6,7))

跟踪特定函数的技巧:

function add(...)
  local s = 0
  for i, v in ipairs{...} do
    s = s + v
  end
  return s
end

-- 用于跟踪add,每次调用前先显示函数名及参数
function tracingAdd(...)
  print("calling add:", ...)
  return add(...)
end

print(tracingAdd(3,4,5,6,7))

结合格式化文本和输出文本两个函数:

function fwrite (fmt, ...)
  return io.write(string.format(fmt, ...))
end

fwrite("%d%d\n", 4, 5)

使用 select 来遍历所有变长参数(包括 nil)

select 的第一个参数为 selector,如果是数字 n,返回第 n 个可变实参,否则只能是字符串 “#”,返回变长参数的总数(包括 nil)

for i = 1, select("#", ...) do
  local arg = select(i, ...)
  print(arg)
end

具名实参

通过把所有实参组织到一个 table 中,并将 table 作为唯一的实参。这样就可以用 table 构造式来代替圆括号。

function Window(options)
  -- 检查必要参数
  if type(options.title) ~= "string" then
    error("no title")
  elseif type(options.width) ~= "number" then
    error("no width")
  elseif type(options.height) ~= "number" then
    error("no height")
  end
  -- 其他参数都是可选的
  -- _Window才是真正创建窗口的函数
  _Window(
    options.title,
    options.x or 0
    options.y or 0
    options.width,
    options.height,
    options.background or "white"
    options.border
    )
end

自己写的例子:

function addABC(numbers)
  -- 检查必要参数
  if type(numbers.a) ~= "number" then
    error("a is not a number")
  elseif type(numbers.b) ~= "number" then
    error("b is not a number")
  elseif type(numbers.c) ~= "number" then
    error("c is not a number")
  end
  return _addABC(numbers.a, numbers.b, numbers.c)
end

function _addABC(a, b, c)
  return a + b + c
end

print(addABC{a = 2, b = 3, c = 4})

06 深入函数

函数与所有其他值一样都是匿名的,一个函数名实际上是一个持有某函数的变量。

最常见的函数编写方式只是一种语法糖:

--[[
function add(a, b)
  return a + b
end
--]]
-- 上下两种写法完全等价
add = function(a, b) return a + b end

类似 cpp 中的 sort,Lua 的 table.sort 接受一个 table 并对其排序,第二个参数是一个次序函数,并且是可选的。该函数接受两个元素,并返回第一个元素是否应该排在第二个元素前面。(cpp 中 sort 的 compare 函数本质上也是这样)

接受另一个函数作为实参的,称其是一个高阶函数(只是一种定义,高阶函数并没有什么特权)

closure(闭合函数)

一个 closure 就是一个函数加上该函数所需访问的所有 “非局部的变量”

Lua 只有 closure,不存在“函数”,函数本身是一种特殊的 closure(恰巧没有 upvalue)

function newCounter()
  local i = 0
  return function ()
    -- 这里的i就是一个upvalue
    -- 可以理解为捕获了外面的i
    -- 在这里对i的修改会影响到外面的i
    i = i + 1
    return i
  end
end

-- c1是匿名函数
c1 = newCounter()
print(c1()) -->1
print(c1()) -->2

c2 = newCounter()
print(c2()) -->1
print(c1()) -->3
print(c2()) -->2

重新定义预定义的函数:

do
  -- 把预定义的print保存在局部变量中
  -- 用do块封装
  local oldPrint = print
  print = function(...)
    oldPrint(...)
    oldPrint("my new print")
  end
end
-- 在do块外面就只能使用新定义的函数了
print(4)

创建安全的运行环境(沙盒),详见 P49

non-global function

只要将一个函数存储到一个局部变量中,就得到了一个局部函数。

注意递归的局部函数的写法,需要预定义函数

--[[ 错误示范
local fact = function(n)
  if n == 0 then
    return 1
  else
    return n * fact(n - 1)
  end
end
--]]

local fact -- 预定义即可
fact = function(n)
  if n == 0 then
    return 1
  else
    return n * fact(n - 1)
  end
end

-- 或者使用语法糖
local function fact(n)
  if n == 0 then
    return 1
  else
    return n * fact(n - 1)
  end
end

正确的尾调用

尾调用:一个函数调用是另一个函数的最后一个动作

Lua 解释器在进行尾调用时不耗费任何栈空间

-- 不管n有多大,都不会栈溢出
foo = function(n)
  print(n)
  if n > 0 then
    return foo(n-1)
  end
end

但是要确保调用是“正确的尾调用”

-- 以下情况都不是正确的尾调用
return g(x) + 1
return x or g(x)
return (g(x))
-- 只有以下是正确的尾调用
return <func>(<args>)

尾调用好比是一条 goto 语句

P53 展示了一个用尾调用来实现状态机的例子,无论状态机中的状态转移多少次也不会造成栈溢出

你可能感兴趣的:(Lua,程序设计,lua)