《Lua程序设计第四版》 第三部分22~25章自做练习题答案

发布时间 2023-08-20 17:23:17作者: 小能日记

Lua程序设计第四版第三部分编程实操自做练习题答案,带⭐为重点。

22.1

本章开始时定义的函数getfield,由于可以接收像math?sin或string!!!gsub这样的字段而不够严谨。请将其进行重写,使得该函数只能支持点作为名称分隔符。

function getfield(f)
    if string.find(f, "[^%w%._]") then
        error("not '.' sep", 2)
    end
    local G = _G
    for v in string.gmatch(f, "[%a_][%w_]*") do
        G = G[v]
    end
    return G
end

read = getfield("io.read")
print(read)
sin = getfield("math?sin")
gsub = getfield("string!!!gsub")

22.2 ⭐

请解释下列程序做了什么,以及输出结果是什么

local foo -- 声明局部变量foo
do
    local _ENV = _ENV -- 在do...end范围内生效的新环境,内部环境=外部环境
    function foo() -- 局部变量foo被赋值了函数
        print(X) -- 内部_ENV.X,也是外部_ENV.X
    end
end
X = 13 -- 外部ENV.X 改为 13
_ENV = nil -- 外部_ENV变为nil
foo() -- 调用了局部变量foo函数,打印内部_ENV.X,因为内部_ENV依旧引用着原先的外部_ENV表,所以打印13
X = 0 -- 修改外部_ENV.X 因为外部ENV此时为nil,故发生错误

-- 13
-- 22.2.lua:11: attempt to index a nil value (upvalue '_ENV')

22.3 ⭐

请详细解释下列程序做了什么,以及输出的结果是什么

local print = print -- 局部变量print赋值了_ENV.print
function foo(_ENV, a) -- _ENV.foo 被赋值了函数,函数体根据传入的_ENV环境参数执行
    print(a + b) -- a是函数定义里的形参,b是传入环境 _ENV.b
end

foo({ -- 调用 _ENV.foo
    b = 14 -- 传入了一个新环境
}, 12)
foo({
    b = 10 -- 传入了一个新环境
}, 1)

-- 26
-- 11

23.1 ⭐

请编写一个实验证明为什么Lua语言需要实现瞬表(记得调用函数collectgarbage来强制进行一次垃圾收集)

一个具有弱引用键和强引用值的表叫瞬表

collectgarbage() -- 清理之前dofile占用的内存空间
n = collectgarbage("count") * 1024
print(n)
local memory = {}
for i = 1, 2 ^ 10, 1 do
    local t = {i}
    memory[t] = function()
        t[1] = t[1] + 1 -- 间接引用了键
        return t
    end
end
-- 假如没有瞬表
collectgarbage()
n = collectgarbage("count") * 1024
print(n)
-- 假如有瞬表了
mt = {}
mt.__mode = "k"
setmetatable(memory, mt)
collectgarbage()
n = collectgarbage("count") * 1024
print(n)
-- 再清空 memory
memory = nil
collectgarbage()
n = collectgarbage("count") * 1024
print(n)

23.2

书上23.6节的第一个示例创建了一个带有析构器的表,该析构器在执行时只是输出一条消息。如果程序没有进行过垃圾收集就退出会发生什么?如果程序调用了os.exit呢?如果程序由于出错而退出呢?

o = {
    x = "hi"
}
setmetatable(o, {
    __gc = function(o)
        print(o.x)
    end
})
o = nil
-- error("123", 123)
-- os.exit()
collectgarbage()
  • 如果一个对象直到程序运行结束后还没有被回收,那么Lua语言就会在整个Lua虚拟机关闭后调用它的析构器
  • 调用os.exit() 不会调用未回收对象的析构器,直接退出了程序
  • 因为出错而退出程序,会在退出时调用未回收对象的析构器

23.3 ⭐

假设要实现一个记忆表,该记忆表所针对函数的参数和返回值都是字符串。由于弱引用表不把字符串当做可回收对象,因此将这个记忆表标记为弱引用并不能使得其中的键值对能够被垃圾收集。在这种情况下,你该如何实现记忆呢?

给字符串值套上一层表,变成只有一个元素的序列对象

memory = {}
setmetatable(memory, {
    __mode = "v",
    __len = function(t)
        local count = 0
        for k, v in pairs(t) do
            count = count + 1
        end
        return count
    end
})
collectgarbage()

