第一版是为 Lua 5.0 编写的。虽然在很大程度上仍然适用于较新版本,但存在一些差异。
第四版针对 Lua 5.3,可在 Amazon 和其他书店购买。
购买本书,您还可以帮助 支持 Lua 项目


25.1 – 表格操作

让我们采用这种态度:现在,我们还希望为窗口配置背景颜色。我们假设最终颜色规范由三个数字组成,其中每个数字都是 RGB 中的颜色分量。通常,在 C 中,这些数字是某个范围内的整数,如 [0,255]。在 Lua 中,因为所有数字都是实数,我们可以使用更自然的范围 [0,1]

这里的一个简单方法是要求用户在不同的全局变量中设置每个分量

    -- configuration file for program `pp'
    width = 200
    height = 300
    background_red = 0.30
    background_green = 0.10
    background_blue = 0
这种方法有两个缺点:它太冗长(实际程序可能需要几十种不同的颜色,用于窗口背景、窗口前景、菜单背景等);并且没有办法预定义常用颜色,因此,稍后,用户可以简单地编写类似 background = WHITE 的内容。为了避免这些缺点,我们将使用一个表格来表示颜色
    background = {r=0.30, g=0.10, b=0}
使用表格为脚本提供了更多结构;现在,用户(或应用程序)可以轻松地为配置文件中的后续使用预定义颜色
    BLUE = {r=0, g=0, b=1}
    ...
    background = BLUE
要在 C 中获取这些值,我们可以执行以下操作
    lua_getglobal(L, "background");
    if (!lua_istable(L, -1))
      error(L, "`background' is not a valid color table");
    
    red = getfield("r");
    green = getfield("g");
    blue = getfield("b");
和往常一样,我们首先获取全局变量 background 的值并确保它是一个表格。接下来,我们使用 getfield 获取每个颜色分量。此函数不是 API 的一部分;我们必须按如下方式定义它
    #define MAX_COLOR       255
    
    /* assume that table is on the stack top */
    int getfield (const char *key) {
      int result;
      lua_pushstring(L, key);
      lua_gettable(L, -2);  /* get background[key] */
      if (!lua_isnumber(L, -1))
        error(L, "invalid component in background color");
      result = (int)lua_tonumber(L, -1) * MAX_COLOR;
      lua_pop(L, 1);  /* remove number */
      return result;
    }
同样,我们面临多态性问题:可能存在许多版本的 getfield 函数,它们的变化包括键类型、值类型、错误处理等。Lua API 提供了一个单一函数 lua_gettable。它接收堆栈中表的位置,从堆栈中弹出键,并推送相应的值。我们的私有 getfield 假设表位于堆栈顶部;因此,在推送键(lua_pushstring)后,表将位于索引 -2。在返回之前,getfield 从堆栈中弹出检索到的值,以使堆栈处于调用之前相同的级别。

我们将进一步扩展我们的示例,并为用户引入颜色名称。用户仍然可以使用颜色表,但也可以对更常见的颜色使用预定义的名称。要实现此功能,我们需要在 C 应用程序中使用一个颜色表

    struct ColorTable {
      char *name;
      unsigned char red, green, blue;
    } colortable[] = {
      {"WHITE",   MAX_COLOR, MAX_COLOR, MAX_COLOR},
      {"RED",     MAX_COLOR,   0,   0},
      {"GREEN",     0, MAX_COLOR,   0},
      {"BLUE",      0,   0, MAX_COLOR},
      {"BLACK",     0, 0, 0},
      ...
      {NULL,        0, 0, 0}  /* sentinel */
    };

我们的实现将创建具有颜色名称的全局变量,并使用颜色表初始化这些变量。结果与用户在其脚本中具有以下行相同

    WHITE = {r=1, g=1, b=1}
    RED   = {r=1, g=0, b=0}
    ...
与这些用户定义的颜色唯一的区别是,应用程序在运行用户脚本之前在 C 中定义了这些颜色。

为了设置表字段,我们定义了一个辅助函数 setfield;它将索引和字段值推送到堆栈上,然后调用 lua_settable

    /* assume that table is at the top */
    void setfield (const char *index, int value) {
      lua_pushstring(L, index);
      lua_pushnumber(L, (double)value/MAX_COLOR);
      lua_settable(L, -3);
    }
与其他 API 函数类似,lua_settable 适用于许多不同的类型,因此它从堆栈中获取所有操作数。它接收表索引作为参数,并弹出键和值。setfield 函数假定在调用之前表位于堆栈顶部(索引 -1);在推送索引和值后,表将位于索引 -3

setcolor 函数定义了一种颜色。它必须创建一个表,设置适当的字段,并将此表分配给相应的全局变量

    void setcolor (struct ColorTable *ct) {
      lua_newtable(L);               /* creates a table */
      setfield("r", ct->red);        /* table.r = ct->r */
      setfield("g", ct->green);      /* table.g = ct->g */
      setfield("b", ct->blue);       /* table.b = ct->b */
      lua_setglobal(L, ct->name);    /* `name' = table */
    }
lua_newtable 函数创建一个空表并将其推送到堆栈上;setfield 调用设置表字段;最后,lua_setglobal 弹出表并将其设置为具有给定名称的全局变量的值。

使用这些先前的函数,以下循环将注册应用程序全局环境中的所有颜色

    int i = 0;
    while (colortable[i].name != NULL)
      setcolor(&colortable[i++]);
请记住,应用程序必须在运行用户脚本之前执行此循环。

还有另一种实现命名颜色的选项。用户可以使用字符串表示颜色名称,而不是全局变量,将她的设置写为 background = "BLUE"。因此,background 可以是表或字符串。使用此实现,应用程序在运行用户的脚本之前不需要执行任何操作。相反,它需要更多工作来获取颜色。当它获取变量 background 的值时,它必须测试该值是否为字符串类型,然后在颜色表中查找该字符串

    lua_getglobal(L, "background");
    if (lua_isstring(L, -1)) {
      const char *name = lua_tostring(L, -1);
      int i = 0;
      while (colortable[i].name != NULL &&
             strcmp(colorname, colortable[i].name) != 0)
        i++;
      if (colortable[i].name == NULL)  /* string not found? */
        error(L, "invalid color name (%s)", colorname);
      else {  /* use colortable[i] */
        red = colortable[i].red;
        green = colortable[i].green;
        blue = colortable[i].blue;
      }
    } else if (lua_istable(L, -1)) {
      red = getfield("r");
      green = getfield("g");
      blue = getfield("b");
    } else
        error(L, "invalid value for `background'");

最佳选项是什么?在 C 程序中,使用字符串表示选项不是一个好习惯,因为编译器无法检测拼写错误。然而,在 Lua 中,全局变量不需要声明,因此当用户拼错颜色名称时,Lua 不会发出任何错误信号。如果用户写了 WITE 而不是 WHITE,则 background 变量会收到 nil(未初始化变量 WITE 的值),这就是应用程序所知道的全部:backgroundnil。没有其他关于错误的信息。另一方面,使用字符串时,background 的值将是拼写错误的字符串;因此,应用程序可以将该信息添加到错误消息中。应用程序还可以不区分大小写地比较字符串,以便用户可以编写 "white""WHITE" 甚至 "White"。此外,如果用户脚本很小并且有很多颜色,那么仅为了让用户选择几种颜色而注册数百种颜色(并创建数百个表和全局变量)可能会很奇怪。使用字符串,您可以避免这种开销。