不可变对象

lua-users home
wiki

本页面讨论 Lua 中关于不可变性 [6] / const 性 [4] 的主题。

概念概述

根据 [Papurt, 1],“不可变实体永远不会改变其值,但不可变性有两种表现形式。一个严格不可变实体在创建时获得初始值,并一直持有该值直到终止。在上下文不可变性中,一个原本可修改的实体在特定上下文中被保持为可修改的。” Lua 中的某些值具有严格的不可变性,例如字符串、数字和布尔值(以及除 upvalues 和环境之外的函数)。Lua 表是可变的,但可以通过元方法在一定程度上模拟严格不可变性(除了 rawset),如 ReadOnlyTables 中所示。Userdata 可以提供更强的执行。相比之下,Lua 的局部变量是可变的(就变量和值之间的关联而言,而不是值本身),而全局变量是作为表实现的,因此与表具有相同的条件。即使一个变量可以被认为是常量(例如 math.pi),如果它在实践中通常不会被修改,这仅仅是一种约定,Lua 编译器或运行时不强制执行。上下文不可变性(或“const 正确性”)在语言中不太常见,尽管它在 C/C++ 中被广泛使用并在 D 中得到扩展([Wikipedia:Const correctness])。上下文不可变性提供了对象的一个只读“视图”。

我们还有一个问题是不可变性的传递性——如果某个对象是不可变的,那么从该对象可达的其他对象是否也是不可变的。例如,如果 document.page[i] 检索文档的第 i 页,并且如果 document 是上下文不可变的,那么从 document.page[i] 检索到的页面是否也是上下文不可变的?在 C 中,我们区分 const 指针、指向 const 的指针和 const 指针指向 const,以及返回 const 指针的 const 方法(在前面的例子中允许传递性)。在 D 中,完全的传递性是内置的。 [4][1][5]

以下是在 Lua 中模拟各种不可变性效果的方法。

常量的不可变性

一种常见的约定(不被编译器强制执行)是将永远不会修改的变量命名为 ALL_CAPS。请参阅 LuaStyleGuide

local MAX_SIZE = 20

for i=1,MAX_SIZE do print(i) end

表的不可变性(只读表)

可以通过元方法使表在很大程度上不可变。请参阅 ReadOnlyTables

Userdata 的不可变性

Userdata 具有表的一些特性。然而,一个可能的优势是,与 Lua 表相比,Userdata 的不可变性可以在 Lua 端得到更强的执行。

函数对象的不可变性

函数可以通过 upvalues、环境变量或通过其他函数调用(例如数据库)可访问的存储区域来存储数据。当一个函数是纯函数时,它可以被认为是不可变的 [1]。使函数不纯的因素包括 upvalues 的更改(如果它有 upvalues)、环境的更改(如果它使用环境​​变量)以及对其他不纯函数的调用(存储在 upvalues 或环境​​变量中)。函数实现可以采取一些简单的步骤来防止这些情况。

do
  local sqrt = math.sqrt -- make the environment variable "math.sqrt"
                         -- a lexical to ensure it is never redefined
  function makepoint(x,y)
    assert(type(x) == 'number' and type(y) == 'number')
    -- the upvalues x and y are only accessible inside this scope.
    return function()
      return x, y, sqrt(x^2 + y^2)
    end
  end
end
local x,y = 10,20
local p = makepoint(x,y)
print(p()) --> 10  20  22.360679774998
math.sqrt = math.cos  -- this has no effect
x,y = 50,60           -- this has no effect
print(p()) --> 10  20  22.360679774998

外部例程仍然可以访问此类函数的环境​​变量和 upvalues。函数的环境​​可以通过 setfenv 更改 [2]。尽管函数 f 的实现可能不使用环境​​变量,但这仍然会影响从函数外部对任何 getfenv(f) 调用的结果,因此从这个意义上讲,函数不是不可变的。其次,upvalues 实际上可以通过 debug.setupvalue 进行修改 [3],但 debug 接口被认为是后门。

有关使用函数表示不可变元组的更多描述,请参阅 SimpleTuples

字符串的不可变性

Lua 字符串是不可变的且被interned。这有一些影响 [4]。要创建一个与现有 100 MB 字符串仅一字符不同的字符串,需要创建一个全新的 100 MB 字符串,因为原始字符串无法修改。

有人使用 userdata 实现(可变的)字符串缓冲区。

在 Lua C API 中,字符串缓冲区 [5] 是可变的。

在运行时模拟 Lua 中的上下文不可变性

这是一个在 Lua 中运行时模拟上下文不可变性的快速示例:[2]

-- converts a mutable object to an immutable one (as a proxy)
local function const(obj)
  local mt = {}
  function mt.__index(proxy,k)
    local v = obj[k]
    if type(v) == 'table' then
      v = const(v)
    end
    return v
  end
  function mt.__newindex(proxy,k,v)
    error("object is constant", 2)
  end
  local tc = setmetatable({}, mt)
  return tc
end

-- reimplementation of ipairs,
-- from https://lua-users.lua.ac.cn/wiki/GeneralizedPairsAndIpairs
local function _ipairs(t, var)
  var = var + 1
  local value = t[var]
  if value == nil then return end
  return var, value
end
local function ipairs(t) return _ipairs, t, 0 end


-- example usage:

local function test2(library)  -- example function
  print(library.books[1].title)
  library:print()

  -- these fail with "object is constant"
  -- library.address = 'brazil'
  -- library.books[1].author = 'someone'
  -- library:addbook {title='BP', author='larry'}
end

local function test(library)  -- example function
  library = const(library)

  test2(library)
end

