隐藏 Lua 元表并仅公开对象的属性



如何创建一个只暴露其属性而不暴露其方法的Lua对象?例如:

local obj = {
attr1 = 1,
attr2 = 2,
print = function(...)
print("obj print: ", ...)
end,
}

生产:

> for k,v in pairs(obj) do print(k, v) end
attr1   1
attr2   2
print   function: 0x7ffe1240a310

另外,在Lua中不使用冒号语法是可能的吗?我不需要继承,多态性,只需要封装和隐私。

我从上面的问题开始,在追逐兔子后,我惊讶于有限的示例数量,缺乏各种元方法的示例(即__ipairs,__pairs,__len),以及关于该主题的Lua 5.2资源。

Lua可以实现OOP,但在我看来,OOP的规定方式对语言和社区是一种伤害(例如,以这种方式支持多态性、多重继承等)。很少有理由使用Lua的大多数OOP特性来解决大多数问题。这并不一定意味着有一个岔路口(例如,为了支持多态性,没有人说你必须使用冒号语法——你可以将文献中描述的技术融入到基于闭包的OOP方法中)。

我很欣赏在Lua中有很多方法来做OOP,但是对象属性和对象方法有不同的语法是令人恼火的(例如obj.attr1vsobj:getAttr()vsobj.method()vsobj:method())。我想要一个单一的,统一的API内部和外部通信。为此,PiL 16.4关于隐私的部分是一个很好的开始,但它是一个不完整的例子,我希望用这个答案来弥补。


以下示例代码:

  • 模拟类的命名空间MyObject = {},并将对象构造函数保存为MyObject.new()
  • 隐藏了对象内部工作的所有细节,因此对象的用户只能看到一个纯表(参见setmetatable()__metatable)
  • 使用闭包进行信息隐藏(参见Lua Pil 16.4和对象基准测试)
  • 防止修改对象(参见__newindex)
  • 允许方法被拦截(见__index)
  • 让您获得所有功能和属性的列表(参见__index中的'key'属性)
  • 看起来,行为,行走,和说话像一个普通的Lua表(见__pairs,__len,__ipairs)
  • 在需要的时候看起来像一个字符串(参见__tostring)
  • 适用于Lua 5.2

下面是构造一个新的MyObject的代码(这可以是一个独立的函数,它不需要存储在MyObject表中——一旦创建了obj,就绝对没有将其与MyObject.new()联系起来的东西,这样做只是为了熟悉和打破惯例):

