笔记: Lua基础: Metatable, 多重返回值, 迭代器

Posted on 2009-05-20 00:10  apex.Cliz  阅读(4925)  评论(0编辑  收藏  举报

metatable

在一些博客上看到这个词被译作元表, 我更偏向把它称作重载表, 因为metatable的作用更像是重载(override)对应表的操作行为的(比如+, *).

构成metatable的方式是一个metatable挂接一个table, 如下所示:

tbl1 = {"alpha", "beta", "gamma"}
mt = {}
setmetatable(tbl1, mt)

可以用getmetatable()语句来检视一个table所挂接的metatable.

> print(getmetatable(tbl1) == mt)
true

metatable通过其包含的函数来给所挂接的table定义一些特殊的操作,包括:

__add: 定义所挂接table的加法操作
__mul: 定义乘法操作
__div: 定义除法操作
__sub: 定义减法操作
__unm: 定义负操作, 即: -table的含义
__tostring: 定义当table作为tostring()函式之参数被呼叫时的行为(例如: print(table)时将呼叫tostring(table)作为输出结果)
__concat: 定义连接操作(".."运算符)
__index: 定义当table中不存在的key值被试图获取时的行为
__newindex: 定义在table中产生新key值时的行为


__add, __mul, __div, __sub, __unm

例如, 可以用下方的语句定义table的加法行为并使用它.

function mt.__add(a,b)
 local result = setmetatable({}, mt)
 
 for i = 1, #a do
  table.insert(result, a[i])
 for i = 1, #b do
  table.insert(result, b[i])

 return result
end

> tbl1 = {"alpha", "beta", "gamma"}
> tbl2 = {"delta", "epsilon", "zeta"}
> setmetatable(tbl1, mt)
> setmetatable(tbl2, mt)
> add_test = tbl1 + tbl2
> print(#add_test)
6
> for i = 1, #add_test do print(i, addtest[i]) end
1, alpha
2, beta
3, gamma
4, delta
5, epsilon
6, zeta

mul, div, sub, unm的操作方式也比较类似, 执行自定义的运算过程, 最后返回一个table.


__tostring

如前所述, __tostring函式用于处理当table作为tostring()函式之参数时的行为. 很显然, __tostring函式应该返回一个字符串. tostring函式需要一个参数(代表作为参数的table).

function mt.__tostring(tbl)
 local result = "{"
 
 for i = 1, #tbl do
  if i > 1 then
   result = result .. ", "
  end

  result = result .. tostring(tbl[i])
 end

 result = result .. "}"
 return result
end

> tbl1 = {"alpha", "beta", "gamma"}
> tbl2 = {"delta", "epsilon", "zeta"}
> setmetatable(tbl1, mt)
> setmetatable(tbl2, mt)
> print(tbl1)
{alpha, beta, gamma}
> print(tbl2)
{delta, epsilon, zeta}


__index

当table中不存在的key值被访问时, 系统将返回nil(空). 在一些情况下, 可能希望返回更有意义的内容. 在这种情况下, 可以通过__index来达到这个效果.

__index的行为如下: 在table中不存在的key值被访问时呼叫__index, 例如: value = tbl.undefined, 此时将呼叫__index(如果其存在, 否则返回nil).

__index可以指向另一个table; 在这种情况下, 当其他语句访问原table中不存在的key值时, 会得到__index所指向的table中同名的key值. 例如:

deDE_races = {
 ["Night elf"] = "Nachtelf"
}
mt = {}
setmetatable(deDE_races, mt)

enUS_races = {
 ["Human"] = "Human",
 ["Night elf"] = "Night elf",
}

mt.__index = enUS_races

> print(deDE_races["Night elf"])
Nachtelf
> print(deDE_races["Human"])
Human

当deDE_races表中不存在的key值(deDE_races.Human)被访问时, 返回了__index所指向的另一个table中同名的key值(enUS_races.Human).

这是一种很方便的定义默认值的方法. 若将所有默认值在一个default_tbl中定义好, 以后其他同类型的table全部以__index挂接default_tbl, 则无需在每个table中重新设置所有key的默认值; 当未设定的某key被访问时, __index将指导其自动返回预设表中的key值.

除了挂接到另一个table以返回“默认值”外, __index还可以指向一个函数, 以处理包括返回错误信息等更加复杂的工作. 在此种情况下, __index必须携带两个参数, 依次为被访问的table及被访问的key值, 并必须返回一个值. 例如, 可以写一个这样的函数来产生错误信息:

function mt.__index(tbl, key)
 print("ERROR: Attempt to access undefined key '"..key.."' in " .. tostring(tbl))
 return nil
end


__newindex

在一些特定的情况下, 可能需要限制table中的key进行赋值的行为(包括产生新key值), 可以用__newindex来处理这个问题. __newindex携带三个参数: 在赋值行为中的table, key及赋予的值(value).

例如, 以下的函式可以防止在所挂接的table中建立banana这个key值:

function mt.__newindex(tbl, key, value)
 if key == "banana" then
  error("ERROR: Cannot set a protected key")
 else
  rawset(tbl, key, value)
 end
end

这样, 当试图建立这个key值时, 会得到如下的报错信息:

> tbl1.banana = "yellow"
stdin: 3: ERROR: Cannot set a protected key
stack traceback:
    [C]: in function 'error'
    stdin:3: in function
    stdin:1: in main chunk
    [C]: ?
> print(tbl1.banana)
nil

这里用到了rawset函数, 与其相关的还有rawget函数, 这两个函数会在忽略所挂接的metatable的前提下, 存取table中的key值, 其语法分别为:

value = rawget(tbl, key)
rawset(tbl, key, value)


多重返回值

在魔兽世界中, 有一些时候我们会需要一个函数返回超过一个值. 看下面的例子:

颜色在界面编程中是一项很重要的数值. 常见的颜色方法是用六位的十六进制数字, 每两位依次表示三原色中一种颜色在最终混合而成的颜色中所拥有的强度,从0(00)到255(FF). 在有些情况下, 可能会分别需要三原色每种色的强度数值. 在获取了六位十六进制数字后, 我们可以自己写一个函式返回分开的三种颜色的强度值.

例如说, 颜色#99CCFF可以被表示为(0.6, 0.8, 1.0), 其中1.0代表最大强度(255).
要设计一个这样的函式, 首先要涉及到以下的两个函式: string.sub()以及tonumber().
string.sub()函式用于取得给定字符串的子字符串, 而tonumber()可以将字符串以给定的进制转化为10进制数字.

这样, 我们得到下面的函式代码:

function ConvertHexToRGB(hex)
 local red = string.sub(hex, 1, 2)
 local green = string.sub(hex, 3, 4)
 local blue = string.sub(hex, 5, 6)

 -- Lua无敌的变量类型可变的优势在这里完美的体现了...
 red = tonumber(red, 16) / 255
 green = tonumber(green, 16) / 255
 blue = tonumber(blue, 16) / 255
 
 return red, green, blue -- 返回多重值的语句写法
end

函数的使用结果如下:

> print(ConvertHexToRGB("FFCC99"))
1, 0.8, 0.6
> print(ConvertHexToRGB("FFFFFF"))
1, 1, 1
> print(ConvertHexTORGB("000000"))
0, 0, 0

如果需要将这个函数返回的值赋给其他的变量, 可以这样写:

red, blue, green = ConvertHexToRGB("FFCC99")

要注意的是, 等号右边也可以有多个数值, 它们看起来应该是和左边一一对应赋值的; 但包括返回多重值的函数时, 情况将发生变化.

red, blue, green, alpha = ConvertHexToRGB("FFCC99"), "yet", "another", "argument"

在这个语句执行之后, 并不表示alpha将得到"yet", 而多余的两个字符串值将被抛弃; 出人意料的, 只有red正确地接收了函数返回的值, 而blue, green, alpha则分别接收了"yet", "another"和"argument"三个字符串.

用具有多重返回值的函数作为参数时, 也有类似的情况:

> print(ConvertHexToRGB("FFFFFF"))
1, 1, 1
> print(ConvertHexToRGB("FFFFFF"), "SomeOtherArgument")
1, SomeOtherArgument

出现这个情况的理由是这样的: 因为一些技术上的原因, 在Lua中, 类似的情况下一个具有多重返回值的函数必须位于参数列表的末尾, 否则它将只返回第一个值. 在第二个语句中, ConvertHexToRGB("FFFFFF")后面还有其他的参数, 于是它只返回了第一个值.

附注: 如果特意只需要第一个返回值, 可以用下面的方法, 即: 为函数语句多增添一对括号:

> print((ConvertHexToRGB("FFFFFF")))
1

有很多魔兽世界中的函式都具有多重返回值, 例如函式GetRaidRosterInfo()以玩家在raid中的编号为参数, 并将返回包括玩家姓名、团队职位(团长、团长助手或一般成员)、所在小队、角色等级、职业、目前所在地区、是否在线、是否死亡等等讯息.

一个函式可能会返回如此多的值, 但并不代表只为了利用其中的某一个值(不是第一个被返回的值), 就必须列一排的变量把结果全部接收下来. 当然在函式返回的值并不是太多的时候, 还是可以用这样的方法; 因为Lua并没有限制一个变量的类型(在赋值的同时就自动更换了), 可以用一些不太会用到的名字命名一个回收箱变量, 然后把不需要的值都丢到里面去. 这种做法被称作虚拟变量法(dummy variable). 例如, 在下面的语句中, 用了单下划线这样正常情况下不会用到的名字来命名一个虚变量, 用它来丢弃不需要的数值.

local _, g = ConvertHexToRGB("FFFFFF")

如同预期, g得到了函式返回的对应绿色(green)的颜色强度值, 而排在其之前被返回的红色强度值则被丢弃到不会用到的变量"_"当中去了.

明显的, 如果函式有了如同前面所说的数量庞大的返回值, 这种方法就一点都不好用了; 你需要浪费存储空间和语句来建立一大堆名字不同的垃圾箱, 只是为了丢弃不需要的变量. Lua提供了一个更优秀的解决方案: 函式select().

select()函式接收一个参数n, 用来指定一个起始点; 然后select函式将返回指定的多重返回值序列中起点开始到序列末尾为止的部份. 如只需要起始处的元素, 则再对函数语句增加一对括号即可.

下面的代码简单地体现了select函式的功能.

> print(select(1, "alpha", "beta", "gamma"))
alpha, beta, gamma
> print(select(2, "alpha", "beta", "gamma"))
beta, gamma

之前提过, 函数可以有不确定个数的参数(笔记: Lua基础: 函数, 控制流: "特别地, Lua支持不确定个数的参数列表..."), select()也可以用来帮助处理这里的参数列表.

select()函数的第一个参数, 除了是返回列表的起始点外, 还可以是字符"#". 在这种情况下, 它返回后续参数列表的长度:

> print(select("#", "alpha", "beta", "gamma"))
3

可以用这样的方法产生类似foreach的功能:

function printargs(...)
 local num_args = select("#", ...)
 for i = 1, num_args do
  local arg = select(i, ...)
  print(i, arg)
 end
end

> printargs("alpha", "beta", "gamma")
1, alpha
2, beta
3, gamma

这个功能比之前的foreach实现方式(for i=1, #tbl do)有一个优点: 可以在参数列表中放入空值(nil).

用#tbl实现时, 如果遇到nil元素, 将导致循环中止, 但用select语句实现的话, 循环不会中止.

function printargs2(...)
 local tbl = {...}
 for i = 1, #tbl do
  print(i, tbl[i])
 end
end

> printargs2("alpha", nil, "gamma")
1, alpha
> printargs("alpha", nil, "gamma")
1, alpha
2, nil
3, gamma


迭代器

目前为止提到的遍历功能都有一个共同的缺陷, 即: 它们都只支持table中标准array结构的部份, 而对以string为key的部份(根据它的实现方式, 也可以称为Hash部份)是无能为力的. 这并不代表不能把这一部份纳入遍历当中; 只是需要换另一种方式来写. 这样的功能需要内建的迭代器(iterator)函式来予以实现.

迭代器是"一个对象, 可以让程序编写人员遍历一个集合中的所有元素, 不管这个元素技术上的实现方式如何".

(Wikipedia: An iterator is an object which allows a programmer to traverse through all elements of a collection, regardless of its specific implementation.)

实际使用时仍依赖for函数, 此时的for格式有所不同, 称作generic for.

for val1, val2, val3, ... in <expression> do
 -- body of the for loop
end

其中在<expression>标签处放置的是迭代表达式, 此表达式返回以下三个值:

在每个迭代循环中被呼叫的函数; 迭代循环的初态; 迭代变量的初始值.

例如, 迭代表达式ipairs()返回下面的内容:

> print(ipairs(table))
function: 0x300980, table: 0x3072c0, 0
> print(table)
table: 0x3072c0

幸运的, 除非需要自己写迭代器, 这三个值一般不需要深入了解. 系统内部会自动处理并生成所需的迭代器.

ipairs()

ipairs()函数用于遍历table中的数组部分.

> tbl = {"alpha", "beta", "gamma"}
> for indx, value in ipairs(tbl) do
>> print(idx, value)
>> end
1, alpha
2, beta
3, gamma

ipairs()函数以一个table为参数并返回for循环遍历table所需的全部信息. 每次呼叫迭代函数时均获得一个元素的索引值, 及元素本身.

for后面的变量名称可以任意, 呼叫迭代函数所返回的值将依序填充到变量列表中的变量中去. 这些变量的作用范围仅限于for循环体.

 

pairs()

pairs()函数基本和ipairs()函数用法相同, 区别在于pairs()可以遍历整个table, 即包括数组及非数组部分.

> tbl = {"alpha", "beta", ["one"] = "uno", ["two"] = "dos"}
> for key, value in pairs(tbl) do
>> print(key, value)
>> end
1, alpha
2, beta
one, uno
two, dos

受到哈希表实现方式的影响, 此遍历过程的顺序和元素加入表的顺序是无关的. 上述结果与输入顺序相同只是巧合.

注意: pairs()函数在遍历时会呼叫table内建的next()函数, 如果在pairs()对table进行遍历的过程中对table进行加值操作, 将使next函数不能正常工作, 并使遍历终止或提前结束.

利用pairs()的遍历还可以清空一个table, 如:

for key,value in pairs(tbl) do
   tbl[key] = nil
end

 

string.gmatch()

string.gmatch()类似是Lua中的"正则表达式配对", 可以用来配对并获取原始字符串中的部分.

> for word in string.gmatch("These are some words", "%S+") do
>> print(word)
>> end
These
are
some
words
> for char in string.gmatch("Hello!", ".") do
>> print(char)
>> end
H
e
l
l
o
!

详细的配对规则到下一篇笔记的时候再写.