Lua 技巧 |
|
技巧可能不如 Lua 设计模式优雅、实用,也不如它们受推荐。然而,它们在学术价值上仍然很有趣。它们可能导致更优雅的解决方案,或者为其他问题提供有用的启发。
每个技巧都以一个名称标识,并包含对其尝试做什么以及如何做的描述。
有些示例使用了 getfenv 或 setfenv,这些函数在 Lua 5.1 后已被弃用。
以下示例使 Lua 表现得像 Perl 的 Exporter/import 用于将模块变量导出到调用者命名空间。
-- file test.lua local mymodule = require("mymodule") assert(hello == nil) -- not imported mymodule.hello() local mymodule = require("mymodule"):import{"hello"} hello() assert(goodbye == nil) -- not imported assert(mymodule.print == nil) -- not visible require("mymodule"):import{":all"} goodbye()
-- file mymodule.lua -- Globals get collected by this table. local collector = {} setfenv(1, setmetatable(collector, {__index = _G})) -- Now define the module functions. function hello() print("hello?") end function goodbye() print("goodbye?") end -- Used to import functions into caller namespace -- (like in Perl's "import" function in Exporter.pm) function import(self, option) -- convert any option list to set local optionset = {} for _,v in ipairs(option) do optionset[v] = true end -- import selected variables into caller namespace local env = getfenv(2) for k,v in pairs(collector) do if optionset[":all"] or optionset[k] then env[k] = v end end return self end -- Return public module object (don't expose globals) local mymodule = {} for k,v in pairs(collector) do mymodule[k] = v end return mymodule
-- output hello? hello? goodbye?
注意:更推荐使用 `mymodule = require "mymodule"; local hello = mymodule.hello` 之类的方法,它只将单个符号导入到您的命名空间(更清晰)。 `import{":all"}` 与 Java 的静态导入有类似的用途和缺点。
--DavidManura, 2006-10, Lua 5.1
警告:以下内容仅供学术研究,在大多数情况下不推荐使用。
在静态作用域流行之前,我们曾经有动态作用域([维基百科:作用域 (编程)])。让我们在 Lua 中模拟它。
为此,我们可以创建一个包装提供的函数的函数,以将动态作用域应用于某些变量。
-- (func, varnames*) --> func function dynamic(func, ...) local varnames = {...} local saves = {} return function(...) for _,varname in ipairs(varnames) do saves[varname] = _G[varname] end local result = pack2(func(...)) for _,varname in ipairs(varnames) do _G[varname] = saves[varname] end return unpack2(result) end end
上面的函数使用了“Vararg Saving”设计模式中的 `pack2` 和 `unpack2` 函数,该模式在 VarargTheSecondClassCitizen 中。`dynamic` 在函数调用之前和之后保存和恢复给定全局变量的值,从而模拟动态变量。以下是使用示例:
test2 = dynamic(function() print("test2:", x, y) x = 6 y = 7 return x, y end, 'y') test1 = dynamic(function() print("test1:", x, y) x = 4 y = 5 print("test2:", test2()) print("test1:", x, y) end, 'x') x = 2 y = 3 test1() -- Output: -- print("main:", x, y) -- test1: 2 3 -- test2: 4 5 -- test2: 6 7 -- test1: 6 5 -- main: 2 5
注意:支持 RAII([维基百科:资源获取即初始化])的语言可以使用 RAII 来实现此功能。可能有一些方法可以在 Lua 中模拟 RAII(LuaList:2006-09/msg00846.html)。
上面的代码不能正确处理引发异常的函数。这需要将 `pcall` 插入到 `dynamic` 中。
--DavidManura, 2007-01
警告:以下内容仅供学术研究,在大多数情况下不推荐使用。
在 Lua 中,如果您为未定义的变量赋值,则该变量将被创建为全局变量而不是局部变量。让我们改变这一点,使变量默认是局部的(就像 Python 一样 [2])。“局部”指的是最小词法作用域的意义,而不是 Lua 实现中的存储在 Lua 堆栈中的意义(在此,我们内部使用堆上的全局变量来实现局部变量)。
与之前的“动态作用域变量”设计模式一样,我们创建一个实用函数来包装我们希望应用此行为的其他函数,并利用 `pack2` 和 `unpack2` 函数。
function localbydefault(func) local upenv = getfenv(2) local mt = {}; mt.__index = upenv return function(...) local env = setmetatable({}, mt) -- storage for locals local oldenv = getfenv(func) setfenv(func, env) local result = pack2(func(...)) setfenv(func, oldenv) return unpack2(result) end end
这会导致为每个函数调用创建一个临时的局部变量环境。
Example
test2 = localbydefault(function() print("test2:", x, y) x = 6; y = 7 _G.z = 8 return x, y end) test1 = localbydefault(function() print("test1:", x, y) x = 4; y = 5 print("test1:", x, y) print("test2:", test2()) print("test1:", x, y) localbydefault(function() -- nested print("test3:", x, y) x = 9; y = 10 print("test3:", x, y) end)() print("test1:", x, y) end) x = 2 test1() print("main:", x, y, z) -- Output: -- test1: 2 nil -- test1: 4 5 -- test2: 2 nil -- test2: 6 7 -- test1: 4 5 -- test3: 4 5 -- test3: 9 10 -- test1: 4 5 -- main: 2 nil 8
请注意,可以通过 `_G` 变量访问全局变量。
这种方法也适用于递归函数。
fibonacci = localbydefault(function(n) if n == 1 then return 0 elseif n == 2 then return 1 else x1 = fibonacci(n - 1) x2 = fibonacci(n - 2) return x1 + x2 end end) assert(fibonacci(10) == 34)
上面的函数是用临时变量编写的,这些临时变量以一种会因其为全局变量而非局部变量而失败的方式编写(例如,尝试从该函数中删除 `localbydefault`)。然而,`x1` 和 `x2` 确实是词法作用域的局部变量,并且函数可以正常工作。
-- David Manura, 2007-01
//注释:pack2 和 unpack2 是什么?
警告:以下内容仅供学术研究,在大多数情况下不推荐使用。
C++ iostreams [3] 的用法如下:
#include <iostream>
using namespace std;
int main()
{
cout << "hello" << "world" << 123 << endl;
return 0;
}
我们可以在 Lua 中这样模仿它。
cout = function (str) if str ~= nil then io.write(tostring(str), " ") else io.write("\n") end return cout end
用法示例
cout "hello" "world" (123) () --> "hello world 123\n"
您甚至可以添加粘性格式化函数。
cout "asdf" (intfmt(3)) (i)
另请参阅 SimpleStringBuffer 以获取相关示例。
警告:以下内容仅供学术研究,在大多数情况下不推荐使用。
如 StringInterpolation 中所示,可以定义一个函数来访问调用者中词法作用域的变量。
local x = 3 assert(interp "x = ${x}" == "x = 3")
这是通过 `debug.getlocal` 实现的。
这可能还有另一个应用,可以消除在 ShortAnonymousFunctions 中将局部变量传递给“字符串化的匿名函数”的需要。
此示例在运行时修改字节码。可能有更多有用的技巧基于此。请注意,`string.dump` 不会保留 upvalue,这限制了它的有用性(但另请参阅 PlutoLibrary)。
function add(x,y) return x + y end function test(x,y) print("here is waht the answer is...") print("teh answer is", add(x,y)) end local replacements = { ["teh"] = "the", ["waht"] = "what" } function fix_spelling() local env = getfenv(2) for k,v in pairs(env) do if type(v) == "function" then local success, bytes = pcall(function() return string.dump(v) end) if success then local is_changed, n = false, nil for k,v in pairs(replacements) do bytes, n = bytes:gsub(k, v) is_changed = is_changed or (n > 0) end if is_changed then env[k] = assert(loadstring(bytes)) end end end end end fix_spelling() test(2,3)
$ lua test.lua here is what the answer is... the answer is 5
--DavidManura, 2007-03
警告:此示例仅供学术研究,不适用于生产环境。
以下是我们如何创建一个代理表,该表可以读写局部变量并进行获取/设置。内部使用了 `debug.getlocal` 和 `debug.setlocal` 调用,这也是它成为一个技巧的原因。
-- Returns a proxy table representing all locals visible to the -- given stack level <level>. The table is readable -- and writable (writing modifies the local). -- -- If <level> is nil, the default used is 1, -- which indicates the stack level of the caller. -- -- NOTE: This function is based on debug.getlocal() -- and may be slow. do local Nil = {} -- placeholder (for storing nils in tables) local function getstackdepth(level) local n = 1 while debug.getinfo(n+level+1, "") ~= nil do n=n+1 end return n end function getlocals(level) level = (level or 1) + 1 -- Note: this correctly handles the case where two locals have the -- same name: "local x=1; local x=2 ... get_locals() ... local x=3". local mt = {} local proxy = setmetatable({}, mt) local levels = {} -- map: variable name --> stack level local indicies = {} -- map: variable name --> stack index local depth = getstackdepth(level) for k=1,depth do -- Determine number of locals (nlocs) -- Note: it would be easier if debug.getinfo returned nlocs. local nlocs = 0 while debug.getlocal(level, nlocs+1) do nlocs = nlocs + 1 end -- Record locations of locals by name. for n=nlocs,1,-1 do local lname, lvalue = debug.getlocal(level, n) if lvalue == nil then lvalue = Nil end -- placeholder if not levels[lname] then -- not shadowed levels[lname] = level indicies[lname] = n end end level = level + 1 end -- proxy handlers for read/write on table. function mt.__index(t, k) local depthdiff = getstackdepth(2) - depth if depthdiff < 0 then error("stack out of scope", 2) end local level = levels[k] local v if level then level = level + depthdiff -- a correction local _; _, v = debug.getlocal(level, indicies[k]) if v == Nil then v = nil end end return v end function mt.__newindex(t, k, v) local depthdiff = getstackdepth(2) - depth if depthdiff < 0 then error("stack out of scope", 2) end local level = levels[k] if level then level = level + depthdiff -- a correction debug.setlocal(level, indicies[k], v) end end -- note: "stack out of scope" could be made more robust (see test suite) return proxy end end -- test suite function test() local function update(L) assert(L.x == 10) L.x = 20 end local L2 local x = 1 local y = 3 local z = 5 function f1() local x = 2 local y = nil local x = 4 local _L = getlocals() assert(_L.w == nil) assert(_L.x == 4) assert(_L.y == nil) assert(_L.z == 5) _L.z = 6 -- modify local through table! assert(z == 6 and _L.z == 6) z = 7 assert(z == 7 and _L.z == 7) _L.x = 10 assert(x == 10 and _L.x == 10) update(_L) assert(x == 20 and _L.x == 20) L2 = _L local x = 5 -- doesn't interfere end f1() -- Note: L2 is invalid at this scope. -- print(L2.x) -- will correctly raise an error -- L2.x = 1 -- will correctly raise an error -- update(L2) -- opps, this doesn't trigger "stack out of scope" print "done" end test()
-- 以上代码不是我写的,我是一个完全不同的人--
我真的觉得这个页面很有趣。我通读了这些,试图理解它们。我没有理解这个的标题或代码,所以我决定跳过它。现在我最近做了与这段代码完全一样的事情,但更短、更高效。
使用这段代码,`_L` 始终存在,无需反复用 `_L = getlocals()` 重新定义它。它也易于读写。理解起来更简单,不像上面的代码那么复杂。代码如下:
--Setup-- setlocal = function(lvl, name, val) local setlocal = saved if lvl ~= 0 then lvl = lvl + 1 end local i = 1 while true do local var, value = debug.getlocal(lvl, i) if not var then break end if var == name then debug.setlocal(lvl, i, val) end i = i + 1 end end --End of setup-- --Code-- setmetatable(_G, { __index = function(tab, var, val) if var == "_L" then local variables = {} local idx = 1 while true do local ln, lv = debug.getlocal(2, idx) if ln ~= nil then variables[ln] = lv else break end idx = 1 + idx end return setmetatable({}, { __index = variables, __newindex = function(tab, var, val) rawset(variables, var, val) setlocal(2, var, val) end }) end end }) --End of code-- --Tests-- local a = 1 print(a) _L.a = 5 print(a) print(_L.a) --End of tests--
我觉得我应该感谢 stackoverflow 上的 u0b34a0f6ae,他提供了 `setlocal` 函数的一部分,他在这里发布了它:"http://stackoverflow.com/questions/2834579/print-all-local-variables-accessible-to-the-current-scope-in-lua"(它被标记为最佳答案)。
我目前正在尝试为 `_L` 实现一个元表,如果您有任何关于如何做到这一点的想法,请在下面评论。
以下模拟了用户定义的控制结构和关键字,允许一定程度的元编程。(另请参阅 RuntimeSyntax)。
local Expr = {} local function ev(o) -- terminate and eval expression if getmetatable(o) == Expr then return o.END() else return o end end function Expr.__unm(a) return -ev(a) end function Expr.__len(a) return #ev(a) end function Expr.__add(a, b) return ev(a) + ev(b) end function Expr.__sub(a, b) return ev(a) - ev(b) end function Expr.__mul(a, b) return ev(a) * ev(b) end function Expr.__div(a, b) return ev(a) / ev(b) end function Expr.__pow(a, b) return ev(a) ^ ev(b) end function Expr.__concat(a,b) return ev(a) .. ev(b) end function Expr.__eq(a,b) return ev(a) == ev(b) end function Expr.__lt(a,b) return ev(a) < ev(b) end function Expr.__le(a,b) return ev(a) <= ev(b) end function Expr.__index(a,b) return ev(a)[ev(b)] end --function Expr.__newindex(a,b,c) ev(a)[ev(b)] = ev(c) end function Expr.__call(o, ...) if ... == ';' then return o.END() elseif o.__CALL then return o.__CALL(...) else return ev(a)(...) end end function Expr:clear() for k,v in pairs(self) do self[k] = nil end end local function eval(t) if type(t) == "function" or getmetatable(t) == Expr then return t() end local s = "" local ts = {} local vals = {} if type(t) == "table" then for k,v in pairs(t) do if type(k) ~= "number" then vals[#ts+1] = v ts[#ts+1] = k end end t = t[1] s = ((#ts > 0) and "local " .. table.concat(ts, ",") .. " = ...; " or "") end local s = s .. "return " .. t local f = loadstring(s) return f(unpack(vals, 1, #ts)) end --print(eval {[[x+y]], x = 2, y = 5}) function Expr.EVAL (expr) return eval(expr) end function Expr.IF (cond) local o = setmetatable({}, Expr) function o.THEN (stat) o:clear() function o.END () if (eval(cond)) then eval(stat) end end o.__CALL = o.END return o end; return o end function Expr.LET (name) local o = setmetatable({}, Expr) function o.EQUAL (expr) o:clear() function o.END () _G[name] = eval(expr) end function o.IN (expr2) o:clear(); function o.END() local oldval = _G[name] _G[name] = eval(expr) local function helper(...) _G[name] = oldval return ... end return helper(eval(expr2)) end; return o end; return o end o.__CALL = o.EQUAL return o end function Expr.FOR (var) local o = setmetatable({}, Expr) function o.FROM (i1) o:clear(); function o.TO (i2) o:clear(); function o.DO (expr) o:clear(); function o.END() for n=eval(i1),eval(i2) do _G[var] = n eval(expr) end end; return o end; return o end; return o end return o end Expr.__index = Expr setmetatable(_G, Expr) -- TEST LET 'x' .EQUAL '1' ';' LET 'y' .EQUAL '3' ';' IF 'x == 1' .THEN 'print(x+2)' ';' FOR 'n' .FROM '1' .TO '3' .DO ( IF 'n > 1' .THEN 'print(n,x)' ) ';' print(1 + (LET 'x' '2' .IN 'x*y') + 1) print(EVAL 'x') --[[OUTPUT: 3 2 1 3 1 8 1 --]]
-- DavidManura, 2007-07
Ruby 有一个叫做符号的功能,它通过特殊语法实现字符串驻留。在那里,通常的做法是将符号用作哈希的键。由于 Lua 默认已经进行了字符串驻留,因此这个技巧仅供学术研究。它通过滥用 `__index` 元方法来实现,该方法返回查询的键作为值。
S=setmetatable({},{__index=function(S,k) return k end}) print( S.Symbol_Name )
这个技巧更有用的表达方式是作为创建枚举表的替代方案。字符串被缓存以提高性能。此外,它还进行了格式化以提高可读性。
Enum = setmetatable( {}, { __index=function(E, k) E[k] = k return k end}) print( Enum.Apple, Enum.Banana, Enum.Carrot )
类似于滥用 `__index` 元方法来实现类似 Ruby 的符号,我们可以跳过在 require 时将模块名括在引号中的需要。这个技巧通过点表示法构建一个字符串,因为模块可能隐藏在目录树的深处。任意限制是目录结构假定第一个子目录。require 函数也需要被包装,因为它不会尝试对传入的表执行 `tostring`。
do local wrap_require = require require = function(t) wrap_require(tostring(t)) end local mt = {} function mt:__index(key) return setmetatable({path=rawget(self, "path").."."..key}, mt) end function mt:__tostring() return rawget(self, "path") or "" end lib = setmetatable({path="lib"},mt) end do require(lib.foo.bar)
在 Lua 5.2 中,setfenv 和 getfenv 都被弃用并删除。尽管许多人偏爱动态环境而不是词法环境。这样做的想法是环境不应该在它所在的函数之外访问。虽然本页面上的许多技巧都使用了它们。它们对于测试非常有用,但并不真正推荐在常规使用或作为程序的主要部分中使用,因为它使用了 debug 库,这会打破一些通用的 Lua 规则,并且可能很慢。
这些代码不是我写的,我在互联网上发现了它们,并认为它们应该放在一起。我认为它们都发布在 stackoverflow 上,快速搜索一下谷歌就会发现谁写的。代码如下:
--Setup function setfenv(f, t) f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) local name local up = 0 repeat up = up + 1 name = debug.getupvalue(f, up) until name == '_ENV' or name == nil if name then debug.upvaluejoin(f, up, function() return t end, 1) end end function getfenv(f) f = (type(f) == 'function' and f or debug.getinfo(f + 1, 'f').func) local name, val local up = 0 repeat up = up + 1 name, val = debug.getupvalue(f, up) until name == '_ENV' or name == nil return val end --Tests function x() print(hi) end x() --nil print(hi) --nil setfenv(x, {print = print, hi = 5}) x() --5 print(hi) --nil for i,v in pairs(getfenv(x)) do print(i, v) end --print function address --hi 5 setfenv(x, {}) x() --attempt to call global 'print' (a nil value)