隐藏Lua metatable并仅暴露对象的属性

时间:2013-08-11 21:55:47

标签: lua lua-table metatable lua-5.2

如何创建仅暴露其属性而不暴露其方法的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中不使用冒号的冒号语法?我不需要继承,多态,只需要封装和隐私。

2 个答案:

答案 0 :(得分:10)

我从上面的问题开始,在追逐兔子洞后,我对实例数量有限感到惊讶,缺少各种元方法的例子(即__ipairs__pairs,{ {1}}),以及Lua 5.2资源在这个主题上的数量很少。

Lua可以做OOP,但IMO处理OOP的方式是对语言和社区的损害(即以支持多态,多重继承等方式)。对于大多数问题,使用Lua的大部分OOP功能的原因很少。它并不一定意味着在路上也存在分叉(例如,为了支持多态性,没有任何东西说你必须使用冒号语法 - 你可以折叠文献' s描述了基于闭包的OOP方法的技术。)

我很欣赏在Lua中有很多方法可以做OOP,但是对象属性与对象方法有不同的语法(例如__len vs obj.attr1 vs obj:getAttr() vs obj.method())。我想要一个统一的API来进行内部和外部通信。为此,PiL 16.4's section on Privacy是一个很棒的开始,但我希望通过这个答案来补救这个不完整的例子。


以下示例代码:

  • 模拟一个类的命名空间obj:method()并将对象构造函数保存为MyObject = {}
  • 隐藏对象内部工作的所有细节,以便对象的用户只能看到纯表(请参阅MyObject.new()setmetatable()
  • 使用闭包来隐藏信息(请参阅Lua Pil 16.4Object Benchmark Tests
  • 阻止修改对象(请参阅__metatable
  • 允许拦截方法(请参阅__newindex
  • 可让您获取所有功能和属性的列表(请参阅__index中的'键'属性)
  • 看起来,行为,走路和谈话就像普通的Lua表一样(见__index__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作为外观嵌入或记录API时,使用OOP +闭包非常方便。
  • Lua OOP也可以非常,非常干净和优雅(这是主观的,但是没有这种风格的规则 - 你总是使用.来访问属性或方法)
  • 让对象的行为与表格完全相同,非常有用,可以编写脚本并查询程序的状态
  • 在沙箱中操作时非常有用

这种风格每个对象消耗的内存略多,但在大多数情况下,这并不是一个问题。考虑到重复使用元数据可以解决这个问题,尽管上面的示例代码并没有。

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

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


更新(2013-10-08):上面详述的基于闭包的OOP风格有一个值得注意的缺点(我仍然认为风格值得开销,但我离题了):每个实例必须有自己的闭包。虽然这在上面的lua版本中是显而易见的,但在处理C端的事情时这会有点问题。

假设我们从这里开始讨论C面的上述封闭风格。 C方面的常见情况是通过userdata对象创建lua_newuserdata()并通过userdata将metatable附加到lua_setmetatable()。在面值上,在你意识到metatable中的方法需要upvalue用户数据之前,这看起来并不像是一个问题。

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()创建的表格如何与metatable名称相关联,就像您使用luaL_getmetatable()注册metatable一样?这是100%可以,因为这些值在闭包之外是完全无法访问的,但它确实意味着luaL_getmetatable()无法用于查找特定的userdata&#39}类型。同样,这也意味着luaL_checkudata()luaL_testudata()也是禁区。

最重要的是,upvalues(例如上面的userdata)与函数调用(例如LI_MyType__tostring)相关联,并且与userdata本身无关。截至目前,我还没有意识到可以将upvalue与值相关联的方式,以便可以跨实例共享元表。


更新(2013-10-14)我在下面的小例子中使用了已注册的元表(luaL_newmetatable())以及lua_setuservalue() / {{ 1}}用于lua_getuservalue()&#39; s&#34;属性和方法&#34;。还添加了随机评论,这些评论一直是我过去常常遇到的错误/热情的根源。还引入了一个C ++ 11技巧来帮助userdata

__index

lua脚本方面看起来像:

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

答案 1 :(得分:2)

  

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

如果您不以任何方式公开方法,则无法调用它们,对吧?从您的示例来看,听起来您真正想要的是一种迭代对象属性而无需查看方法的方法,这是公平的。

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

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

是否可以在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中免费获得多态,无论是否有类。如果您的动物园有Lion,Zebra,Giraffe,每个都可以Eat()并希望将它们传递到相同的Feed(animal)例程,使用静态类型的OO语言,您需要放置{{1}在一个公共基类中(例如Eat())。 Lua是动态类型的,您的Animal例程可以传递任何对象。重要的是你传递的对象有一个Feed方法。

这有时被称为"duck typing":如果它像鸭子一样呱呱叫,像鸭子一样游泳,那就是鸭子。就我们的Eat例程而言,如果它像动物一样吃,它就是动物。

  只有封装和隐私。

然后我认为在隐藏方法时暴露数据成员与你想要做的事情相反。