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_pushstring将key入栈之后,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_settable以table在栈中的索引作为参数,并将栈中的key和value出栈,用这两个值修改table。Setfield函数假定调用之前table是在栈顶位置(索引为-1)。将index和value入栈之后,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_setglobal将table出栈并将其赋给一个全局变量名。
有了前面这些函数,下面的循环注册所有的颜色到应用程序中的全局变量:
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误写成WITE,background变量将为nil(WITE的值没有初始化),然后应用程序就认为background的值为nil。没有其他关于这个错误的信息可以获得。另一方面,使用字符串表示,background的值也可能是拼写错了的字符串。因此,应用程序可以在发生错误的时候,定制输出的错误信息。应用可以不区分大小写比较字符串,因此,用户可以写"white","WHITE",甚至"White"。但是,如果用户脚本很小,并且颜色种类比较多,注册成百上千个颜色(需要创建成百上千个table和全局变量),最终用户可能只是用其中几个,这会让人觉得很怪异。在使用字符串表示的时候,应避免这种情况出现。