MyObject = {}
MyObject.new = function(name)
local objectName = name
-- A table of the attributes we want exposed
local attrs = {
attr1 = 123,
}
-- A table of the object's methods (note the comma on "end,")
local methods = {
method1 = function()
print("tmethod1")
end,
print = function(...)
print("MyObject.print(): ", ...)
end,
-- Support the less than desirable colon syntax
printOOP = function(self, ...)
print("MyObject:printOOP(): ", ...)
end,
}
-- Another style for adding methods to the object (I prefer the former
-- because it's easier to copy/paste function()'s around)
function methods.addAttr(k, v)
attrs[k] = v
print("taddAttr: adding a new attr: " .. k .. "="" .. v .. """)
end
-- The metatable used to customize the behavior of the table returned by new()
local mt = {
-- Look up nonexistent keys in the attrs table. Create a special case for the 'keys' index
__index = function(t, k)
v = rawget(attrs, k)
if v then
print("INFO: Successfully found a value for key "" .. k .. """)
return v
end
-- 'keys' is a union of the methods and attrs
if k == 'keys' then
local ks = {}
for k,v in next, attrs, nil do
ks[k] = 'attr'
end
for k,v in next, methods, nil do
ks[k] = 'func'
end
return ks
else
print("WARN: Looking up nonexistant key "" .. k .. """)
end
end,
__ipairs = function()
local function iter(a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
return iter, attrs, 0
end,
__len = function(t)
local count = 0
for _ in pairs(attrs) do count = count + 1 end
return count
end,
__metatable = {},
__newindex = function(t, k, v)
if rawget(attrs, k) then
print("INFO: Successfully set " .. k .. "="" .. v .. """)
rawset(attrs, k, v)
else
print("ERROR: Ignoring new key/value pair " .. k .. "="" .. v .. """)
end
end,
__pairs = function(t, k, v) return next, attrs, nil end,
__tostring = function(t) return objectName .. "[" .. tostring(#t) .. "]" end,
}
setmetatable(methods, mt)
return methods
end

现在的用法是:

-- Create the object
local obj = MyObject.new("my object's name")
print("Iterating over all indexes in obj:")
for k,v in pairs(obj) do print('', k, v) end
print()
print("obj has a visibly empty metatable because of the empty __metatable:")
for k,v in pairs(getmetatable(obj)) do print('', k, v) end
print()
print("Accessing a valid attribute")
obj.print(obj.attr1)
obj.attr1 = 72
obj.print(obj.attr1)
print()
print("Accessing and setting unknown indexes:")
print(obj.asdf)
obj.qwer = 123
print(obj.qwer)
print()
print("Use the print and printOOP methods:")
obj.print("Length: " .. #obj)
obj:printOOP("Length: " .. #obj) -- Despite being a PITA, this nasty calling convention is still supported
print("Iterate over all 'keys':")
for k,v in pairs(obj.keys) do print('', k, v) end
print()
print("Number of attributes: " .. #obj)
obj.addAttr("goosfraba", "Satoshi Nakamoto")
print("Number of attributes: " .. #obj)
print()
print("Iterate over all keys a second time:")
for k,v in pairs(obj.keys) do print('', k, v) end
print()
obj.addAttr(1, "value 1 for ipairs to iterate over")
obj.addAttr(2, "value 2 for ipairs to iterate over")
obj.addAttr(3, "value 3 for ipairs to iterate over")
obj.print("ipairs:")
for k,v in ipairs(obj) do print(k, v) end
print("Number of attributes: " .. #obj)
print("The object as a string:", obj)

生成预期的——格式很差的——输出:

Iterating over all indexes in obj:
attr1   123
obj has a visibly empty metatable because of the empty __metatable:
Accessing a valid attribute
INFO: Successfully found a value for key "attr1"
MyObject.print():   123
INFO: Successfully set attr1="72"
INFO: Successfully found a value for key "attr1"
MyObject.print():   72
Accessing and setting unknown indexes:
WARN: Looking up nonexistant key "asdf"
nil
ERROR: Ignoring new key/value pair qwer="123"
WARN: Looking up nonexistant key "qwer"
nil
Use the print and printOOP methods:
MyObject.print():   Length: 1
MyObject.printOOP():        Length: 1
Iterate over all 'keys':
addAttr func
method1 func
print   func
attr1   attr
printOOP        func
Number of attributes: 1
addAttr: adding a new attr: goosfraba="Satoshi Nakamoto"
Number of attributes: 2
Iterate over all keys a second time:
addAttr func
method1 func
print   func
printOOP        func
goosfraba       attr
attr1   attr
addAttr: adding a new attr: 1="value 1 for ipairs to iterate over"
addAttr: adding a new attr: 2="value 2 for ipairs to iterate over"
addAttr: adding a new attr: 3="value 3 for ipairs to iterate over"
MyObject.print():   ipairs:
1   value 1 for ipairs to iterate over
2   value 2 for ipairs to iterate over
3   value 3 for ipairs to iterate over
Number of attributes: 5
The object as a string: my object's name[5]

  • 在将Lua作为facade嵌入或记录API时,使用OOP +闭包非常方便。
  • Lua OOP也可以非常非常干净和优雅(这是主观的,但是这种风格没有任何规则-您总是使用.来访问属性或方法)
  • 让一个对象的行为完全像一个表,对于编写脚本和询问程序的状态是非常非常有用的
  • 在沙盒中操作时非常有用

这种风格在每个对象上消耗的内存稍微多一些,但在大多数情况下这不是问题。分解元表以供重用可以解决这个问题,尽管上面的示例代码没有解决这个问题。

最后一个想法。一旦你忽略了文献中的大多数例子,Lua OOP实际上是非常好的。我不是说文献不好,顺便说一句(这离事实太远了!),但是PiL和其他在线资源中的一组示例示例导致您只使用冒号语法(即所有函数的第一个参数是self,而不是使用closureupvalue来保留对self的引用)。

希望这是一个有用的,更完整的例子。


更新(2013-10-08)上面详细介绍的基于闭包的OOP风格有一个明显的缺点(我仍然认为这种风格值得开销,但我离题了):每个实例必须有自己的闭包。虽然这在上面的lua版本中很明显,但在c端处理事情时,这就变得有点问题了。

假设我们从这里开始讨论c端上面的闭包风格。C端常见的情况是通过lua_newuserdata()对象创建userdata,并通过lua_setmetatable()将元表附加到userdata。从表面上看,这似乎不是一个问题,直到您意识到元表中的方法需要userdata的上值。

using FuncArray = std::vector<const ::luaL_Reg>;
static const FuncArray funcs = {
{ "__tostring", LI_MyType__tostring },
};
int LC_MyType_newInstance(lua_State* L) {
auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType)));
new(userdata) MyType();
// Create the metatable
lua_createtable(L, 0, funcs.size());     // |userdata|table|
lua_pushvalue(L, -2);                    // |userdata|table|userdata|
luaL_setfuncs(L, funcs.data(), 1);       // |userdata|table|
lua_setmetatable(L, -2);                 // |userdata|
return 1;
}
int LI_MyType__tostring(lua_State* L) {
// NOTE: Blindly assume that upvalue 1 is my userdata
const auto n = lua_upvalueindex(1);
lua_pushvalue(L, n);                     // |userdata|
auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, -1));
lua_pushstring(L, myTypeInst->str());    // |userdata|string|
return 1;                                // |userdata|string|
}

请注意,使用lua_createtable()创建的表与使用luaL_getmetatable()注册的元表名称是不相关联的。这是100%没问题的,因为这些值在闭包之外是完全不可访问的,但这确实意味着luaL_getmetatable()不能用于查找特定的userdata的类型。同样,这也意味着luaL_checkudata()luaL_testudata()也是禁止的。

底线是上值(如上面的userdata)与函数调用(如LI_MyType__tostring)相关联,而与userdata本身无关。到目前为止,我还不知道有什么方法可以将一个上值与一个值相关联,从而使跨实例共享元表成为可能。


更新(2013-10-14)我在下面包括一个使用注册元表(luaL_newmetatable())和lua_setuservalue()/lua_getuservalue()userdata的"属性和方法"的小例子。还添加了一些随机的注释,这些注释是我过去一直在寻找的bug/热点的来源。还提供了一个c++ 11技巧来帮助__index

namespace {
using FuncArray = std::vector<const ::luaL_Reg>;
static const std::string MYTYPE_INSTANCE_METAMETHODS{"goozfraba"}; // I use a UUID here
static const FuncArray MyType_Instnace_Metamethods = {
{ "__tostring", MyType_InstanceMethod__tostring },
{ "__index",    MyType_InstanceMethod__index },
{ nullptr,      nullptr }, // reserve space for __metatable
{ nullptr, nullptr } // sentinel
};
static const FuncArray MyType_Instnace_methods = {
{ "fooAttr", MyType_InstanceMethod_fooAttr },
{ "barMethod", MyType_InstanceMethod_barMethod },
{ nullptr, nullptr } // sentinel
};
// Must be kept alpha sorted
static const std::vector<const std::string> MyType_Instance___attrWhitelist = {
"fooAttr",
};
static int MyType_ClassMethod_newInstance(lua_State* L) {
// You can also use an empty allocation as a placeholder userdata object
// (e.g. lua_newuserdata(L, 0);)
auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType)));
new(userdata) MyType(); // Placement new() FTW
// Use luaL_newmetatable() since all metamethods receive userdata as 1st arg
if (luaL_newmetatable(L, MYTYPE_INSTANCE_METAMETHODS.c_str())) { // |userdata|metatable|
luaL_setfuncs(L, MyType_Instnace_Metamethods.data(), 0); // |userdata|metatable|
// Prevent examining the object: getmetatable(MyType.new()) == empty table
lua_pushliteral(L, "__metatable");     // |userdata|metatable|literal|
lua_createtable(L, 0, 0);              // |userdata|metatable|literal|table|
lua_rawset(L, -3);                     // |userdata|metatable|
}
lua_setmetatable(L, -2);                 // |userdata|
// Create the attribute/method table and populate with one upvalue, the userdata
lua_createtable(L, 0, funcs.size());     // |userdata|table|
lua_pushvalue(L, -2);                    // |userdata|table|userdata|
luaL_setfuncs(L, funcs.data(), 1);       // |userdata|table|
// Set an attribute that can only be accessed via object's fooAttr, stored in key "fooAttribute"
lua_pushliteral(L, "foo's value is hidden in the attribute table"); // |userdata|table|literal|
lua_setfield(L, -2, "fooAttribute");     // |userdata|table|
// Make the attribute table the uservalue for the userdata
lua_setuserdata(L, -2);                  // |userdata|
return 1;
}
static int MyType_InstanceMethod__tostring(lua_State* L) {
// Since we're using closures, we can assume userdata is the first value on the stack.
// You can't make this assumption when using metatables, only closures.
luaL_checkudata(L, 1, MYTYPE_INSTANCE_METAMETHODS.c_str()); // Test anyway
auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, 1));
lua_pushstring(L, myTypeInst->str());    // |userdata|string|
return 1;                                // |userdata|string|
}
static int MyType_InstanceMethod__index(lua_State* L) {
lua_getuservalue(L, -2);        // |userdata|key|attrTable|
lua_pushvalue(L, -2);           // |userdata|key|attrTable|key|
lua_rawget(L, -2);              // |userdata|key|attrTable|value|
if (lua_isnil(L, -1)) {         // |userdata|key|attrTable|value?|
return 1;                     // |userdata|key|attrTable|nil|
}
// Call cfunctions when whitelisted, otherwise the caller has to call the
// function.
if (lua_type(L, -1) == LUA_TFUNCTION) {
std::size_t keyLen = 0;
const char* keyCp = ::lua_tolstring(L, -3, &keyLen);
std::string key(keyCp, keyLen);
if (std::binary_search(MyType_Instance___attrWhitelist.cbegin(),
MyType_Instance___attrWhitelist.cend(), key))
{
lua_call(L, 0, 1);
}
}
return 1;
}
static int MyType_InstanceMethod_fooAttr(lua_State* L) {
// Push the uservalue on to the stack from fooAttr's closure (upvalue 1)
lua_pushvalue(L, lua_upvalueindex(1)); // |userdata|
lua_getuservalue(L, -1);               // |userdata|attrTable|
// I haven't benchmarked whether lua_pushliteral() + lua_rawget()
// is faster than lua_getfield() - (two lua interpreter locks vs one lock + test for
// metamethods).
lua_pushliteral(L, "fooAttribute");    // |userdata|attrTable|literal|
lua_rawget(L, -2);                     // |userdata|attrTable|value|
return 1;
}
static int MyType_InstanceMethod_barMethod(lua_State* L) {
// Push the uservalue on to the stack from barMethod's closure (upvalue 1)
lua_pushvalue(L, lua_upvalueindex(1)); // |userdata|
lua_getuservalue(L, -1);               // |userdata|attrTable|
// Push a string to finish the example, not using userdata or attrTable this time
lua_pushliteral(L, "bar() was called!"); // |userdata|attrTable|literal|
return 1;
}
} // unnamed-namespace

