25.1 表操作

现在,我们打算使用Lua作为配置文件,配置窗口的背景颜色。我们假定最终的颜色有三个数字(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

这个方法有两个缺点:第一,太冗余(为了表示窗口的背景,窗口的前景,菜单的背景等,一个实际的应用程序可能需要几十个不同的颜色);第二,没有办法预定义共同部分的颜色,比如,假如我们事先定义了WHITE,用户可以简单的写background = WHITE来表示所有的背景色为白色。为了避免这些缺点,我们使用一个table来表示颜色:

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");

一般来说,我们首先获取全局变量backgroud的值,并保证它是一个table。然后,我们使用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的版本,key的类型,value的类型,错误处理等都不尽相同。Lua API只提供了一个lua_gettable函数,他接受table在栈中的位置为参数,将对应key值出栈,返回与key对应的value。我们上面的getfield函数假定table在栈顶,因此,lua_pushstringkey入栈之后,table-2的位置。返回之前,getfield会将栈恢复到调用前的状态。

我们对上面的例子稍作延伸,加入颜色名。用户仍然可以使用颜色table,但是也可以为共同部分的颜色预定义名字,为了实现这个功能,我们在C代码中需要一个颜色table

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

};

我们的这个实现会使用颜色名创建一个全局变量,然后使用颜色table初始化这些全局变量。结果和用户在脚本中使用下面这几行代码是一样的:

WHITE  = {r=1, g=1, b=1}

RED    = {r=1, g=0, b=0}

...

脚本中用户定义的颜色和应用中(C代码)定义的颜色不同之处在于:应用在脚本之前运行。

为了可以设置table域的值,我们定义个辅助函数setfield;这个函数将field的索引和field的值入栈,然后调用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在不同的参数类型情况下都可以使用,他从栈中获取所有的参数。lua_settabletable在栈中的索引作为参数,并将栈中的keyvalue出栈,用这两个值修改tableSetfield函数假定调用之前table是在栈顶位置(索引为-1)。将indexvalue入栈之后,table索引变为-3

Setcolor函数定义一个单一的颜色,首先创建一个table,然后设置对应的域,然后将这个table赋值给对应的全局变量:

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(ct->name);    /* 'name' = table */

}

lua_newtable函数创建一个新的空table然后将其入栈,调用setfield设置table的域,最后lua_setglobaltable出栈并将其赋给一个全局变量名。

有了前面这些函数,下面的循环注册所有的颜色到应用程序中的全局变量:

int i = 0;

while (colortable[i].name != NULL)

    setcolor(&colortable[i++]);

记住:应用程序必须在运行用户脚本之前,执行这个循环。

对于上面的命名颜色的实现有另外一个可选的方法。用一个字符串来表示颜色名,而不是上面使用全局变量表示,比如用户可以这样设置background = "BLUE"。所以,background可以是table也可以是string。对于这种实现,应用程序在运行用户脚本之前不需要做任何特殊处理。但是需要额外的工作来获取颜色。当他得到变量background的值之后,必须判断这个值的类型,是table还是string

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不会发出任何错误信息。比如,用户将WHITE误写成WITEbackground变量将为nil(WITE的值没有初始化),然后应用程序就认为background的值为nil。没有其他关于这个错误的信息可以获得。另一方面,使用字符串表示,background的值也可能是拼写错了的字符串。因此,应用程序可以在发生错误的时候,定制输出的错误信息。应用可以不区分大小写比较字符串,因此,用户可以写"white""WHITE",甚至"White"。但是,如果用户脚本很小,并且颜色种类比较多,注册成百上千个颜色(需要创建成百上千个table和全局变量),最终用户可能只是用其中几个,这会让人觉得很怪异。在使用字符串表示的时候,应避免这种情况出现。


相关链接:
lua程序设计目录 - 中国lua开发者 - lua论坛