不可变对象 |
|
根据 [Papurt, 1],“不可变实体永远不会改变其值,但不可变性以两种方式体现。严格不可变实体在创建时获得初始值,并保持该值直到终止。在上下文不可变性中,一个原本可修改的实体在特定上下文中被视为不可修改。”。Lua 中的某些值具有严格的不可变性,例如字符串、数字和布尔值(以及不包含上值和环境的函数)。Lua 表是可变的,但可以通过元方法在一定程度上模拟严格的不可变性(除了 rawset
),如 ReadOnlyTables 中所示。用户数据可以提供更强的强制执行。相反,Lua 局部变量是可变的(就变量和值之间的关联而言,而不是值本身),而全局变量则作为表实现,因此与表具有相同的条件。虽然一个变量可能被认为是一个常量(例如 math.pi
),如果它在实践中通常不会被修改,但这只是一个约定,不受 Lua 编译器或运行时的强制执行。上下文不可变性(或“const 正确性”)在语言中不太常见,尽管它在 C/C++ 中被广泛使用,并在 D 中扩展 ([维基百科:Const 正确性])。上下文不可变性提供对象的只读“视图”。
我们还有不可变性的传递性问题——如果某个对象是不可变的,那么从该对象可达的其他对象是否也是不可变的。例如,如果 document.page[i]
检索文档的第 i
页,并且如果文档是上下文不可变的,那么从 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。
用户数据提供了一些类似于表的特性。然而,一个可能的优势是,与 Lua 表相比,用户数据的不可变性可以在 Lua 端得到更强的执行。
函数可以在上值、环境变量或通过其他函数调用(例如数据库)访问的存储区域中存储数据。当函数是纯函数 [1] 时,可以认为它是不可变的。可能使函数不纯净的事项包括对上值(如果有上值)的更改、对环境(如果使用环境变量)的更改以及对其他不纯函数(存储在上值或环境变量中)的调用。函数实现可以采取一些简单的步骤来防止这些事情发生。
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
外部例程仍然可以访问此类函数的环境变量和上值。函数的环境可以通过 setfenv
[2] 更改。虽然函数 f
的实现可能不使用环境变量,但这仍然会影响从函数外部进行的任何 getfenv(f)
调用的结果,因此,从这种意义上来说,函数不是不可变的。其次,上值实际上可以通过 debug.setupvalue
[3] 修改,但调试接口被认为是后门。
有关使用函数表示不可变元组的更多说明,请参见 SimpleTuples。
Lua 字符串是不可变的且被驻留。这有一些影响 [4]。要创建一个与现有 100 MB 字符串仅在一个字符上不同的字符串,需要创建一个全新的 100 MB 字符串,因为原始字符串无法修改。
有些人已经用用户数据实现了(可变)字符串缓冲区。
在 Lua C API 中,字符串缓冲区 [5] 是可变的。
以下是一个在 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)
为不可变性提供了一种传递性。
这种方法有一些上述代码中提到的注意事项。原始对象的方法不会传递原始对象,而是传递对象的代理。因此,这些方法必须避免原始获取和设置(不会触发元方法)。在 LuaFiveTwo 之前,我们还遇到了 pairs
/ipairs
/#
的问题(参见 GeneralizedPairsAndIpairs)。上述内容可以扩展以支持运算符重载。
上述方法确实会带来一些运行时开销。但是,在生产环境中,您可以将 const
定义为标识函数,甚至可以从源代码中删除这些函数。
注意:以上只是一个概念证明,并不一定建议在实践中用于一般用途。
在编译时(静态分析)执行 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 ?