-- define an object representing a library, as an example.
local library = {
  books = {}
}
function library:addbook(book)
  self.books[#self.books+1] = book
  -- warning: rawset ignores __newindex metamethod on const proxy.
  -- table.insert(self.books, book)
end
function library:print()
  for i,book in ipairs(self.books) do
    print(i, book.title, book.author)
  end
end

library:addbook {title='PiL', author='roberto'}
library:addbook {title='BLP', author='kurt'}

test(library)

关键行是“library = const(library)”,它将一个可变参数转换为不可变参数。const 返回一个代理对象,该对象包装了给定的对象,允许读取对象​​的字段但不能写入。它提供了对象的“视图”(想象一下:数据库)。

请注意,递归调用 v = const(v) 提供了对不可变性的某种传递性。

上述代码中指出了这种方法的几个注意事项。原始对象的​​方法不会接收原始对象,而是接收对象的代理。因此,这些方法必须避免使用原始的 get 和 set(它们不会触发元方法)。在 LuaFiveTwo 之前,我们还有 pairs/ipairs/# 的问题(参见 GeneralizedPairsAndIpairs)。上述内容可以扩展以支持运算符重载。

上述方法确实会带来一些运行时开销。然而,在生产环境中,您可以将 const 定义为身份函数,甚至从源代码中删除这些函数。

注意:上述内容是一个概念验证,不一定建议在实际中使用。

在编译时模拟 Lua 中的上下文不可变性

在编译时(静态分析)执行 const 正确性检查可能更可取,就像在 C/C++ 中一样。可以使用工具来编写,例如,使用 MetaLua 的 gg/mlp、Leg 等(参见 LuaGrammar),或者可能通过 luac -p -l 进行一些技巧(例如,参见 DetectingUndefinedVariables)。这样的代码“可能”看起来像这样。

local function test2(library)
  print(library.books[1].title)
  library:print()

  -- these fail with "object is constant"
  -- library.address = 'brazil'
  -- library.books[1].author = 'someone'
  -- library:addbook {title='BP', author='larry'}
  library:print()
end

local function test(library)  --! const(library)
  test2(library)
end

local library = {
  books = {}
}
function library:addbook(book)
  table.insert(self.books[#self.books+1], book)
end
function library:print()
  for i,book in ipairs(self.books) do
    print(i, book.title, book.author)
  end
end

library:addbook {title='PiL', author='roberto'}
library:addbook {title='BLP', author='kurt'}

test(library)

上面,使用特殊格式的注释(--!)中的注解来指示静态分析检查工具,给定的参数应被视为常量。这在实践中如何工作尚不清楚。显然,由于 Lua 的动态特性,这只能在受限的情况下完成(例如,大量使用未被修改的局部变量,并假设 table.insert 等全局变量保留其通常的语义)。

表的运行时常量检查

以下库可以在调试期间使用,以确保名称后辍为“_c”的函数参数在函数调用之间保持不变。

-- debugconstcheck.lua
-- D.Manura, 2012-02, public domain.
-- Lua 5.2. WARNING: not well tested; "proof-of-concept"; has big performance impact
-- May fail with coroutines.

local stack = {}
local depth = 0

-- Gets unique identifier for current state of Lua object `t`.
-- This implementation could be improved.
-- Currently it only does a shallow table traversal and ignores metatables.
-- It could represent state with a smaller hash (less memory).
-- Note: false negatives are not serious problems for purposes here.
local function checksum(t)
  if type(t) ~= 'table' then
    return ("(%q%q)"):format(tostring(type(t)), tostring(t))
  end
  local ts = {}
  for k, v in next, t do
    ts[#ts+1] = ('%q%q'):format(tostring(k), tostring(v))
  end
  return '("table"'..table.concat(ts)..')'
end

-- Checks function parameters on call/returns with a debug hook.
debug.sethook(function(op)
  if op ~= 'return' then

    depth = depth + 1
    local info = debug.getinfo(2, 'ufn')
    --print('+', depth, info.name, op)
    local nfound = 0
    for i=1, info.nparams do
      local name, val = debug.getlocal(2, i)
      if name:match('_c$') then -- record state of param on function entry
        stack[#stack+1] = val
        stack[#stack+1] = checksum(val)
        --print(name, stack[#stack])
        nfound = nfound + 1
      end
    end
    if nfound ~= 0 then stack[#stack+1] = nfound; stack[#stack+1] = depth end
  
  else -- 'call' or 'tail call'

    --print('-', depth, debug.getinfo(2, 'n').name)
    if depth == stack[#stack] then -- check state of params on function exit
      table.remove(stack)
      local count = table.remove(stack)
      for i=count,1,-1 do
        local sum = table.remove(stack)
        local val = table.remove(stack)
        local current_sum = checksum(val)
        --print('check', '\n'..sum, '\n'..current_sum)
        if sum ~= current_sum then error("const variable mutated") end
      end
    end
    depth = depth - 1
  
  end
end, 'cr')

return {}

示例(使用 lua -ldebugconstcheck example.lua 运行)

-- example.lua

local function g(a,b,c)
  b.x=1 -- ok
  table.sort(a) -- bad
end

local function f(a_c, b, c_c)
  return g(a_c, b, c_c)
end

f({'b','a'}, {}, {})

结果

lua52: ./debugconstcheck.lua:46: const variable mutated
stack traceback:
	[C]: in function 'error'
	./debugconstcheck.lua:46: in function <./debugconstcheck.lua:17>
	example.lua:10: in main chunk
	[C]: in ?

另请参阅 / 参考


RecentChanges · preferences
编辑 · 历史
最后编辑于 2016 年 5 月 6 日下午 3:52 GMT (diff)