用户数据环境示例

lua-users home
wiki

以下是一些示例代码,允许使用环境表作为“按需”存储未知键,我习惯这样做,因为它允许 Lua 程序为实现为完整用户数据的 C 对象添加属性。代码摘自一个快速且脏的向量库,可以从 [1][2] 下载,示例输出在 [3]。我(RiciLake)昨晚才把它拼凑起来,所以不保证任何东西。

这是基本的用户数据结构

// How do you get the wiki to syntax colour C?
typedef struct lvec_s {
#ifdef VERSION51
  int has_env;
#else
  int len;
#endif
  lua_Number d[1];
} lvec_t;
注意 has_env 成员的使用。除了将用户数据头填充到 x86 架构上的双字边界,这可以防止双精度浮点数跨越缓存边界,它还用作一个标志,指示对象是否具有有用的环境表。(在 5.0 版本中,len 成员会使对齐失效。最好使用 union {int len; lua_Number dummy} u;

Lua API 不允许创建没有环境表的用户数据,我发现的所有其他检查其存在性的解决方案都比较慢。

这是 getter 方法,它期望在 upvalue 索引 2 处找到对象的元表(我在所有方法中使用 upvalue 1 作为元表)

/* getter and setter, allowing overriding in 5.1. 5.0 doesn't have environment tables */
static int lvec_meta_index (lua_State *L) {
  int len = 0;
  lvec_t *u = getveclen(L, 1, &len); 
首先,我们检查键是否是一个合理的数字索引,如果不是,则返回 nil。(我的观点是 getter 永远不应该抛出错误,它们应该简单地返回 nil。这与通常的表语义一致,并且也使编写不会生成随机错误的代码变得更容易。)
  if (lua_type(L, 2) == LUA_TNUMBER) {
    int idx = checkkey(L, len);
    if (idx) {
      lua_pushnumber(L, u->d[idx-1]);
      return 1;
    }
    else
      return 0;
  }
通常,我会在此时检查 getter 方法。我稍后会找到一个此类代码的好例子,但在这个特定的问题域中,检查有点特殊
  if (lua_type(L, 2) == LUA_TSTRING) {
    size_t slen;
    const char *key = lua_tolstring(L, 2, &slen);
    if (slen == 1) {
      int idx = key[0] - 'x';
      if (len <= 3 && idx >= 0 && idx < len) {
        lua_pushnumber(L, u->d[idx]);
        return 1;
      }
    }
  }
现在,如果它不是一个内置属性,我们会检查是否创建了环境表,如果创建了,则在其中查找键。我们还没有检查实例方法,这允许为特定对象覆盖实例方法。事实上,如果我不是为了在这个特定代码中实现 5.0 半兼容性,我会使用不同的解决方案,其中方法表在创建环境表后成为环境表的 __index 元方法,这意味着最终的查找只需要进行一次,并且还允许在 Lua 中创建对象的子类。
#ifdef VERSION51
  if (u->has_env) {
    lua_getfenv(L, 1);
    lua_pushvalue(L, 2);
    lua_gettable(L, -2);
    if (!lua_isnil(L, -1))
      return 1;
    lua_pop(L, 2);
  }
#endif
最后,键在实例方法表中查找,该表作为 __index 函数的第 2 个上值附加(为了查找速度)。如果在该表中找不到,我们只返回 nil
  lua_gettable(L, lua_upvalueindex(2));
  return 1;
}

__newindex 方法的逻辑略有不同。同样,第一个检查是针对数字和短字符串键,但在这种情况下,对于错误的数字键和错误的值都会抛出错误。(对 luaL_checknumber 的调用是不正确的,我当时很懒:它会在某个时候被修复。它会产生难以理解的错误消息。)如果失败,则在必要时创建环境表,并将键值对插入新创建的表中。(如上所述,我通常会在创建新的环境表时将方法表作为 __index 元附加。)

static int lvec_meta_newindex (lua_State *L) {
  int len = 0;
  lvec_t *u = getveclen(L, 1, &len);
  if (lua_type(L, 2) == LUA_TNUMBER) {
    int idx = checkkey(L, len);
    if (idx)
      u->d[idx-1] = luaL_checknumber(L, 3);
    else
      luaL_error(L, "Vector index not integer or out of range");
    return 0;
  }
  if (lua_type(L, 2) == LUA_TSTRING) {
    size_t slen;
    const char *key = lua_tolstring(L, 2, &slen);
    if (slen == 1) {
      int idx = key[0] - 'x';
      if (len <= 3 && idx >= 0 && idx < len) {
        u->d[idx] = luaL_checknumber(L, 3);
        return 0;
      }
    }
  }
#ifdef VERSION51
  if (!u->has_env) {
    lua_newtable(L); 
    lua_pushvalue(L, -1);
    lua_setfenv(L, 1);
    u->has_env = 1;
  }
  else
    lua_getfenv(L, 1);
  lua_replace(L, 1);
  lua_settable(L, 1);
#else
  else
    luaL_error(L, "Vector index not integer or out of range");
#endif
  return 0;
}

讨论

Rici,这是一个很好/实用的例子。顺便说一下,示例输出链接已损坏。我花了一些时间才弄清楚页面的主要目的。虽然它在顶部说明了“环境表作为未知键的“按需”存储”,但我对它在什么上下文中以及 5.0 与 5.1 问题是如何成为一个因素感到有点困惑。但是,我现在明白了(虽然我还没有完全处理它),这是一种为用户数据实现 __index__newindex 元方法的方法,它可以很好地处理元表,并且显然是按需/延迟构建的。它受到 5.1 如何允许在 C 函数中使用单个环境表的影响。5.0/5.1 兼容性的既定目标可能在早期提供。大量使用括号 () 会降低可读性。这可以放在 Lua Fu 的 LuaDirectory 中。我认为那里有一个地方可以称为设计模式。--DavidManura

我修复了链接。你说得对——它确实需要编辑。我总是会在初稿中使用太多括号,这可能是 LISP 对我童年的影响 :) -- RiciLake

最近更改 · 偏好设置
编辑 · 历史记录
最后编辑于 2006 年 9 月 26 日上午 9:50 GMT (差异)