Lua 技巧

lua-users home
wiki

此页面列出了 Lua 技巧。我们使用技巧一词,其含义来自维基百科 [1]

在现代计算机编程中,“技巧”可以指一种正确运作但“丑陋”的解决方案或方法,它在环境的既定结构和规范之外工作,或者难以扩展或维护(参见 kludge)……。类似地,“技巧”也可以指计算机编程之外的工作。例如,数学技巧指的是解决数学问题的巧妙方法。

技巧可能不如 Lua设计模式 优雅、实用和推荐。但是,它们仍然具有学术价值。它们可能导致更优雅的解决方案,或为其他问题提供有用的启发。

每个技巧都用一个名称标识,并包含对它试图做什么以及如何做的描述。

一些示例使用 getfenv 或 setfenv,这些函数在 Lua 5.1 之后被弃用。

其他页面上的技巧

技巧:Perl 风格的模块

以下示例使 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 = requre "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

上面的函数使用了 VarargTheSecondClassCitizen 中“Vararg 保存”设计模式的 pack2unpack2 函数。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)。

上面的代码没有正确处理引发异常的函数。这将需要在 dynamic 中插入一个 pcall

--DavidManura,2007-01

模式:默认局部作用域

警告:以下内容属于学术性内容,不建议在大多数情况下使用。

在 Lua 中,如果你给一个未定义的变量赋值,该变量将被创建为全局变量而不是局部变量。让我们改变这一点,使变量默认情况下为局部变量(就像 Python 中一样 [2])。“局部”是指在最小的词法作用域中,而不是在 Lua 实现意义上的存储在 Lua 堆栈中(在这里,我们使用堆上的全局变量在内部实现局部变量)。

就像之前的“动态作用域变量”设计模式一样,我们创建了一个实用函数来包装我们想要为其分配此行为的其他函数,并使用 pack2unpack2 函数

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

这会导致为每个函数调用创建一个用于局部变量的临时环境。

示例

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)。但是,x1x2 确实是词法作用域的局部变量,并且该函数有效。

-- David Manura,2007-01

//注释:什么是 pack2 和 unpack2

技巧:模拟 C++ Iostreams

警告:以下内容属于学术性内容,不建议在大多数情况下使用。

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 中传递局部变量到“字符串化匿名函数”的需要。

--DavidManura

技巧:修改字节码

此示例在运行时修改字节码。可能会有更多有用的技巧基于它。请注意,string.dump 不会保留上值,这限制了此方法的实用性(但另请参阅 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

技巧:局部变量的代理表,_L

警告:此示例仅供学术研究,不适合生产环境使用。

以下是我们如何创建一个代理表,该表可以对局部变量进行读写操作,并进行 get/set 操作。在内部,它使用 debug.getlocaldebug.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 的符号

Ruby 具有名为 Symbols 的功能,它使用特殊语法实现字符串驻留。在 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 )

技巧:无需引号加载模块

类似于滥用 Ruby 风格符号的 __index 元方法,我们可以跳过在加载模块时用引号括住模块名称的步骤。这个技巧通过点符号构建字符串,因为模块可能隐藏在目录树的深处。任意限制是目录结构假设第一个子目录。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 中的 s(g)etfenv

在 Lua 5.2 中,setfenv 和 getfenv 都被弃用并删除了。虽然许多人更喜欢动态环境而不是词法环境。这个想法是环境不应该在它的函数之外被访问。尽管本页上的许多技巧都使用了它们。它们非常适合测试目的,但并不推荐在常规使用中使用,也不推荐作为程序的主要部分,因为它使用了调试库,这违反了一些通用的 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)

最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2014 年 8 月 23 日上午 6:59 GMT (差异)