function myMemory(k, v)
    local tmp = memory[k]
    if tmp == nil then
        memory[k] = {v or k}
    end
    return memory[k][1]
end

myMemory("a", "a")
myMemory("b", "b")
myMemory("c", "c")
v = myMemory "b"
print(v)
print(#memory)
collectgarbage()
print(#memory)

23.4 ⭐

解释示例的程序的输出

local count = 0

local mt = {
    __gc = function()
        count = count - 1
    end
}
local a = {}

for i = 1, 10000 do
    count = count + 1
    a[i] = setmetatable({}, mt)
end

collectgarbage()
print(collectgarbage("count") * 1024, count) -- 多出来的内存,10000
a = nil
collectgarbage()
print(collectgarbage("count") * 1024, count) -- 删除键的对象内存,执行值的对象的析构方法,0
collectgarbage()
print(collectgarbage("count") * 1024, count) -- 删除已被析构的值的对象内存,0
--[[
844706.0        10000
582570.0        0
22570.0 0
--]]

23.5

你需要至少一个使用很多内存的Lua脚本

  • 使用不同的pause和stepmul运行脚本。看看对性能的影响
  • 调整脚本,使其能够完整地控制垃圾收集器。脚本应该让垃圾收集器停止运行,然后时不时地完成垃圾收集工作。
-- collectgarbage("setpause", 0)
-- collectgarbage("setpause", 1000)
-- collectgarbage("setstepmul", 1)
-- collectgarbage("setstepmul", 1000000)
start = os.clock()
local a = {}
setmetatable(a, {
    __mode = "v"
})
collectgarbage("stop")
for i = 1, 2 ^ 26, 1 do
    a[i] = {}
    if i % 2 ^ 12 == 0 then -- or use os.time
        collectgarbage()
    end
end
print(os.clock() - start)
--[[
默认 3.723
pause=0  4.404
pause=1000  3.571
stepmul=1 4.85
stepmul=1000000 3.972

默认+i % 2 ^ 13 == 0 4.385
自定义 4.559

--]]

协程传递值 ⭐

function consumer()
    while true do
        print(coroutine.yield())
    end
end

consumer = coroutine.create(consumer)

coroutine.resume(consumer, 1) -- 第一次唤醒,为consumer传入形参,运行到yield阻塞并返回,等待一个对resume的再次调用
coroutine.resume(consumer, 2) -- 第二次唤醒,yield函数结束,返回第二次resume调用的值
coroutine.resume(consumer, 3)

--[[
  2
  3
]]

协程全排列

function permgen(a, n)
    n = n or #a
    if n <= 1 then -- 迭代终止条件(全排列的某一种结果)
        -- printResult(a)
        coroutine.yield(a)
    else
        for i = 1, n, 1 do
            a[i], a[n] = a[n], a[i]
            permgen(a, n - 1)
            a[i], a[n] = a[n], a[i]
        end
    end
end

function printResult(a)
    for i = 1, #a do
        io.write(a[i], " ")
    end
    io.write("\n")
end

function permutations1(a)
    local thread = coroutine.create(function() -- 需要一个函数而不是一个函数的调用
        permgen(a)  -- 闭包
    end)
    return function()
        local code, res = coroutine.resume(thread)
        return res
    end
end

function permutations2(a)
    local thread = coroutine.create(permgen)
    return function()
        local code, res = coroutine.resume(thread, a) -- 第一次唤醒协程传入参数a,闭包
        return res
    end
end

function permutations3(a)
    return coroutine.wrap(function()
        permgen(a)
    end)
end

for v in permutations1({1, 2, 3}) do
    printResult(v)
end

for v in permutations2({1, 2, 3}) do
    printResult(v)
end

for v in permutations3({1, 2, 3}) do
    printResult(v)
end

24.1

使用生产者驱动式设计重写生产者-消费者的示例,其中消费者是协程,而生产者是主线程

io.input("test.txt")

function producer()
    while true do
        local x = io.read(1)
        if not x then
            return
        end
        send(x)
    end
end

function consumer(x)
    while true do
        io.write(x, "\n")
        x = receive()
    end
end

consumer = coroutine.create(consumer)

function send(x)
    coroutine.resume(consumer, x)
end

function receive()
    return coroutine.yield()
end

producer()

24.2 ⭐

练习6.5要求编写一个函数来输出指定数组元素的所有组合。请使用协程把该函数修改为组合的生成器。该生成器的用法如下

for c in combinations({"a","b","c"}, 2) do
   printResult(c)
end
-- ^ 递归里不要对传入的实参做手脚,即函数形参t
function comb(list, n, res, index)
    res = res or {}
    index = index or 1
    if #res == n then
        -- io.write "{"
        -- io.write(table.concat(res, ","))
        -- io.write "}\n"
        coroutine.yield(res)
        return
    end
    if n - #res > #list or index > #list then
        return
    end
    -- 选择第index个
    res[#res + 1] = list[index]
    comb(list, n, res, index + 1)
    res[#res] = nil
    -- 不选第index个
    comb(list, n, res, index + 1)
end

function combinations(list, n)
    c = coroutine.wrap(comb)
    return function(s, c)
        return c(list, n)
    end, nil, nil
end

for c in combinations({1, 2, 3}, 2) do
    io.write "{"
    io.write(table.concat(c, ","))
    io.write "}\n"
end

24.3

在示例24.5中,函数getline和putline每一次调用都会产生一个新的闭包。请使用记忆机制来避免这种资源浪费。

local lib = require 'async-lib'

function run(code)
    local co = coroutine.wrap(function()
        code()
        lib.stop()
    end)
    co()
    lib.runloop()
end

local putlineMemory = {}
function putline(stream, line)
    local co = coroutine.running()
    local callback = putlineMemory[co]
    if not callback then
        callback = (function()
            coroutine.resume(co)
        end)
        putlineMemory[co] = callback
    end
    lib.writeline(stream, line, callback)
    coroutine.yield()
end

local getlineMemory = {}
function getline(stream, line)
    local co = coroutine.running()
    local callback = getlineMemory[co]
    if not callback then
        callback = (function(l)
            coroutine.resume(co, l)
        end)
        getlineMemory[co] = callback
    end
    lib.readline(stream, callback)
    local line = coroutine.yield()
    return line
end

run(function()
    local t = {}
    local inp = io.input("test.js")
    local out = io.output()

    while true do
        local line = getline(inp)
        if not line then
            break
        end
        t[#t + 1] = line
    end
    for i = #t, 1, -1 do
        putline(out, t[i] .. "\n")
    end
end)

24.4

请为基于协程的库(示例24.5)编写一个行迭代器,以便于使用for循环来读取一个文件

local lib = require 'async-lib'

function run(code)
    local co = coroutine.wrap(function()
        code()
        lib.stop()
    end)
    co()
    lib.runloop()
end

local putlineMemory = {}
function putline(stream, line)
    local co = coroutine.running()
    local callback = putlineMemory[co]
    if not callback then
        callback = (function()
            coroutine.resume(co)
        end)
        putlineMemory[co] = callback
    end
    lib.writeline(stream, line, callback)
    coroutine.yield()
end

local getlineMemory = {}
function getline(stream, line)
    local co = coroutine.running()
    local callback = getlineMemory[co]
    if not callback then
        callback = (function(l)
            coroutine.resume(co, l)
        end)
        getlineMemory[co] = callback
    end

    return function()
        lib.readline(stream, callback)
        local line = coroutine.yield()
        return line
    end, nil, nil
end

run(function()
    local t = {}
    local inp = io.input("test.js")
    local out = io.output()

    for line in getline(inp) do
        t[#t + 1] = line
    end

    for i = #t, 1, -1 do
        putline(out, t[i] .. "\n")
    end
end)

24.6 ⭐ ⭐

用Lua实现一个transfer函数。如果你觉得唤醒-挂起和调用-返回类似,那么transfer就类似goto:它会挂起当前运行的协程

然后唤醒其他作为参数的协程(提示:使用某种调度机制来控制协程。这样的话,transfer会把控制权转交给调度器以通知下一个协程的运行,调度器就会唤醒下一个协程)

image-20230820004240299

a协程想切换到b协程,a内部调用transfer,调用yield返回唤醒a的dispatch的resume调用,resume调用返回接收到a协程调用yield传递过来的callback,然后调用callback唤醒b协程。

local coroutines = {}

function transfer(name)
    local callback = function()
        return coroutine.resume(coroutines[name])
    end
    coroutine.yield(callback)
end

-- 注册三个线程

coroutines["print a"] = coroutine.create(function()
    while true do
        print("a")
        transfer("print b")
    end
end)
coroutines["print b"] = coroutine.create(function()
    while true do
        print("b")
        transfer("print c")
    end
end)
coroutines["print c"] = coroutine.create(function()
    while true do
        print("c")
        transfer("print a")
    end
end)

-- 调度器
dispatch = coroutine.create(function()
    local callback = nil
    while true do
        if callback == nil then
            _, callback = coroutine.resume(coroutines["print a"]) -- 起始线程 
        end
        _, callback = callback()
    end
end)

coroutine.resume(main)

25.1

改进getvarvalue,使之能处理不同的协程

function co_getvarvalue(co, name, level, isenv)
    if type(co) ~= "thread" then
        co = coroutine.running()
    end
    local value
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(co, level, i)
        if not n then
            break
        end
        if n == name then
            value = v
            found = true
        end
    end
    if found then
        return "local", value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(co, level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            return "upvalue", v
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local _, env = co_getvarvalue(co, "_ENV", level, true)
    if env[name] then
        return "global", env[name]
    else
        return "noenv"
    end
end

local val1 = 1
local co = coroutine.wrap(function()
    local val2 = val1
    print(co_getvarvalue(nil, "val1", 1))
    print(co_getvarvalue(nil, "val2", 1))
end)
co()
print(co_getvarvalue(nil, "val1", 1))
print(co_getvarvalue(nil, "val2", 1))

_ENV

print(_G._G)
do
    _ENV = {
        print = _G.print -- _G是外层_G的_G
    }
    a = 1
    print(a)
    do
        b = 2
        print(b)
        do
            _ENV = {
                print = print --  如果_G.print的_G是外层_ENV的_G,所以结果是nil
            }
            c = 3
            print(c)
            do
                d = 4
                print(4)
            end
        end
    end
end

25.2 ⭐

请编写一个函数与getvarvalue类似的setvarvalue

function getvarvalue(name, level, isenv)
    local value
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            value = v
            found = true
        end
    end
    if found then
        return "local", value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            return "upvalue", v
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local _, env = getvarvalue("_ENV", level, true)
    if env[name] ~= nil then
        return "global", env[name]
    else
        return "noenv"
    end
end

function setvarvalue(name, value, level, isenv)
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            debug.setlocal(level, i, value)
            found = true
        end
    end
    if found then
        return "local change\t" .. name .. "=>" .. value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            debug.setupvalue(func, i, value)
            return "upvalue change\t" .. name .. "=>" .. value
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local _, env = getvarvalue("_ENV", level, true)
    if env[name] then
        env[name] = value
        return "global change\t" .. name .. "=>" .. value
    else
        return "noenv"
    end
end

a = "xx"
-- local a = 4;
print(getvarvalue("a"))
setvarvalue("a", 8)
print(getvarvalue("a"))

25.3

请编写函数getvarvalue的另一个版本,该函数返回一个包括调用函数可见的所有变量的表(返回的表中不应该包括环境中的变量,而应该从原来的环境中继承这些变量)

function getvarvalue(level, isenv)
    local res = {}
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        res[n] = v
    end
    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == "_ENV" then -- 发现环境变量上值
            print("发现_ENV")
            mt = {}
            mt.__index = _ENV -- 继承
            setmetatable(res, mt)
            goto continue
        end
        res[n] = v
        ::continue::
    end
    return res
end

a = "1"
b = "2"
local c = 3
function test()
    local d = 4
    local e = c -- c是闭包用到的上值
    res = getvarvalue()
    for k, v in pairs(res) do
        print(k, v)
    end
    print("从继承的_ENV中寻找b", res["b"])
end
test()

25.4

请编写一个函数debug.debug的改进版,该函数再调用debug.debug的函数的词法定界中运行指定的命令(提示:在一个空环境中运行命令,并使用__index元方法让函数getvarvalue进行所有的变量访问)

a = {1, 2, 3}
debug.debug() -- 原版debug访问并修改了debug函数外的变量
print(#a)

--[[
> dofile 'test9.lua' 
lua_debug> a = {1,2}
lua_debug> cont      
2
]]

把 load 每次编译的代码理解为一次 do end 那么 local 也就只在当前代码段有作用,需要保存的变量要以非局部变量的方式保存到环境中。

function getvarvalue(name, level, isenv)
    local value
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            value = v
            found = true
        end
    end
    if found then
        return value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            return v
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local env = getvarvalue("_ENV", level, true)
    if env[name] ~= nil then
        return env[name]
    else
        return "noenv"
    end
end

function mydebug()
    myENV = {
        print = print
    }
    mt = {}
    mt.__index = function(table, name)
        return getvarvalue(name, 1) -- 会截取第一个参数
    end
    setmetatable(myENV, mt)
    while true do
        io.write("debug > ")
        line = io.read()
        if line == "cont" then
            break
        end
        assert(load(line, nil, "bt", myENV))()
    end
end

hello = "1234"
mydebug()

--[[
> dofile '25.4.lua'   
debug > print(hello) 
1234
]]

想保留值的类型提示,可以改成返回表。

function getvarvalue(name, level, isenv)
    local value
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            value = v
            found = true
        end
    end
    if found then
        return {"local", value}
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            return {"upvalue", v}
        end
    end

    if isenv then
        return {"noenv", nil}
    end

    -- 寻找环境变量
    local env = getvarvalue("_ENV", level, true)[2]
    if env[name] ~= nil then
        return {"global", env[name]}
    else
        return {"noenv", nil}
    end
end

function mydebug()
    myENV = {
        print = print
    }
    mt = {}
    mt.__index = function(table, name)
        return getvarvalue(name, 2)[2]
    end
    setmetatable(myENV, mt)
    print(myENV.hello)
end

hello = "1234"
mydebug()

25.5 ⭐

改进上例,使之也能处理更新操作

function getvarvalue(name, level, isenv)
    local value
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            value = v
            found = true
        end
    end
    if found then
        return value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            return v
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local env = getvarvalue("_ENV", level, true)
    if env[name] ~= nil then
        return env[name]
    else
        return "noenv"
    end
end

function setvarvalue(name, value, level, isenv)
    local found = false

    level = (level or 1) + 1

    -- 寻找局部变量
    for i = 1, math.huge do
        local n, v = debug.getlocal(level, i)
        if not n then
            break
        end
        if n == name then
            debug.setlocal(level, i, value)
            found = true
        end
    end
    if found then
        return "local change\t" .. name .. "=>" .. value
    end

    -- 寻找非局部变量
    local func = debug.getinfo(level, "f").func
    for i = 1, math.huge do
        local n, v = debug.getupvalue(func, i)
        if not n then
            break
        end
        if n == name then
            debug.setupvalue(func, i, value)
            return "upvalue change\t" .. name .. "=>" .. value
        end
    end

    if isenv then
        return "noenv"
    end

    -- 寻找环境变量
    local env = getvarvalue("_ENV", level, true)
    if env[name] then
        env[name] = value
        return "global change\t" .. name .. "=>" .. value
    else
        return "noenv"
    end
end

function mydebug()
    myENV = {
        print = print
    }
    mt = {}
    mt.__index = function(table, name)
        return getvarvalue(name, 2) -- 会截取第一个参数
    end
    mt.__newindex = function(table, name, value)
        setvarvalue(name, value, 2)
    end
    setmetatable(myENV, mt)
    while true do
        io.write("debug > ")
        line = io.read()
        if line == "cont" then
            break
        end
        assert(load(line, nil, "bt", myENV))()
    end
end

hello = "1234"
mydebug()

--[[
> dofile '25.5.lua'
debug > print(hello)      
1234
debug > hello = "2345"      
debug > print(hello)   
2345
]]

25.6

实现25.3节中开发的基本性能调优工具中的一些建议的改进

local Counters = {}
local Names = {}

local function hook()
    local f = debug.getinfo(2, "f").func
    local count = Counters[f]
    if count == nil then
        Counters[f] = 1
        Names[f] = debug.getinfo(2, "Sn")
    else
        Counters[f] = count + 1
    end
end

-- 排序
a = function()
    print(1)
end
b = function()
    print(1)
end
c = function()
    print(1)
end
Counters[a] = 300
Counters[b] = 100
Counters[c] = 200
Names[a] = "print a"
Names[b] = "print b"
Names[c] = "print c"
s = {}
for f, name in pairs(Names) do
    s[#s + 1] = f
end
table.sort(s, function(a, b)
    return Counters[a] > Counters[b]
end)
for _, f in ipairs(s) do
    print(Names[f])
end

25.8

示例25.6中沙盒的问题之一在于沙盒中的代码不能调用其自身的函数。请问如何纠正这个问题。

思路很简单,沙盒提供一个函数用于注册自己创建的函数,运行自己创建的函数也需要在沙盒环境的限制下运行