lua脚本部分看起来像这样:

t = MyType.new()
print(typue(t))    --> "userdata"
print(t.foo)       --> "foo's value is hidden in the attribute table"
print(t.bar)       --> "function: 0x7fb560c07df0"
print(t.bar())     --> "bar() was called!"

如何创建只暴露其属性而不暴露其方法的lua对象?

如果你不以任何方式公开方法,你就不能调用它们,对吗?从您的示例中判断,听起来您真正想要的是一种遍历对象属性而不看到方法的方法,这是公平的。

最简单的方法是使用元表,它将方法放在一个单独的表中:

-- create Point class
Point = {}
Point.__index = Point
function Point:report() print(self.x, self.y) end
-- create instance of Point
pt = setmetatable({x=10, y=20}, Point)
-- call method
pt:report() --> 10 20
-- iterate attributes
for k,v in pairs(pt) do print(k,v) end --> x 10 y 20

是有可能不使用冒号语法的OOP在Lua?

你可以用闭包代替,但是pairs会看到你的方法。

function Point(x, y)
local self = { x=x, y=y}
function pt.report() print(self.x, self.y) end
return self
end
pt = Point(10,20)
pt.report() --> 10 20
for k,v in pairs(pt) do print(k,v) end --> x 10 y 20 report function: 7772112

你可以通过编写一个只显示属性的迭代器来解决后一个问题:

function nextattribute(t, k)
local v
repeat
k,v = next(t, k)
if type(v) ~= 'function' then return k,v end
until k == nil
end
function attributes (t)
return nextattribute, t, nil
end
for k,v in attributes(pt) do print(k,v) end --> x 10 y 20

我不需要继承,多态性

在Lua中,无论有没有类,都可以免费获得多态性。如果你的动物园有狮子,斑马,长颈鹿,它们都可以Eat(),并希望将它们传递给相同的Feed(animal)例程,在静态类型OO语言中,你需要将Eat()放在一个公共基类中(例如Animal)。Lua是动态类型的,你的Feed例程可以传递任何对象。重要的是你传递给它的对象有一个Eat方法。

这有时被称为"鸭子打字":如果它像鸭子一样叫,像鸭子一样游泳,它就是一只鸭子。就我们的Feed(animal)例程而言,如果它像动物一样吃,它就是动物。

只有封装和隐私。

那么我认为在隐藏方法的同时暴露数据成员是与你想要做的相反的。

最新更新