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 = 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

警告:以下内容仅供学术研究,在大多数情况下不推荐使用。

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` 不会保留 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

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

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

以下是我们如何创建一个代理表,该表可以读写局部变量并进行获取/设置。内部使用了 `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 的符号

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 )

技巧:不带引号的 require 模块

类似于滥用 `__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 中的 s(g)etfenv

在 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)

RecentChanges · preferences
编辑 · 历史
最后编辑于 2014 年 8 月 23 日凌晨 12:59 GMT (差异)