lua 4-14


新一篇: (LUA教程)第5章 函数 | 旧一篇: (LUA教程)第3章 表达式

LuaCPASCAL几乎支持所有的传统语句:赋值语句、控制结构语句、函数调用等,同时也支持非传统的多变量赋值、局部变量声明。
4.1 赋值语句
赋值是改变一个变量的值和改变表域的最基本的方法。
a = "hello" .. "world"
t.n = t.n + 1
Lua可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。
a, b = 10, 2*x       <-->       a=10; b=2*x
遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:
x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i]         -- swap 'a[i]' for 'a[i]'
当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:
a. 变量个数 > 值的个数             按变量个数补足nil
b. 变量个数 < 值的个数             多余的值会被忽略
例如:
a, b, c = 0, 1
print(a,b,c)             --> 0   1   nil
 
a, b = a+1, b+1, b+2     -- value of b+2 is ignored
print(a,b)               --> 1   2
 
a, b, c = 0
print(a,b,c)             --> 0   nil   nil
上面最后一个例子是一个常见的错误情况,注意:如果要对多个变量赋值必须依次对每个变量赋值。
a, b, c = 0, 0, 0
print(a,b,c)             --> 0   0   0
多值赋值经常用来交换变量,或将函数调用返回给变量:
a, b = f()
f()返回两个值,第一个赋给a,第二个赋给b
4.2 局部变量与代码块(block
使用local创建一个局部变量,与全局变量不同,局部变量只在被声明的那个代码块内有效。代码块:指一个控制结构内,一个函数体,或者一个chunk(变量被声明的那个文件或者文本串)。
x = 10
local i = 1              -- local to the chunk
 
while i<=x do
    local x = i*2        -- local to the while body
    print(x)             --> 2, 4, 6, 8, ...
    i = i + 1
end
 
if i > 20 then
    local x              -- local to the "then" body
    x = 20
    print(x + 2)
else
    print(x)             --> 10 (the global one)
end
 
print(x)                 --> 10 (the global one)
注意,如果在交互模式下上面的例子可能不能输出期望的结果,因为第二句local i=1是一个完整的chunk,在交互模式下执行完这一句后,Lua将开始一个新的chunk,这样第二句的i已经超出了他的有效范围。可以将这段代码放在do..end(相当于c/c++{})块中。
应该尽可能的使用局部变量,有两个好处:
1. 避免命名冲突
2. 访问局部变量的速度比全局变量更快.
我们给block划定一个明确的界限:do..end内的部分。当你想更好的控制局部变量的作用范围的时候这是很有用的。
do
    local a2 = 2*a
    local d = sqrt(b^2 - 4*a*c)
    x1 = (-b + d)/a2
    x2 = (-b - d)/a2
end            -- scope of 'a2' and 'd' ends here
 
print(x1, x2)
4.3 控制结构语句
控制结构的条件表达式结果可以是任何值,Lua认为falsenil为假,其他值为真。
if语句,有三种形式:
if conditions then
    then-part
end;
 
if conditions then
    then-part
else
    else-part
end;
 
if conditions then
    then-part
elseif conditions then
    elseif-part
..            --->多个elseif
else
    else-part
end;
while语句:
while condition do
    statements;
end;
repeat-until语句:
repeat
    statements;
until conditions;
for语句有两大类:
第一,数值for循环:
for var=exp1,exp2,exp3 do
    loop-part
end
for将用exp3作为stepexp1(初始值)到exp2(终止值),执行loop-part。其中exp3可以省略,默认step=1
有几点需要注意:
1. 三个表达式只会被计算一次,并且是在循环开始前。
for i=1,f(x) do
    print(i)
end
 
for i=10,1,-1 do
    print(i)
end
第一个例子f(x)只会在循环前被调用一次。
2. 控制变量var是局部变量自动被声明,并且只在循环内有效.
for i=1,10 do
    print(i)
end
max = i       -- probably wrong! 'i' here is global
如果需要保留控制变量的值,需要在循环中将其保存
-- find a value in a list
local found = nil
for i=1,a.n do
    if a[i] == value then
       found = i         -- save value of 'i'
       break
    end
end
print(found)
3. 循环过程中不要改变控制变量的值,那样做的结果是不可预知的。如果要退出循环,使用break语句。
 
第二,范型for循环:
前面已经见过一个例子:
-- print all values of array 'a'
for i,v in ipairs(a) do print(v) end
范型for遍历迭代子函数返回的每一个值。
再看一个遍历表key的例子:
-- print all keys of table 't'
for k in pairs(t) do print(k) end
范型for和数值for有两点相同:
1. 控制变量是局部变量
2. 不要修改控制变量的值
再看一个例子,假定有一个表:
days = {"Sunday", "Monday", "Tuesday", "Wednesday",
              "Thursday", "Friday", "Saturday"}
现在想把对应的名字转换成星期几,一个有效地解决问题的方式是构造一个反向表:
revDays = {["Sunday"] = 1, ["Monday"] = 2,
                     ["Tuesday"] = 3, ["Wednesday"] = 4,
                     ["Thursday"] = 5, ["Friday"] = 6, 
                     ["Saturday"] = 7}
下面就可以很容易获取问题的答案了:
x = "Tuesday"
print(revDays[x])        --> 3
我们不需要手工,可以自动构造反向表
revDays = {}
for i,v in ipairs(days) do
    revDays[v] = i
end
如果你对范型for还有些不清楚在后面的章节我们会继续来学习。
4.4 breakreturn语句
break语句用来退出当前循环(forrepeatwhile)。在循环外部不可以使用。
return用来从函数返回结果,当一个函数自然结束时,结尾会有一个默认的return。(这种函数类似pascal的过程(procedure))
Lua语法要求breakreturn只能出现在block的结尾一句(也就是说:作为chunk的最后一句,或者在end之前,或者else前,或者until前),例如:
local i = 1
while a[i] do
    if a[i] == v then break end
    i = i + 1
end
有时候为了调试或者其他目的需要在block的中间使用return或者break,可以显式的使用do..end来实现:
function foo ()
    return            --<< SYNTAX ERROR
    -- 'return' is the last statement in the next block
    do return end        -- OK
    ...               -- statements not reached
end

发表于 @ 2008年05月08日 11:37:50|评论(0)|编辑




新一篇: (LUA教程)第6章 再论函数 | 旧一篇: (LUA教程)第4章 基本语法

函数有两种用途:1.完成指定的任务,这种情况下函数作为调用语句使用;2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。
语法:
function func_name (arguments-list)
    statements-list;
end;
调用函数的时候,如果参数列表为空,必须使用()表明是函数调用。
print(8*9, 9/8)
a = math.sin(3) + math.cos(10)
print(os.date())
上述规则有一个例外,当函数只有一个参数并且这个参数是字符串或者表构造的时候,()可有可无:
print "Hello World"      <-->       print("Hello World")
dofile 'a.lua'           <-->       dofile ('a.lua')
print [[a multi-line     <-->       print([[a multi-line
           message]]                          message]])
f{x=10, y=20}            <-->       f({x=10, y=20})
type{}                   <-->       type({})
Lua也提供了面向对象方式调用函数的语法,比如o:foo(x)o.foo(o, x)是等价的,后面的章节会详细介绍面向对象内容。
Lua使用的函数,既可是Lua编写的,也可以是其他语言编写的,对于Lua程序员,用什么语言实现的函数使用起来都一样。
Lua函数实参和形参的匹配与赋值语句类似,多余部分被忽略,缺少部分用nil补足。
function f(a, b) return a or b end
 
CALL              PARAMETERS
 
f(3)              a=3, b=nil
f(3, 4)           a=3, b=4
f(3, 4, 5)        a=3, b=4   (5 is discarded)
5.1 多返回值
Lua函数可以返回多个结果值,比如string.find,其返回匹配串“开始和结束的下标”(如果不存在匹配串返回nil)。
s, e = string.find("hello Lua users", "Lua")
print(s, e)       --> 7 9
Lua函数中,在return后列出要返回的值得列表即可返回多值,如:
function maximum (a)
    local mi = 1             -- maximum index
    local m = a[mi]          -- maximum value
    for i,val in ipairs(a) do
       if val > m then
           mi = i
           m = val
       end
    end
    return m, mi
end
 
print(maximum({8,10,23,12,5}))     --> 23   3
Lua总是调整函数返回值的个数以适用调用环境,当作为独立的语句调用函数时,所有返回值将被忽略。假设有如下三个函数:
function foo0 () end                   -- returns no results
function foo1 () return 'a' end        -- returns 1 result
function foo2 () return 'a','b' end    -- returns 2 results
第一,当作为表达式调用函数时,有以下几种情况:
1. 当调用作为表达式最后一个参数或者仅有一个参数时,根据变量个数函数尽可能多地返回多个值,不足补nil,超出舍去。
2. 其他情况下,函数调用仅返回第一个值(如果没有返回值为nil
x,y = foo2()             -- x='a', y='b'
x = foo2()               -- x='a', 'b' is discarded
x,y,z = 10,foo2()        -- x=10, y='a', z='b'
 
x,y = foo0()             -- x=nil, y=nil
x,y = foo1()             -- x='a', y=nil
x,y,z = foo2()           -- x='a', y='b', z=nil
 
x,y = foo2(), 20         -- x='a', y=20
x,y = foo0(), 20, 30     -- x='nil', y=20, 30 is discarded
第二,函数调用作为函数参数被调用时,和多值赋值是相同。
print(foo0())            -->
print(foo1())            --> a
print(foo2())            --> a   b
print(foo2(), 1)         --> a   1
print(foo2() .. "x")     --> ax
第三,函数调用在表构造函数中初始化时,和多值赋值时相同。
a = {foo0()}             -- a = {}    (an empty table)
a = {foo1()}             -- a = {'a'}
a = {foo2()}             -- a = {'a', 'b'}
 
a = {foo0(), foo2(), 4} -- a[1] = nil, a[2] = 'a', a[3] = 4
另外,return f()这种形式,则返回“f()的返回值”:
function foo (i)
    if i == 0 then return foo0()
    elseif i == 1 then return foo1()
    elseif i == 2 then return foo2()
    end
end
 
print(foo(1))        --> a
print(foo(2))        --> a b
print(foo(0))        -- (no results)
print(foo(3))        -- (no results)
可以使用圆括号强制使调用返回一个值。
print((foo0()))      --> nil
print((foo1()))      --> a
print((foo2()))      --> a
一个return语句如果使用圆括号将返回值括起来也将导致返回一个值。
函数多值返回的特殊函数unpack,接受一个数组作为输入参数,返回数组的所有元素。unpack被用来实现范型调用机制,在C语言中可以使用函数指针调用可变的函数,可以声明参数可变的函数,但不能两者同时可变。在Lua中如果你想调用可变参数的可变函数只需要这样:
f(unpack(a))
unpack返回a所有的元素作为f()的参数
f = string.find
a = {"hello", "ll"}
print(f(unpack(a)))      --> 3  4
预定义的unpack函数是用C语言实现的,我们也可以用Lua来完成:
function unpack(t, i)
    i = i or 1
    if t[i] then
       return t[i], unpack(t, i + 1)
    end
end
5.2 可变参数
Lua函数可以接受可变数目的参数,和C语言类似在函数参数列表中使用三点(...)表示函数有可变的参数。Lua将函数的参数放在一个叫arg的表中,除了参数以外,arg表中还有一个域n表示参数的个数。
例如,我们可以重写print函数:
printResult = ""
 
function print(...)
    for i,v in ipairs(arg) do
       printResult = printResult .. tostring(v) .. "/t"
    end
    printResult = printResult .. "/n"
end
有时候我们可能需要几个固定参数加上可变参数
function g (a, b, ...) end
 
CALL              PARAMETERS
 
g(3)              a=3, b=nil, arg={n=0}
g(3, 4)           a=3, b=4, arg={n=0}
g(3, 4, 5, 8)     a=3, b=4, arg={5, 8; n=2}
如上面所示,Lua会将前面的实参传给函数的固定参数,后面的实参放在arg表中。
举个具体的例子,如果我们只想要string.find返回的第二个值。一个典型的方法是使用哑元(dummy variable,下划线):
local _, x = string.find(s, p)
-- now use `x'
...
还可以利用可变参数声明一个select函数:
function select (n, ...)
    return arg[n]
end
 
print(string.find("hello hello", " hel")) --> 6 9
print(select(1, string.find("hello hello", " hel"))) --> 6
print(select(2, string.find("hello hello", " hel"))) --> 9
有时候需要将函数的可变参数传递给另外的函数调用,可以使用前面我们说过的unpack(arg)返回arg表所有的可变参数,Lua提供了一个文本格式化的函数string.format(类似C语言的sprintf函数):
function fwrite(fmt, ...)
    return io.write(string.format(fmt, unpack(arg)))
end
这个例子将文本格式化操作和写操作组合为一个函数。
5.3 命名参数
Lua的函数参数是和位置相关的,调用时实参会按顺序依次传给形参。有时候用名字指定参数是很有用的,比如rename函数用来给一个文件重命名,有时候我们我们记不清命名前后两个参数的顺序了:
-- invalid code
rename(old="temp.lua", new="temp1.lua")
上面这段代码是无效的,Lua可以通过将所有的参数放在一个表中,把表作为函数的唯一参数来实现上面这段伪代码的功能。因为Lua语法支持函数调用时实参可以是表的构造。
rename{old="temp.lua", new="temp1.lua"}
根据这个想法我们重定义了rename
function rename (arg)
    return os.rename(arg.old, arg.new)
end
当函数的参数很多的时候,这种函数参数的传递方式很方便的。例如GUI库中创建窗体的函数有很多参数并且大部分参数是可选的,可以用下面这种方式:
w = Window {
    x=0, y=0, width=300, height=200,
    title = "Lua", background="blue",
    border = true
}
 
function Window (options)
    -- check mandatory options
    if type(options.title) ~= "string" then
       error("no title")
    elseif type(options.width) ~= "number" then
       error("no width")
    elseif type(options.height) ~= "number" then
       error("no height")
    end
 
    -- everything else is optional
    _Window(options.title,
       options.x or 0,          -- default value
       options.y or 0,          -- default value
       options.width, options.height,
       options.background or "white", -- default
       options.border           -- default is false (nil)
    )
end

发表于 @ 2008年05月08日 11:38:53|评论(0)|编辑


新一篇: (LUA教程)第7章 迭代器与泛型for | 旧一篇: (LUA教程)第5章 函数

Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。
第一类值指:在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。
词法定界指:嵌套的函数可以访问他外部函数中的变量。这一特性给Lua提供了强大的编程能力。
Lua中关于函数稍微难以理解的是函数也可以没有名字,匿名的。当我们提到函数名(比如print),实际上是说一个指向函数的变量,像持有其他类型值的变量一样:
a = {p = print}
a.p("Hello World")   --> Hello World
print = math.sin -- `print' now refers to the sine function
a.p(print(1))     --> 0.841470
sin = a.p         -- `sin' now refers to the print function
sin(10, 20)       --> 10   20
既然函数是值,那么表达式也可以创建函数了,Lua中我们经常这样写:
function foo (x) return 2*x end
这实际上是Lua语法的特例,下面是原本的函数:
foo = function (x) return 2*x end
函数定义实际上是一个赋值语句,将类型为function的变量赋给一个变量。我们使用function (x) ... end来定义一个函数和使用{}创建一个表一样。
table标准库提供一个排序函数,接受一个表作为输入参数并且排序表中的元素。这个函数必须能够对不同类型的值(字符串或者数值)按升序或者降序进行排序。Lua不是尽可能多地提供参数来满足这些情况的需要,而是接受一个排序函数作为参数(类似C++的函数对象),排序函数接受两个排序元素作为输入参数,并且返回两者的大小关系,例如:
network = {
    {name = "grauna",    IP = "210.26.30.34"},
    {name = "arraial",   IP = "210.26.30.23"},
    {name = "lua",       IP = "210.26.23.12"},
    {name = "derain",    IP = "210.26.23.20"},
}
如果我们想通过表的name域排序:
table.sort(network, function (a,b)
    return (a.name > b.name)
end)
以其他函数作为参数的函数在Lua中被称作高级函数(higher-order function),如上面的sort。在Lua中,高级函数与普通函数没有区别,它们只是把“作为参数的函数”当作第一类值(first-class value)处理而已。
下面给出一个绘图函数的例子:
function eraseTerminal()
    io.write("/27[2J")
end
 
-- writes an '*' at column 'x' , 'row y'
function mark (x,y)
    io.write(string.format("/27[%d;%dH*", y, x))
end
 
-- Terminal size
TermSize = {w = 80, h = 24}
 
-- plot a function
-- (assume that domain and image are in the range [-1,1])
function plot (f)
    eraseTerminal()
    for i=1,TermSize.w do
       local x = (i/TermSize.w)*2 - 1
       local y = (f(x) + 1)/2 * TermSize.h
       mark(i, y)
    end
    io.read() -- wait before spoiling the screen
end
要想让这个例子正确的运行,你必须调整你的终端类型和代码中的控制符[3]一致:
plot(function (x) return math.sin(x*2*math.pi) end)
将在屏幕上输出一个正弦曲线。
将第一类值函数应用在表中是Lua实现面向对象和包机制的关键,这部分内容在后面章节介绍。
6.1 闭包
当一个函数内部嵌套另一个函数定义时,内部的函数体可以访问外部的函数的局部变量,这种特征我们称作词法定界。虽然这看起来很清楚,事实并非如此,词法定界加上第一类函数在编程语言里是一个功能强大的概念,很少语言提供这种支持。
下面看一个简单的例子,假定有一个学生姓名的列表和一个学生名和成绩对应的表;现在想根据学生的成绩从高到低对学生进行排序,可以这样做:
names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
    return grades[n1] > grades[n2]    -- compare the grades
end)
假定创建一个函数实现此功能:
function sortbygrade (names, grades)
    table.sort(names, function (n1, n2)
       return grades[n1] > grades[n2]    -- compare the grades
    end)
end
例子中包含在sortbygrade函数内部的sort中的匿名函数可以访问sortbygrade的参数grades,在匿名函数内部grades不是全局变量也不是局部变量,我们称作外部的局部变量(external local variable)或者upvalue。(upvalue意思有些误导,然而在Lua中他的存在有历史的根源,还有他比起external local variable简短)。
看下面的代码:
function newCounter()
    local i = 0
    return function()    -- anonymous function
       i = i + 1
        return i
    end
end
 
c1 = newCounter()
print(c1()) --> 1
print(c1()) --> 2
匿名函数使用upvalue i保存他的计数,当我们调用匿名函数的时候i已经超出了作用范围,因为创建i的函数newCounter已经返回了。然而Lua用闭包的思想正确处理了这种情况。简单的说,闭包是一个函数以及它的upvalues。如果我们再次调用newCounter,将创建一个新的局部变量i,因此我们得到了一个作用在新的变量i上的新闭包。
c2 = newCounter()
print(c2()) --> 1
print(c1()) --> 3
print(c2()) --> 2
c1c2是建立在同一个函数上,但作用在同一个局部变量的不同实例上的两个不同的闭包。
技术上来讲,闭包指值而不是指函数,函数仅仅是闭包的一个原型声明;尽管如此,在不会导致混淆的情况下我们继续使用术语函数代指闭包。
闭包在上下文环境中提供很有用的功能,如前面我们见到的可以作为高级函数(sort)的参数;作为函数嵌套的函数(newCounter)。这一机制使得我们可以在Lua的函数世界里组合出奇幻的编程技术。闭包也可用在回调函数中,比如在GUI环境中你需要创建一系列button,但用户按下button时回调函数被调用,可能不同的按钮被按下时需要处理的任务有点区别。具体来讲,一个十进制计算器需要10个相似的按钮,每个按钮对应一个数字,可以使用下面的函数创建他们:
function digitButton (digit)
    return Button{ label = digit,
           action = function ()
              add_to_display(digit)
           end
    }
end
这个例子中我们假定Button是一个用来创建新按钮的工具, label是按钮的标签,action是按钮被按下时调用的回调函数。(实际上是一个闭包,因为他访问upvalue digit)。digitButton完成任务返回后,局部变量digit超出范围,回调函数仍然可以被调用并且可以访问局部变量digit
闭包在完全不同的上下文中也是很有用途的。因为函数被存储在普通的变量内我们可以很方便的重定义或者预定义函数。通常当你需要原始函数有一个新的实现时可以重定义函数。例如你可以重定义sin使其接受一个度数而不是弧度作为参数:
oldSin = math.sin
math.sin = function (x)
    return oldSin(x*math.pi/180)
end
更清楚的方式:
do
    local oldSin = math.sin
    local k = math.pi/180
    math.sin = function (x)
       return oldSin(x*k)
    end
end
这样我们把原始版本放在一个局部变量内,访问sin的唯一方式是通过新版本的函数。
利用同样的特征我们可以创建一个安全的环境(也称作沙箱,和java里的沙箱一样),当我们运行一段不信任的代码(比如我们运行网络服务器上获取的代码)时安全的环境是需要的,比如我们可以使用闭包重定义io库的open函数来限制程序打开的文件。
do
    local oldOpen = io.open
    io.open = function (filename, mode)
       if access_OK(filename, mode) then
           return oldOpen(filename, mode)
       else
           return nil, "access denied"
       end
    end
end
6.2 非全局函数
Lua中函数可以作为全局变量也可以作为局部变量,我们已经看到一些例子:函数作为table的域(大部分Lua标准库使用这种机制来实现的比如io.readmath.sin)。这种情况下,必须注意函数和表语法:
1. 表和函数放在一起
Lib = {}
Lib.foo = function (x,y) return x + y end
Lib.goo = function (x,y) return x - y end
2. 使用表构造函数
Lib = {
    foo = function (x,y) return x + y end,
    goo = function (x,y) return x - y end
}
3. Lua提供另一种语法方式
Lib = {}
function Lib.foo (x,y)
    return x + y
end
function Lib.goo (x,y)
    return x - y
end
当我们将函数保存在一个局部变量内时,我们得到一个局部函数,也就是说局部函数像局部变量一样在一定范围内有效。这种定义在包中是非常有用的:因为Luachunk当作函数处理,在chunk内可以声明局部函数(仅仅在chunk内可见),词法定界保证了包内的其他函数可以调用此函数。下面是声明局部函数的两种方式:
1. 方式一
local f = function (...)
    ...
end
 
local g = function (...)
    ...
    f()   -- external local `f' is visible here
    ...
end
2. 方式二
local function f (...)
    ...
end
有一点需要注意的是在声明递归局部函数的方式:
local fact = function (n)
    if n == 0 then
       return 1
    else
       return n*fact(n-1)   -- buggy
    end
end
上面这种方式导致Lua编译时遇到fact(n-1)并不知道他是局部函数factLua会去查找是否有这样的全局函数fact。为了解决这个问题我们必须在定义函数以前先声明:
local fact
 
fact = function (n)
    if n == 0 then
       return 1
    else
       return n*fact(n-1)
    end
end
这样在fact内部fact(n-1)调用是一个局部函数调用,运行时fact就可以获取正确的值了。
但是Lua扩展了他的语法使得可以在直接递归函数定义时使用两种方式都可以。
在定义非直接递归局部函数时要先声明然后定义才可以:
local f, g        -- `forward' declarations
 
function g ()
    ... f() ...
end
 
function f ()
    ... g() ...
end
6.3 正确的尾调用(Proper Tail Calls
Lua中函数的另一个有趣的特征是可以正确的处理尾调用(proper tail recursion,一些书使用术语“尾递归”,虽然并未涉及到递归的概念)。
尾调用是一种类似在函数结尾的goto调用,当函数最后一个动作是调用另外一个函数时,我们称这种调用尾调用。例如:
function f(x)
    return g(x)
end
g的调用是尾调用。
例子中f调用g后不会再做任何事情,这种情况下当被调用函数g结束时程序不需要返回到调用者f;所以尾调用之后程序不需要在栈中保留关于调用者的任何信息。一些编译器比如Lua解释器利用这种特性在处理尾调用时不使用额外的栈,我们称这种语言支持正确的尾调用。
由于尾调用不需要使用栈空间,那么尾调用递归的层次可以无限制的。例如下面调用不论n为何值不会导致栈溢出。
function foo (n)
    if n > 0 then return foo(n - 1) end
end
需要注意的是:必须明确什么是尾调用。
一些调用者函数调用其他函数后也没有做其他的事情但不属于尾调用。比如:
function f (x)
    g(x)
    return
end
上面这个例子中f在调用g后,不得不丢弃g地返回值,所以不是尾调用,同样的下面几个例子也不时尾调用:
return g(x) + 1      -- must do the addition
return x or g(x)     -- must adjust to 1 result
return (g(x))        -- must adjust to 1 result
Lua中类似return g(...)这种格式的调用是尾调用。但是gg的参数都可以是复杂表达式,因为Lua会在调用之前计算表达式的值。例如下面的调用是尾调用:
return x[i].foo(x[j] + a*b, i + j)
可以将尾调用理解成一种goto,在状态机的编程领域尾调用是非常有用的。状态机的应用要求函数记住每一个状态,改变状态只需要goto(or call)一个特定的函数。我们考虑一个迷宫游戏作为例子:迷宫有很多个房间,每个房间有东西南北四个门,每一步输入一个移动的方向,如果该方向存在即到达该方向对应的房间,否则程序打印警告信息。目标是:从开始的房间到达目的房间。
这个迷宫游戏是典型的状态机,每个当前的房间是一个状态。我们可以对每个房间写一个函数实现这个迷宫游戏,我们使用尾调用从一个房间移动到另外一个房间。一个四个房间的迷宫代码如下:
function room1 ()
    local move = io.read()
    if move == "south" then
       return room3()
    elseif move == "east" then
       return room2()
    else
       print("invalid move")
       return room1()   -- stay in the same room
    end
end
 
function room2 ()
    local move = io.read()
    if move == "south" then
       return room4()
    elseif move == "west" then
       return room1()
    else
       print("invalid move")
       return room2()
    end
end
 
function room3 ()
    local move = io.read()
    if move == "north" then
       return room1()
    elseif move == "east" then
       return room4()
    else
       print("invalid move")
       return room3()
    end
end
 
function room4 ()
    print("congratilations!")
end
我们可以调用room1()开始这个游戏。
如果没有正确的尾调用,每次移动都要创建一个栈,多次移动后可能导致栈溢出。但正确的尾调用可以无限制的尾调用,因为每次尾调用只是一个goto到另外一个函数并不是传统的函数调用。

发表于 @ 2008年05月08日 11:40:28|评论(0)|编辑



新一篇: (LUA教程)第8章 编译·运行·错误信息 | 旧一篇: (LUA教程)第6章 再论函数

在这一章我们讨论为范性for写迭代器,我们从一个简单的迭代器开始,然后我们学习如何通过利用范性for的强大之处写出更高效的迭代器。
7.1 迭代器与闭包
迭代器是一种支持指针类型的结构,它可以遍历集合的每一个元素。在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。
迭 代器需要保留上一次成功调用的状态和下一次成功调用的状态,也就是他知道来自于哪里和将要前往哪里。闭包提供的机制可以很容易实现这个任务。记住:闭包是 一个内部函数,它可以访问一个或者多个外部函数的外部局部变量。每次闭包的成功调用后这些外部局部变量都保存他们的值(状态)。当然如果要创建一个闭包必 须要创建其外部局部变量。所以一个典型的闭包的结构包含两个函数:一个是闭包自己;另一个是工厂(创建闭包的函数)。
举一个简单的例子,我们为一个list写一个简单的迭代器,与ipairs()不同的是我们实现的这个迭代器返回元素的值而不是索引下标:
function list_iter (t)
    local i = 0
    local n = table.getn(t)
    return function ()
       i = i + 1
       if i <= n then return t[i] end
    end
end
这个例子中list_iter是一个工厂,每次调用他都会创建一个新的闭包(迭代器本身)。闭包保存内部局部变量(t,i,n),因此每次调用他返回list中的下一个元素值,当list中没有值时,返回nil.我们可以在while语句中使用这个迭代器:
t = {10, 20, 30}
iter = list_iter(t)      -- creates the iterator
while true do
    local element = iter()   -- calls the iterator
    if element == nil then break end
    print(element)
end
我们设计的这个迭代器也很容易用于范性for语句
t = {10, 20, 30}
for element in list_iter(t) do
    print(element)
end
范性for为迭代循环处理所有的薄记(bookkeeping):首先调用迭代工厂;内部保留迭代函数,因此我们不需要iter变量;然后在每一个新的迭代处调用迭代器函数;当迭代器返回nil时循环结束(后面我们将看到范性for能胜任更多的任务)。
下面看一个稍微复杂一点的例子:我们写一个迭代器遍历一个文件内的所有匹配的单词。为了实现目的,我们需要保留两个值:当前行和在当前行的偏移量,我们使用两个外部局部变量linepos保存这两个值。
function allwords()
    local line = io.read()   -- current line
    local pos = 1            -- current position in the line
    return function ()       -- iterator function
       while line do        -- repeat while there are lines
       local s, e = string.find(line, "%w+", pos)
           if s then         -- found a word?
              pos = e + 1   -- next position is after this word
              return string.sub(line, s, e) -- return the word
           else
              line = io.read() -- word not found; try next line
              pos = 1       -- restart from first position
           end
       end
    return nil    -- no more lines: end of traversal
    end
end
迭代函数的主体部分调用了string.find函数,string.find在当前行从当前位置开始查找匹配的单词,例子中匹配的单词使用模式'%w+'描述的;如果查找到一个单词,迭代函数更新当前位置pos为单词后的第一个位置,并且返回这个单词(string.sub函数从line中提取两个位置参数之间的子串)。否则迭代函数读取新的一行并重新搜索。如果没有line可读返回nil结束。
尽管迭代函数有些复杂,但使用起来是很直观的:
for word in allwords() do
    print(word)
end
通常情况下,迭代函数大都难写易用。这不是大问题,一般Lua编程不需要自己写迭代函数,语言本身提供了许多。当然,必要时,自己动手构造一二亦可。
7.2 范性for的语义
前面我们看到的迭代器有一个缺点:每次调用都需要创建一个闭包,大多数情况下这种做法都没什么问题,例如在allwords迭代器中创建一个闭包的代价比起读整个文件来说微不足道,然而在有些情况下创建闭包的代价是不能忍受的。在这些情况下我们可以使用范性for本身来保存迭代的状态。
前面我们看到在循环过程中范性for在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。下面详细说明。
范性for的文法如下:
for <var-list> in <exp-list> do
    <body>
end
<var-list>是以一个或多个逗号分隔的变量名列表,<exp-list>是以一个或多个逗号分隔的表达式列表,通常情况下exp-list只有一个值:迭代工厂的调用。
for k, v in pairs(t) do
    print(k, v)
end
上面代码中,k, v为变量列表;pair(t)为表达式列表。
在很多情况下变量列表也只有一个变量,比如:
for line in io.lines() do
    io.write(line, '/n')
end
我们称变量列表中第一个变量为控制变量,其值为nil时循环结束。
下面我们看看范性for的执行过程:
首先,初始化,计算in后面表达式的值,表达式应该返回范性for需要的三个值:迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用nil补足,多出部分会被忽略。
第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于for结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
第三,将迭代函数返回的值赋给变量列表。
第四,如果返回的第一个值为nil循环结束,否则执行循环体。
第五,回到第二步再次调用迭代函数。
更具体地说:
for var_1, ..., var_n in explist do block end
等价于
do
    local _f, _s, _var = explist
    while true do
       local var_1, ... , var_n = _f(_s, _var)
       _var = var_1
       if _var == nil then break end
       block
    end
end
如果我们的迭代函数是f,状态常量是s,控制变量的初始值是a0,那么控制变量将循环:a1=f(s,a0)a2=f(s,a1)、……,直到ai=nil
7.3 无状态的迭代器
无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。这种无状态迭代器的典型的简单的例子是ipairs,他遍历数组的每一个元素。
a = {"one", "two", "three"}
for i, v in ipairs(a) do
    print(i, v)
end
迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs和迭代函数都很简单,我们在Lua中可以这样实现:
function iter (a, i)
    i = i + 1
    local v = a[i]
    if v then
       return i, v
    end
end
 
function ipairs (a)
    return iter, a, 0
end
Lua调用ipairs(a)开始循环时,他获取三个值:迭代函数iter、状态常量a、控制变量初始值0;然后Lua调用iter(a,0)返回1,a[1](除非a[1]=nil);第二次迭代调用iter(a,1)返回2,a[2]……直到第一个非nil元素。
Lua库中实现的pairs是一个用next实现的原始方法:
function pairs (t)
    return next, t, nil
end
还可以不使用ipairs直接使用next
for k, v in next, t do
    ...
end
记住:exp-list返回结果会被调整为三个,所以Lua获取nexttnil;确切地说当他调用pairs时获取。
7.4 多状态的迭代器
很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table内,将table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数。
下面我们重写allwords迭代器,这一次我们不是使用闭包而是使用带有两个域(line, pos)的table
开始迭代的函数是很简单的,他必须返回迭代函数和初始状态:
local iterator       -- to be defined later
 
function allwords()
    local state = {line = io.read(), pos = 1}
    return iterator, state
end
真正的处理工作是在迭代函数内完成:
function iterator (state)
    while state.line do      -- repeat while there are lines
       -- search for next word
       local s, e = string.find(state.line, "%w+", state.pos)
       if s then     -- found a word?
           -- update next position (after this word)
           state.pos = e + 1
           return string.sub(state.line, s, e)
       else   -- word not found
           state.line = io.read()   -- try next line...
           state.pos = 1     -- ... from first position
       end
    end
    return nil        -- no more lines: end loop
end
我们应该尽可能的写无状态的迭代器,因为这样循环的时候由for来保存状态,不需要创建对象花费的代价小;如果不能用无状态的迭代器实现,应尽可能使用闭包;尽可能不要使用table这种方式,因为创建闭包的代价要比创建table小,另外Lua处理闭包要比处理table速度快些。后面我们还将看到另一种使用协同来创建迭代器的方式,这种方式功能更强但更复杂。
7.5 真正的迭代器
迭代器的名字有一些误导,因为它并没有迭代,完成迭代功能的是for语句,也许更好的叫法应该是生成器(generator);但是在其他语言比如javaC++迭代器的说法已经很普遍了,我们也就沿用这个术语。
有一种方式创建一个在内部完成迭代的迭代器。这样当我们使用迭代器的时候就不需要使用循环了;我们仅仅使用每一次迭代需要处理的任务作为参数调用迭代器即可,具体地说,迭代器接受一个函数作为参数,并且这个函数在迭代器内部被调用。
作为一个具体的例子,我们使用上述方式重写allwords迭代器:
function allwords (f)
    -- repeat for each line in the file
    for l in io.lines() do
       -- repeat for each word in the line
       for w in string.gfind(l, "%w+") do
           -- call the function
           f(w)
       end
    end
end
如果我们想要打印出单词,只需要
allwords(print)
更一般的做法是我们使用匿名函数作为作为参数,下面的例子打印出单词'hello'出现的次数:
local count = 0
allwords(function (w)
    if w == "hello" then count = count + 1 end
end)
print(count)
for结构完成同样的任务:
local count = 0
for w in allwords() do
    if w == "hello" then count = count + 1 end
end
print(count)
真正的迭代器风格的写法在Lua老版本中很流行,那时还没有for循环。
两种风格的写法相差不大,但也有区别:一方面,第二种风格更容易书写和理解;另一方面,for结构更灵活,可以使用breakcontinue语句;在真正的迭代器风格写法中return语句只是从匿名函数中返回而不是退出循环. 

发表于 @ 2008年05月08日 11:41:33|评论(0)|编辑



新一篇: (LUA教程)第9章 协同程序 | 旧一篇: (LUA教程)第7章 迭代器与泛型for

虽然我们把Lua当作解释型语言,但是Lua会首先把代码预编译成中间码然后再执行(很多解释型语言都是这么做的)。在解释型语言中存在编译阶段听起来不合适,然而,解释型语言的特征不在于他们是否被编译,而是编译器是语言运行时的一部分,所以,执行编译产生的中间码速度会更快。我们可以说函数dofile的存在就是说明可以将Lua作为一种解释型语言被调用。
前面我们介绍过dofile,把它当作Lua运行代码的chunk的一种原始的操作。dofile实际上是一个辅助的函数。真正完成功能的函数是loadfile;与dofile不同的是loadfile编译代码成中间码并且返回编译后的chunk作为一个函数,而不执行代码;另外loadfile不会抛出错误信息而是返回错误码。我们可以这样定义dofile
function dofile (filename)
    local f = assert(loadfile(filename))
    return f()
end
如果loadfile失败assert会抛出错误。
完成简单的功能dofile比较方便,他读入文件编译并且执行。然而loadfile更加灵活。在发生错误的情况下,loadfile返回nil和错误信息,这样我们就可以自定义错误处理。另外,如果我们运行一个文件多次的话,loadfile只需要编译一次,但可多次运行。dofile却每次都要编译。
loadstringloadfile相似,只不过它不是从文件里读入chunk,而是从一个串中读入。例如:
f = loadstring("i = i + 1")
f将是一个函数,调用时执行i=i+1
i = 0
f(); print(i)     --> 1
f(); print(i)     --> 2
loadstring函数功能强大,但使用时需多加小心。确认没有其它简单的解决问题的方法再使用。
Lua把每一个chunk都作为一个匿名函数处理。例如:chunk "a = 1"loadstring返回与其等价的function () a = 1 end
与其他函数一样,chunks可以定义局部变量也可以返回值:
f = loadstring("local a = 10; return a + 20")
print(f())        --> 30
loadfileloadstring都不会抛出错误,如果发生错误他们将返回nil加上错误信息:
print(loadstring("i i"))
       --> nil    [string "i i"]:1: '=' expected near 'i'
另外,loadfileloadstring都不会有边界效应产生,他们仅仅编译chunk成为自己内部实现的一个匿名函数。通常对他们的误解是他们定义了函数。Lua中的函数定义是发生在运行时的赋值而不是发生在编译时。假如我们有一个文件foo.lua
-- file `foo.lua'
function foo (x)
    print(x)
end
当我们执行命令f = loadfile("foo.lua")后,foo被编译了但还没有被定义,如果要定义他必须运行chunk
f()           -- defines `foo'
foo("ok")     --> ok
如果你想快捷的调用dostring(比如加载并运行),可以这样
loadstring(s)()
调用loadstring返回的结果,然而如果加载的内容存在语法错误的话,loadstring返回nil和错误信息(attempt to call a nil value);为了返回更清楚的错误信息可以使用assert
assert(loadstring(s))()
通常使用loadstring加载一个字串没什么意义,例如:
f = loadstring("i = i + 1")
大概与f = function () i = i + 1 end等价,但是第二段代码速度更快因为它只需要编译一次,第一段代码每次调用loadstring都会重新编译,还有一个重要区别:loadstring编译的时候不关心词法范围:
local i = 0
f = loadstring("i = i + 1")
g = function () i = i + 1 end
这个例子中,和想象的一样g使用局部变量i,然而f使用全局变量iloadstring总是在全局环境中编译他的串。
loadstring通常用于运行程序外部的代码,比如运行用户自定义的代码。注意:loadstring期望一个chunk,即语句。如果想要加载表达式,需要在表达式前加return,那样将返回表达式的值。看例子:
print "enter your expression:"
local l = io.read()
local func = assert(loadstring("return " .. l))
print("the value of your expression is " .. func())
loadstring返回的函数和普通函数一样,可以多次被调用:
print "enter function to be plotted (with variable 'x'):"
local l = io.read()
local f = assert(loadstring("return " .. l))
for i=1,20 do
    x = i   -- global 'x' (to be visible from the chunk)
    print(string.rep("*", f()))
end
8.1 require函数
Lua提供高级的require函数来加载运行库。粗略的说requiredofile完成同样的功能但有两点不同:
1.       require会搜索目录加载文件
2.       require会判断是否文件已经加载避免重复加载同一文件。由于上述特征,requireLua中是加载库的更好的函数。
require使用的路径和普通我们看到的路径还有些区别,我们一般见到的路径都是一个目录列表。require的路径是一个模式列表,每一个模式指明一种由虚文件名(require的参数)转成实文件名的方法。更明确地说,每一个模式是一个包含可选的问号的文件名。匹配的时候Lua会首先将问号用虚文件名替换,然后看是否有这样的文件存在。如果不存在继续用同样的方法用第二个模式匹配。例如,路径如下:
?;?.lua;c:/windows/?;/usr/local/lua/?/?.lua
调用require "lili"时会试着打开这些文件:
lili
lili.lua
c:/windows/lili
/usr/local/lua/lili/lili.lua
require关注的问题只有分号(模式之间的分隔符)和问号,其他的信息(目录分隔符,文件扩展名)在路径中定义。
为了确定路径,Lua首先检查全局变量LUA_PATH是否为一个字符串,如果是则认为这个串就是路径;否则require检查环境变量LUA_PATH的值,如果两个都失败require使用固定的路径(典型的"?;?.lua"
require的另一个功能是避免重复加载同一个文件两次。Lua保留一张所有已经加载的文件的列表(使用table保存)。如果一个加载的文件在表中存在require简单的返回;表中保留加载的文件的虚名,而不是实文件名。所以如果你使用不同的虚文件名require同一个文件两次,将会加载两次该文件。比如require "foo"require "foo.lua",路径为"?;?.lua"将会加载foo.lua两次。我们也可以通过全局变量_LOADED访问文件名列表,这样我们就可以判断文件是否被加载过;同样我们也可以使用一点小技巧让require加载一个文件两次。比如,require "foo"之后_LOADED["foo"]将不为nil,我们可以将其赋值为nilrequire "foo.lua"将会再次加载该文件。
一个路径中的模式也可以不包含问号而只是一个固定的路径,比如:
?;?.lua;/usr/local/default.lua
这种情况下,require没有匹配的时候就会使用这个固定的文件(当然这个固定的路径必须放在模式列表的最后才有意义)。在require运行一个chunk以前,它定义了一个全局变量_REQUIREDNAME用来保存被required的虚文件的文件名。我们可以通过使用这个技巧扩展require的功能。举个极端的例子,我们可以把路径设为"/usr/local/lua/newrequire.lua",这样以后每次调用require都会运行newrequire.lua,这种情况下可以通过使用_REQUIREDNAME的值去实际加载required的文件。
8.2 C Packages
LuaC是很容易结合的,使用CLua写包。与Lua中写包不同,C包在使用以前必须首先加载并连接,在大多数系统中最容易的实现方式是通过动态连接库机制,然而动态连接库不是ANSI C的一部分,也就是说在标准C中实现动态连接是很困难的。
通常Lua不包含任何不能用标准C实现的机制,动态连接库是一个特例。我们可以将动态连接库机制视为其他机制之母:一旦我们拥有了动态连接机制,我们就可以动态的加载Lua中不存在的机制。所以,在这种特殊情况下,Lua打破了他平台兼容的原则而通过条件编译的方式为一些平台实现了动态连接机制。标准的LuawindowsLinuxFreeBSDSolaris和其他一些Unix平台实现了这种机制,扩展其它平台支持这种机制也是不难的。在Lua提示符下运行print(loadlib())看返回的结果,如果显示bad arguments则说明你的发布版支持动态连接机制,否则说明动态连接机制不支持或者没有安装。
Lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:库的绝对路径和初始化函数。所以典型的调用的例子如下:
local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")
loadlib函数加载指定的库并且连接到Lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为Lua的一个函数,这样我们就可以直接在Lua中调用他。如果加载动态库或者查找初始化函数时出错,loadlib将返回nil和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:
local path = "/usr/local/lua/lib/libluasocket.so"
-- or path = "C://windows//luasocket.dll"
local f = assert(loadlib(path, "luaopen_socket"))
f() -- actually open the library
一般情况下我们期望二进制的发布库包含一个与前面代码段相似的stub文件,安装二进制库的时候可以随便放在某个目录,只需要修改stub文件对应二进制库的实际路径即可。将stub文件所在的目录加入到LUA_PATH,这样设定后就可以使用require函数加载C库了。
8.3 错误
Errare humanum est(拉丁谚语:犯错是人的本性)。所以我们要尽可能的防止错误的发生,Lua经常作为扩展语言嵌入在别的应用中,所以不能当错误发生时简单的崩溃或者退出。相反,当错误发生时Lua结束当前的chunk并返回到应用中。
Lua遇到不期望的情况时就会抛出错误,比如:两个非数字进行相加;调用一个非函数的变量;访问表中不存在的值等(可以通过metatables修改这种行为,后面介绍)。你也可以通过调用error函数显式地抛出错误,error的参数是要抛出的错误信息。
print "enter a number:"
n = io.read("*number")
if not n then error("invalid input") end
Lua提供了专门的内置函数assert来完成上面类似的功能:
print "enter a number:"
n = assert(io.read("*number"), "invalid input")
assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。第二个参数是可选的。注意,assert会首先处理两个参数,然后才调用函数,所以下面代码,无论n是否为数字,字符串连接操作总会执行:
n = io.read()
assert(tonumber(n), "invalid input: " .. n .. " is not a number")
当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误。选择哪一种方式,没有固定的规则,不过基本的原则是:对于程序逻辑上能够避免的异常,以抛出错误的方式处理之,否则返回错误代码。
例如sin函数,假定我们让sin碰到错误时返回错误代码,则使用sin的代码可能变为:
local res = math.sin(x)
if not res then      -- error
    ...
当然,我们也可以在调用sin前检查x是否为数字:
if not tonumber(x) then     -- error: x is not a number
    ...
而事实上,我们既不是检查参数也不是检查返回结果,因为参数错误可能意味着我们的程序某个地方存在问题,这种情况下,处理异常最简单最实际的方式是抛出错误并且终止代码的运行。
再来看一个例子。io.open函数用于打开文件,如果文件不存在,结果会如何?很多系统中,我们通过“试着去打开文件”来判断文件是否存在。所以如果io.open不能打开文件(由于文件不存在或者没有权限),函数返回nil和错误信息。依据这种方式,我们可以通过与用户交互(比如:是否要打开另一个文件)合理地处理问题:
local file, msg
repeat
    print "enter a file name:"
    local name = io.read()
    if not name then return end     -- no input
    file, msg = io.open(name, "r")
    if not file then print(msg) end
until file
如果你想偷懒不想处理这些情况,又想代码安全的运行,可以使用assert
file = assert(io.open(name, "r"))
Lua中有一个习惯:如果io.open失败,assert将抛出错误。
file = assert(io.open("no-file", "r"))
       --> stdin:1: no-file: No such file or directory
注意:io.open返回的第二个结果(错误信息)会作为assert的第二个参数。
8.4 异常和错误处理
很多应用中,不需要在Lua进行错误处理,一般有应用来完成。通常应用要求Lua运行一段chunk,如果发生异常,应用根据Lua返回的错误代码进行处理。在控制台模式下的Lua解释器如果遇到异常,打印出错误然后继续显示提示符等待下一个命令。
如果在Lua中需要处理错误,需要使用pcall函数封装你的代码。
假定你想运行一段Lua代码,这段代码运行过程中可以捕捉所有的异常和错误。
第一步:将这段代码封装在一个函数内
function foo ()
    ...
    if unexpected_condition then error() end
    ...
    print(a[i])   -- potential error: `a' may not be a table
    ...
end
第二步:使用pcall调用这个函数
if pcall(foo) then
    -- no errors while running `foo'
    ...
else
    -- `foo' raised an error: take appropriate actions
    ...
end
当然也可以用匿名函数的方式调用pcall
if pcall(function () ... end) then ...
else ...
pcall在保护模式(protected mode)下执行函数内容,同时捕获所有的异常和错误。若一切正常,pcall返回true以及“被执行函数”的返回值;否则返回nil和错误信息。
错误信息不一定仅为字符串(下面的例子是一个table),传递给error的任何信息都会被pcall返回:
local status, err = pcall(function () error({code=121}) end)
print(err.code) --> 121
这种机制提供了强大的能力,足以应付Lua中的各种异常和错误情况。我们通过error抛出异常,然后通过pcall捕获之。
8.5 错误信息和回跟踪(Tracebacks
虽然你可以使用任何类型的值作为错误信息,通常情况下,我们使用字符串来描述遇到的错误。如果遇到内部错误(比如对一个非table的值使用索引下标访问)Lua将自己产生错误信息,否则Lua使用传递给error函数的参数作为错误信息。不管在什么情况下,Lua都尽可能清楚的描述问题发生的缘由。
local status, err = pcall(function () a = 'a'+1 end)
print(err)
--> stdin:1: attempt to perform arithmetic on a string value
 
local status, err = pcall(function () error("my error") end)
print(err)
--> stdin:1: my error
例子中错误信息给出了文件名(stdin)与行号。
函数error还可以有第二个参数,表示错误发生的层级。比如,你写了一个函数用来检查“error是否被正确调用”:
function foo (str)
    if type(str) ~= "string" then
       error("string expected")
    end
    ...
end
可有人这样调用此函数:
foo({x=1})
Lua会指出发生错误的是foo而不是error,实际上,错误是调用error时产生的。为了纠正这个问题,修改前面的代码让error报告错误发生在第二级(你自己的函数是第一级)如下:
function foo (str)
    if type(str) ~= "string" then
       error("string expected", 2)
    end
    ...
end
当错误发生的时候,我们常常希望了解详细的信息,而不仅是错误发生的位置。若能了解到“错误发生时的栈信息”就好了,但pcall返回错误信息时,已经释放了保存错误发生情况的栈信息。因此,若想得到tracebacks,我们必须在pcall返回以前获取。Lua提供了xpcall来实现这个功能,xpcall接受两个参数:调用函数、错误处理函数。当错误发生时,Lua会在栈释放以前调用错误处理函数,因此可以使用debug库收集错误相关信息。有两个常用的debug处理函数:debug.debugdebug.traceback,前者给出Lua的提示符,你可以自己动手察看错误发生时的情况;后者通过traceback创建更多的错误信息,也是控制台解释器用来构建错误信息的函数。你可以在任何时候调用debug.traceback获取当前运行的traceback信息:
print(debug.traceback())

发表于 @ 2008年05月08日 11:42:47|评论(0)|编辑



新一篇: (LUA教程)第10章 完整示例 | 旧一篇: (LUA教程)第8章 编译·运行·错误信息

协同程序(coroutine)与多线程情况下的线程比较类似:有自己的堆栈,自己的局部变量,有自己的指令指针(IPinstruction pointer),但与其它协同程序共享全局变量等很多信息。线程和协同程序的主要不同在于:在多处理器情况下,从概念上来讲多线程程序同时运行多个线程;而协同程序是通过协作来完成,在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只在必要时才会被挂起。
协同是非常强大的功能,但是用起来也很复杂。如果你是第一次阅读本章,某些例子可能会不大理解,不必担心,可先继续阅读后面的章节,再回头琢磨本章内容。
9.1 协同的基础
Lua的所有协同函数存放于coroutine table中。create函数用于创建新的协同程序,其只有一个参数:一个函数,即协同程序将要运行的代码。若一切顺利,返回值为thread类型,表示创建成功。通常情况下,create的参数是匿名函数:
co = coroutine.create(function ()
    print("hi")
end)
 
print(co)     --> thread: 0x8071d98
协同有三个状态:挂起态(suspended)、运行态(running)、停止态(dead)。当我们创建协同程序成功时,其为挂起态,即此时协同程序并未运行。我们可用status函数检查协同的状态:
print(coroutine.status(co))     --> suspended
函数coroutine.resume使协同程序由挂起状态变为运行态:
coroutine.resume(co)            --> hi
本例中,协同程序打印出"hi"后,任务完成,便进入终止态:
print(coroutine.status(co))     --> dead
当目前为止,协同看起来只是一种复杂的调用函数的方式,真正的强大之处体现在yield函数,它可以将正在运行的代码挂起,看一个例子:
co = coroutine.create(function ()
    for i=1,10 do
       print("co", i)
       coroutine.yield()
    end
end)
执行这个协同程序,程序将在第一个yield处被挂起:
coroutine.resume(co)            --> co   1
print(coroutine.status(co))     --> suspended
从协同的观点看:使用函数yield可以使程序挂起,当我们激活被挂起的程序时,将从函数yield的位置继续执行程序,直到再次遇到yield或程序结束。
coroutine.resume(co)     --> co   2
coroutine.resume(co)     --> co   3
...
coroutine.resume(co)     --> co   10
coroutine.resume(co)     -- prints nothing
上面最后一次调用时,协同体已结束,因此协同程序处于终止态。如果我们仍然希望激活它,resume将返回false和错误信息。
print(coroutine.resume(co))
        --> false   cannot resume dead coroutine
注意:resume运行在保护模式下,因此,如果协同程序内部存在错误,Lua并不会抛出错误,而是将错误返回给resume函数。
Lua中协同的强大能力,还在于通过resume-yield来交换数据。
第一个例子中只有resume,没有yieldresume把参数传递给协同的主程序。
co = coroutine.create(function (a,b,c)
    print("co", a,b,c)
end)
coroutine.resume(co, 1, 2, 3)      --> co 1 2 3
第二个例子,数据由yield传给resumetrue表明调用成功,true之后的部分,即是yield的参数。
co = coroutine.create(function (a,b)
    coroutine.yield(a + b, a - b)
end)
print(coroutine.resume(co, 20, 10))    --> true 30 10
相应地,resume的参数,会被传递给yield
co = coroutine.create (function ()
    print("co", coroutine.yield())
end)
coroutine.resume(co)
coroutine.resume(co, 4, 5)      --> co 4 5
最后一个例子,协同代码结束时的返回值,也会传给resume
co = coroutine.create(function ()
    return 6, 7
end)
print(coroutine.resume(co))     --> true 6 7
我们很少在一个协同程序中同时使用多个特性,但每一种都有用处。
现在已大体了解了协同的基础内容,在我们继续学习之前,先澄清两个概念:Lua的协同称为不对称协同(asymmetric coroutines),指“挂起一个正在执行的协同函数”与“使一个被挂起的协同再次执行的函数”是不同的,有些语言提供对称协同(symmetric coroutines),即使用同一个函数负责“执行与挂起间的状态切换”。
有人称不对称的协同为半协同,另一些人使用同样的术语表示真正的协同,严格意义上的协同不论在什么地方只要它不是在其他的辅助代码内部的时候都可以并且只能使执行挂起,不论什么时候在其控制栈内都不会有不可决定的调用。(However, other people use the same term semi-coroutine to denote a restricted implementation of coroutines, where a coroutine can only suspend its execution when it is not inside any auxiliary function, that is, when it has no pending calls in its control stack.)。只有半协同程序内部可以使用yieldpython中的产生器(generator)就是这种类型的半协同。
与对称的协同和不对称协同的区别不同的是,协同与产生器的区别更大。产生器相对比较简单,他不能完成真正的协同所能完成的一些任务。我们熟练使用不对称的协同之后,可以利用不对称的协同实现比较优越的对称协同。
9.2 管道和过滤器
协同最具代表性的例子是用来解决生产者-消费者问题。假定有一个函数不断地生产数据(比如从文件中读取),另一个函数不断的处理这些数据(比如写到另一文件中),函数如下:
function producer ()
    while true do
       local x = io.read()      -- produce new value
       send(x)                  -- send to consumer
    end
end
 
function consumer ()
    while true do
       local x = receive()      -- receive from producer
       io.write(x, "/n")        -- consume new value
    end
end
(例子中生产者和消费者都在不停的循环,修改一下使得没有数据的时候他们停下来并不困难),问题在于如何使得receivesend协同工作。只是一个典型的谁拥有主循环的情况,生产者和消费者都处在活动状态,都有自己的主循环,都认为另一方是可调用的服务。对于这种特殊的情况,可以改变一个函数的结构解除循环,使其作为被动的接受。然而这种改变在某些特定的实际情况下可能并不简单。
协同为解决这种问题提供了理想的方法,因为调用者与被调用者之间的resume-yield关系会不断颠倒。当一个协同调用yield时并不会进入一个新的函数,取而代之的是返回一个未决的resume的调用。相似的,调用resume时也不会开始一个新的函数而是返回yield的调用。这种性质正是我们所需要的,与使得send-receive协同工作的方式是一致的。receive唤醒生产者生产新值,send把产生的值送给消费者消费。
function receive ()
    local status, value = coroutine.resume(producer)
    return value
end
 
function send (x)
    coroutine.yield(x)
end
 
producer = coroutine.create( function ()
    while true do
       local x = io.read()      -- produce new value
       send(x)
    end
end)
这种设计下,开始时调用消费者,当消费者需要值时他唤起生产者生产值,生产者生产值后停止直到消费者再次请求。我们称这种设计为消费者驱动的设计。
我们可以使用过滤器扩展这个设计,过滤器指在生产者与消费者之间,可以对数据进行某些转换处理。过滤器在同一时间既是生产者又是消费者,他请求生产者生产值并且转换格式后传给消费者,我们修改上面的代码加入过滤器(给每一行前面加上行号)。完整的代码如下:
function receive (prod)
    local status, value = coroutine.resume(prod)
    return value
end
 
function send (x)
    coroutine.yield(x)
end
 
function producer ()
    return coroutine.create(function ()
       while true do
           local x = io.read()      -- produce new value
           send(x)
       end
    end)
end
 
function filter (prod)
    return coroutine.create(function ()
       local line = 1
       while true do
           local x = receive(prod) -- get new value
           x = string.format("%5d %s", line, x)
           send(x)       -- send it to consumer
           line = line + 1
       end
    end)
end
 
function consumer (prod)
    while true do
       local x = receive(prod) -- get new value
       io.write(x, "/n")        -- consume new value
    end
end
可以调用:
p = producer()
f = filter(p)
consumer(f)
或者:
consumer(filter(producer()))
看完上面这个例子你可能很自然的想到UNIX的管道,协同是一种非抢占式的多线程。管道的方式下,每一个任务在独立的进程中运行,而协同方式下,每个任务运行在独立的协同代码中。管道在读(consumer)与写(producer)之间提供了一个缓冲,因此两者相关的的速度没有什么限制,在上下文管道中这是非常重要的,因为在进程间的切换代价是很高的。协同模式下,任务间的切换代价较小,与函数调用相当,因此读写可以很好的协同处理。
9.3 用作迭代器的协同
我们可以将循环的迭代器看作生产者-消费者模式的特殊的例子。迭代函数产生值给循环体消费。所以可以使用协同来实现迭代器。协同的一个关键特征是它可以不断颠倒调用者与被调用者之间的关系,这样我们毫无顾虑的使用它实现一个迭代器,而不用保存迭代函数返回的状态。
我们来完成一个打印一个数组元素的所有的排列来阐明这种应用。直接写这样一个迭代函数来完成这个任务并不容易,但是写一个生成所有排列的递归函数并不难。思路是这样的:将数组中的每一个元素放到最后,依次递归生成所有剩余元素的排列。代码如下:
function permgen (a, n)
    if n == 0 then
       printResult(a)
    else
       for i=1,n do
 
           -- put i-th element as the last one
           a[n], a[i] = a[i], a[n]
 
           -- generate all permutations of the other elements
           permgen(a, n - 1)
 
           -- restore i-th element
           a[n], a[i] = a[i], a[n]
 
       end
    end
end
 
function printResult (a)
    for i,v in ipairs(a) do
       io.write(v, " ")
    end
    io.write("/n")
end
 
permgen ({1,2,3,4}, 4)
有了上面的生成器后,下面我们将这个例子修改一下使其转换成一个迭代函数:
1. 第一步printResult 改为 yield
function permgen (a, n)
    if n == 0 then
       coroutine.yield(a)
    else
       ...
2. 第二步,我们定义一个迭代工厂,修改生成器在生成器内创建迭代函数,并使生成器运行在一个协同程序内。迭代函数负责请求协同产生下一个可能的排列。
function perm (a)
    local n = table.getn(a)
    local co = coroutine.create(function () permgen(a, n) end)
    return function ()   -- iterator
       local code, res = coroutine.resume(co)
       return res
    end
end
这样我们就可以使用for循环来打印出一个数组的所有排列情况了:
for p in perm{"a", "b", "c"} do
    printResult(p)
end
--> b c a
--> c b a
--> c a b
--> a c b
--> b a c
--> a b c
perm函数使用了Lua中常用的模式:将一个对协同的resume的调用封装在一个函数内部,这种方式在Lua非常常见,所以Lua专门为此专门提供了一个函数coroutine.wrap。与create相同的是,wrap创建一个协同程序;不同的是wrap不返回协同本身,而是返回一个函数,当这个函数被调用时将resume协同。wrapresume协同的时候不会返回错误代码作为第一个返回结果,一旦有错误发生,将抛出错误。我们可以使用wrap重写perm
function perm (a)
    local n = table.getn(a)
    return coroutine.wrap(function () permgen(a, n) end)
end
一般情况下,coroutine.wrapcoroutine.create使用起来简单直观,前者更确切的提供了我们所需要的:一个可以resume协同的函数,然而缺少灵活性,没有办法知道wrap所创建的协同的状态,也没有办法检查错误的发生。
9.4 非抢占式多线程
如前面所见,Lua中的协同是一协作的多线程,每一个协同等同于一个线程,yield-resume可以实现在线程中切换。然而与真正的多线程不同的是,协同是非抢占式的。当一个协同正在运行时,不能在外部终止他。只能通过显示的调用yield挂起他的执行。对于某些应用来说这个不存在问题,但有些应用对此是不能忍受的。不存在抢占式调用的程序是容易编写的。不需要考虑同步带来的bugs,因为程序中的所有线程间的同步都是显示的。你仅仅需要在协同代码超出临界区时调用yield即可。
对非抢占式多线程来说,不管什么时候只要有一个线程调用一个阻塞操作(blocking operation),整个程序在阻塞操作完成之前都将停止。对大部分应用程序而言,只是无法忍受的,这使得很多程序员离协同而去。下面我们将看到这个问题可以被有趣的解决。
看一个多线程的例子:我们想通过http协议从远程主机上下在一些文件。我们使用Diego Nehab开发的LuaSocket库来完成。我们先看下在一个文件的实现,大概步骤是打开一个到远程主机的连接,发送下载文件的请求,开始下载文件,下载完毕后关闭连接。
第一,加载LuaSocket
require "luasocket"
第二,定义远程主机和需要下载的文件名
host = "www.w3.org"
file = "/TR/REC-html32.html"
第三,打开一个TCP连接到远程主机的80端口(http服务的标准端口)
c = assert(socket.connect(host, 80))
上面这句返回一个连接对象,我们可以使用这个连接对象请求发送文件
c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")
receive函数返回他送接收到的数据加上一个表示操作状态的字符串。当主机断开连接时,我们退出循环。
第四,关闭连接
c:close()
现 在我们知道了如何下载一个文件,下面我们来看看如何下载多个文件。一种方法是我们在一个时刻只下载一个文件,这种顺序下载的方式必须等前一个文件下载完成 后一个文件才能开始下载。实际上是,当我们发送一个请求之后有很多时间是在等待数据的到达,也就是说大部分时间浪费在调用receive上。 如果同时可以下载多个文件,效率将会有很大提高。当一个连接没有数据到达时,可以从另一个连接读取数据。很显然,协同为这种同时下载提供了很方便的支持, 我们为每一个下载任务创建一个线程,当一个线程没有数据到达时,他将控制权交给一个分配器,由分配器唤起另外的线程读取数据。
使用协同机制重写上面的代码,在一个函数内:
function download (host, file)
    local c = assert(socket.connect(host, 80))
    local count = 0      -- counts number of bytes read
    c:send("GET " .. file .. " HTTP/1.0/r/n/r/n")
    while true do
       local s, status = receive©
       count = count + string.len(s)
       if status == "closed" then break end
    end
    c:close()
    print(file, count)
end
由于我们不关心文件的内容,上面的代码只是计算文件的大小而不是将文件内容输出。(当有多个线程下载多个文件时,输出会混杂在一起),在新的函数代码中,我们使用receive从远程连接接收数据,在顺序接收数据的方式下代码如下:
function receive (connection)
    return connection:receive(2^10)
end
在同步接受数据的方式下,函数接收数据时不能被阻塞,而是在没有数据可取时yield,代码如下:
function receive (connection)
    connection:timeout(0)    -- do not block
    local s, status = connection:receive(2^10)
    if status == "timeout" then
       coroutine.yield(connection)
    end
    return s, status
end
调用函数timeout(0)使得对连接的任何操作都不会阻塞。当操作返回的状态为timeout时意味着操作未完成就返回了。在这种情况下,线程yield。非false的数值作为yield的参数告诉分配器线程仍在执行它的任务。(后面我们将看到分配器需要timeout连接的情况),注意:即使在timeout模式下,连接依然返回他接受到直到timeout为止,因此receive会一直返回s给她的调用者。
下面的函数保证每一个下载运行在自己独立的线程内:
threads = {}      -- list of all live threads
function get (host, file)
    -- create coroutine
    local co = coroutine.create(function ()
       download(host, file)
    end)
    -- insert it in the list
    table.insert(threads, co)
end
代码中table中为分配器保存了所有活动的线程。
分配器代码是很简单的,它是一个循环,逐个调用每一个线程。并且从线程列表中移除已经完成任务的线程。当没有线程可以运行时退出循环。
function dispatcher ()
    while true do
       local n = table.getn(threads)
       if n == 0 then break end    -- no more threads to run
       for i=1,n do
           local status, res = coroutine.resume(threads[i])
           if not res then   -- thread finished its task?
              table.remove(threads, i)
              break
           end
       end
    end
end
最后,在主程序中创建需要的线程调用分配器,例如:从W3C站点上下载4个文件:
host = "www.w3c.org"
 
get(host, "/TR/html401/html40.txt")
get(host, "/TR/2002/REC-xhtml1-20020801/xhtml1.pdf")
get(host, "/TR/REC-html32.html")
get(host,
    "/TR/2000/REC-DOM-Level-2-Core-20001113/DOM2-Core.txt")
 
dispatcher()      -- main loop
使用协同方式下,我的机器花了6s下载完这几个文件;顺序方式下用了15s,大概2倍的时间。
尽管效率提高了,但距离理想的实现还相差甚远,当至少有一个线程有数据可读取的时候,这段代码可以很好的运行。否则,分配器将进入忙等待状态,从一个线程到另一个线程不停的循环判断是否有数据可获取。结果协同实现的代码比顺序读取将花费30倍的CPU时间。
为了避免这种情况出现,我们可以使用LuaSocket库中的select函数。当程序在一组socket中不断的循环等待状态改变时,它可以使程序被阻塞。我们只需要修改分配器,使用select函数修改后的代码如下:
function dispatcher ()
    while true do
       local n = table.getn(threads)
       if n == 0 then break end    -- no more threads to run
       local connections = {}
       for i=1,n do
           local status, res = coroutine.resume(threads[i])
           if not res then   -- thread finished its task?
              table.remove(threads, i)
              break
           else   -- timeout
              table.insert(connections, res)
           end
       end
       if table.getn(connections) == n then
           socket.select(connections)
       end
    end
end
在内层的循环分配器收集连接表中timeout地连接,注意:receive将连接传递给yield,因此resume返回他们。当所有的连接都timeout分配器调用select等待任一连接状态的改变。最终的实现效率和上一个协同实现的方式相当,另外,他不会发生忙等待,比起顺序实现的方式消耗CPU的时间仅仅多一点点。
 

发表于 @ 2008年05月08日 11:44:22|评论(0)|编辑


新一篇: (LUA教程)第11章 数据结构 | 旧一篇: (LUA教程)第9章 协同程序

本章通过两个完整的例子,来展现Lua的实际应用。第一个例子来自于Lua官方网站,其展示了Lua作为数据描述语言的应用。第二个例子为马尔可夫链算法的实现,算法在Kernighan & Pike著作的Practice of Programming书中有描述。本章结束后,Lua语言方面的介绍便到此结束。后续章节将分别介绍table与面向对象(object-orient)、标准库以及C-API等内容。
10.1 Lua作为数据描述语言使用
慢慢地,Lua正被世界上越来越多的人使用。Lua官方网站的数据库中保存着一些“使用了Lua”的项目的信息。在数据库中,我们用一个构造器以自动归档的方式表示每个工程入口,代码如下:
entry{
    title = "Tecgraf",
    org = "Computer Graphics Technology Group, PUC-Rio",
    url = "http://www.tecgraf.puc-rio.br/",
    contact = "Waldemar Celes",
    description = [[
    TeCGraf is the result of a partnership between PUC-Rio,
    the Pontifical Catholic University of Rio de Janeiro,
    and <A HREF="http://www.petrobras.com.br/">PETROBRAS</A>,
    the Brazilian Oil Company.
    TeCGraf is Lua's birthplace,
    and the language has been used there since 1993.
    Currently, more than thirty programmers in TeCGraf use
    Lua regularly; they have written more than two hundred
    thousand lines of code, distributed among dozens of
    final products.]]
}
有趣的是,工程入口是存放在Lua文件中的,每个工程入口以table的形式作为参数去调用entry函数。我们的目的是写一个程序将这些数据以html格式展示出来。由于工程太多,我们首先列出工程的标题,然后显示每个工程的明细。结果如下:
<HTML>
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.
 
 
<UL>
<LI><A HREF="#1">TeCGraf</A>
<LI> ...
</UL>
 
<H3>
<A NAME="1"
    HREF="http://www.tecgraf.puc-rio.br/">TeCGraf</A>
 
 
<SMALL><EM>Computer Graphics Technology Group,
PUC-Rio</EM></SMALL>
</H3>
 
TeCGraf is the result of a partnership between
...
distributed among dozens of final products.<P>
Contact: Waldemar Celes
 
<A NAME="2"></A><HR>
...
 
</BODY></HTML>
为了读取数据,我们需要做的是正确的定义函数entry,然后使用dofile直接运行数据文件(db.lua)即可。注意,我们需要遍历入口列表两次,第一次为了获取标题,第二次为了获取每个工程的表述。一种方法是:使用相同的entry函数运行数据文件一次将所有的入口放在一个数组内;另一种方法:使用不同的entry函数运行数据文件两次。因为Lua编译文件是很快的,这里我们选用第二种方法。
首先,我们定义一个辅助函数用来格式化文本的输出(参见5.2函数部分内容)
function fwrite (fmt, ...)
    return io.write(string.format(fmt, unpack(arg)))
end
第二,我们定义一个BEGIN函数用来写html页面的头部
function BEGIN()
io.write([[
<HTML>
<HEAD><TITLE>Projects using Lua</TITLE></HEAD>
<BODY BGCOLOR="#FFFFFF">
Here are brief descriptions of some projects around the
world that use <A HREF="home.html">Lua</A>.
]])
end
第三,定义entry函数
a. 第一个entry函数,将每个工程一列表方式写出,entry的参数o是描述工程的table
function entry0 (o)
    N=N + 1
    local title = o.title or '(no title)'
    fwrite('<LI><A HREF="#%d">%s</A>/n', N, title)
end
如果o.titlenil表明table中的域title没有提供,我们用固定的"no title"替换。
b. 第二个entry函数,写出工程所有的相关信息,稍微有些复杂,因为所有项都是可选的。
function entry1 (o)
    N=N + 1
    local title = o.title or o.org or 'org'
    fwrite('<HR>/n<H3>/n')
    local href = ''
 
    if o.url then
       href = string.format(' HREF="%s"', o.url)
    end
    fwrite('<A NAME="%d"%s>%s</A>/n', N, href, title)
 
    if o.title and o.org then
       fwrite('/n<SMALL><EM>%s</EM></SMALL>', o.org)
    end
    fwrite('/n</H3>/n')
 
    if o.description then
       fwrite('%s', string.gsub(o.description,
                     '/n/n/n*', '<P>/n'))
       fwrite('<P>/n')
    end
 
    if o.email then
       fwrite('Contact: <A HREF="mailto:%s">%s</A>/n',
              o.email, o.contact or o.email)
    elseif o.contact then
       fwrite('Contact: %s/n', o.contact)
    end
end
由于html中使用双引号,为了避免冲突我们这里使用单引号表示串。
第四,定义END函数,写html的尾部
function END()
fwrite('</BODY></HTML>/n')
end
在主程序中,我们首先使用第一个entry运行数据文件输出工程名称的列表,然后再以第二个entry运行数据文件输出工程相关信息。
BEGIN()
 
N = 0
entry = entry0
fwrite('<UL>/n')
dofile('db.lua')
fwrite('</UL>/n')
 
N = 0
entry = entry1
dofile('db.lua')
 
END()
10.2 马尔可夫链算法
我们第二个例子是马尔可夫链算法的实现,我们的程序以前n(n=2)个单词串为基础随机产生一个文本串。
程 序的第一部分读出原文,并且对没两个单词的前缀建立一个表,这个表给出了具有那些前缀的单词的一个顺序。建表完成后,这个程序利用这张表生成一个随机的文 本。在此文本中,每个单词都跟随着它的的前两个单词,这两个单词在文本中有相同的概率。这样,我们就产生了一个非常随机,但并不完全随机的文本。例如,当 应用这个程序的输出结果会出现“构造器也可以通过表构造器,那么一下几行的插入语对于整个文件来说,不是来存储每个功能的内容,而是来展示它的结构。”如 果你想在队列里找到最大元素并返回最大值,接着显示提示和运行代码。下面的单词是保留单词,不能用在度和弧度之间转换。
我们编写一个函数用来将两个单词中间加上空个连接起来:
function prefix (w1, w2)
    return w1 .. ' ' .. w2
end
我们用NOWORD(即/n)表示文件的结尾并且初始化前缀单词,例如,下面的文本:
the more we try the more we do
初始化构造的表为:
{
    ["/n /n"]     = {"the"},
    ["/n the"]    = {"more"},
    ["the more"] = {"we", "we"},
    ["more we"]   = {"try", "do"},
    ["we try"]    = {"the"},
    ["try the"]   = {"more"},
    ["we do"]     = {"/n"},
}
我们使用全局变量statetab来保存这个表,下面我们完成一个插入函数用来在这个statetab中插入新的单词。
function insert (index, value)
    if not statetab[index] then
       statetab[index] = {value}
    else
       table.insert(statetab[index], value)
    end
end
这个函数中首先检查指定的前缀是否存在,如果不存在则创建一个新的并赋上新值。如果已经存在则调用table.insert将新值插入到列表尾部。
我们使用两个变量w1w2来保存最后读入的两个单词的值,对于每一个前缀,我们保存紧跟其后的单词的列表。例如上面例子中初始化构造的表。
初始化表之后,下面来看看如何生成一个MAXGEN=1000)个单词的文本。首先,重新初始化w1w2,然后对于每一个前缀,在其next单词的列表中随机选择一个,打印此单词并更新w1w2,完整的代码如下:
-- Markov Chain Program in Lua
 
function allwords ()
    local line = io.read() -- current line
    local pos = 1 -- current position in the line
    return function () -- iterator function
       while line do -- repeat while there are lines
           local s, e = string.find(line, "%w+", pos)
           if s then -- found a word?
              pos = e + 1 -- update next position
              return string.sub(line, s, e) -- return the word
           else
              line = io.read() -- word not found; try next line
              pos = 1 -- restart from first position
           end
       end
       return nil -- no more lines: end of traversal
    end
end
 
function prefix (w1, w2)
    return w1 .. ' ' .. w2
end
 
local statetab
 
function insert (index, value)
    if not statetab[index] then
       statetab[index] = {n=0}
    end
    table.insert(statetab[index], value)
end
 
local N = 2
local MAXGEN = 10000
local NOWORD = "/n"
 
-- build table
statetab = {}
local w1, w2 = NOWORD, NOWORD
for w in allwords() do
    insert(prefix(w1, w2), w)
    w1 = w2; w2 = w;
end
insert(prefix(w1, w2), NOWORD)
 
-- generate text
w1 = NOWORD; w2 = NOWORD -- reinitialize
for i=1,MAXGEN do
    local list = statetab[prefix(w1, w2)]
    -- choose a random item from list
    local r = math.random(table.getn(list))
    local nextword = list[r]
    if nextword == NOWORD then return end
    io.write(nextword, " ")
    w1 = w2; w2 = nextword
end

发表于 @ 2008年05月08日 11:45:41|评论(0)|编辑


新一篇: (LUA教程)第12章 数据文件与持久化 | 旧一篇: (LUA教程)第10章 完整示例

tableLua中唯一的数据结构,其他语言所提供的数据结构,如:arraysrecordslistsqueuessets等,Lua都是通过table来实现,并且在luatable很好的实现了这些数据结构。
在传统的C语言或者Pascal语言中我们经常使用arrayslistsrecord+pointer)来实现大部分的数据结构,在Lua中不仅可以用table完成同样的功能,而且table的功能更加强大。通过使用table很多算法的实现都简化了,比如你在lua中很少需要自己去实现一个搜索算法,因为table本身就提供了这样的功能。
我们需要花一些时间去学习如何有效的使用table,下面通过一些例子,我们来看看如果通过table来实现一些常用的数据结构。首先,我们从arrayslists开始,因为两者是其他数据结构的基础,大家也比较熟悉。前面章节,我们已接触了table的一些内容,本章,我们将彻底了解它。
11.1 数组
lua中通过整数下标访问table中元素,即是数组。并且数组大小不固定,可动态增长。
通常我们初始化数组时,就间接地定义了数组的大小,例如:
a = {}     -- new array
for i=1, 1000 do
    a[i] = 0
end
数组a的大小为1000,访问1-1000范围外的值,将返回nil。数组下标可以根据需要,从任意值开始,比如:
-- creates an array with indices from -5 to 5
a = {}
for i=-5, 5 do
    a[i] = 0
end
然而习惯上,Lua的下标从1开始。Lua的标准库遵循此惯例,因此你的数组下标必须也是从1开始,才可以使用标准库的函数。
我们可以用构造器在创建数组的同时初始化数组:
squares = {1, 4, 9, 16, 25, 36, 49, 64, 81}
这样的语句中,数组的大小可以任意的大。
11.2 矩阵和多维数组
Lua中有两种表示矩阵的方法,一是“数组的数组”。也就是说,table的每个元素是另一个table。例如,可以使用下面代码创建一个nm列的矩阵:
mt = {}           -- create the matrix
for i=1,N do
    mt[i] = {}    -- create a new row
    for j=1,M do
       mt[i][j] = 0
    end
end
由于Luatable是对象,所以每一行我们必须显式地创建一个table,比起cpascal,这显得冗余,但另一方面也提供了更多的灵活性,例如可修改前面的例子创建一个三角矩阵:
for j=1,M do
改成
for j=1,i do
这样实现的三角矩阵比起整个矩阵,仅使用一半的内存空间。
表示矩阵的另一方法,是将行和列组合起来。如果索引下标都是整数,通过第一个索引乘于一个常量(列)再加上第二个索引,看下面的例子实现创建nm列的矩阵:
mt = {}           -- create the matrix
for i=1,N do
    for j=1,M do
       mt[i*M + j] = 0
    end
end
如果索引是字符串,可用一个单字符将两个字符串索引连接起来构成一个单一的索引下标,例如一个矩阵m,索引下标为st,假定st都不包含冒号,代码为:m[s..':'..t],如果s或者t包含冒号将导致混淆,比如("a:", "b") ("a", ":b"),当对这种情况有疑问的时候可以使用控制字符来连接两个索引字符串,比如'/0'
实际应用中常常使用稀疏矩阵,稀疏矩阵指矩阵的大部分元素都为空或者0的矩阵。例如,我们通过图的邻接矩阵来存储图,也就是说:当m,n两个节点有连接时,矩阵的m,n值为对应的x,否则为nil。如果一个图有10000个节点,平均每个节点大约有5条边,为了存储这个图需要一个行列分别为10000的矩阵,总计10000*10000个元素,实际上大约只有50000个元素非空(每行有五列非空,与每个节点有五条边对应)。很多数据结构的书上讨论采用何种方式才能节省空间,但是在Lua中你不需要这些技术,因为用table实现的数据本身天生的就具有稀疏的特性。如果用我们上面说的第一种多维数组来表示,需要10000table,每个table大约需要五个元素(table);如果用第二种表示方法来表示,只需要一张大约50000个元素的表,不管用那种方式,你只需要存储那些非nil的元素。
11.3 链表
Lua中用tables很容易实现链表,每一个节点是一个table,指针是这个表的一个域(field),并且指向另一个节点(table)。例如,要实现一个只有两个域:值和指针的基本链表,代码如下:
根节点:
list = nil
在链表开头插入一个值为v的节点:
list = {next = list, value = v}
要遍历这个链表只需要:
local l = list
while l do
    print(l.value)
    l = l.next
end
其他类型的链表,像双向链表和循环链表类似的也是很容易实现的。然后在Lua中在很少情况下才需要这些数据结构,因为通常情况下有更简单的方式来替换链表。比如,我们可以用一个非常大的数组来表示栈,其中一个域n指向栈顶。
11.4 队列和双向队列
虽然可以使用Luatable库提供的insertremove操作来实现队列,但这种方式实现的队列针对大数据量时效率太低,有效的方式是使用两个索引下标,一个表示第一个元素,另一个表示最后一个元素。
function ListNew ()
    return {first = 0, last = -1}
end
为了避免污染全局命名空间,我们重写上面的代码,将其放在一个名为listtable中:
List = {}
function List.new ()
    return {first = 0, last = -1}
end
下面,我们可以在常量时间内,完成在队列的两端进行插入和删除操作了。
function List.pushleft (list, value)
    local first = list.first - 1
    list.first = first
    list[first] = value
end
 
function List.pushright (list, value)
    local last = list.last + 1
    list.last = last
    list[last] = value
end
 
function List.popleft (list)
    local first = list.first
    if first > list.last then error("list is empty") end
    local value = list[first]
    list[first] = nil    -- to allow garbage collection
    list.first = first + 1
    return value
end
 
function List.popright (list)
    local last = list.last
    if list.first > last then error("list is empty") end
    local value = list[last]
    list[last] = nil     -- to allow garbage collection
    list.last = last - 1
    return value
end
对严格意义上的队列来讲,我们只能调用pushrightpopleft,这样以来,firstlast的索引值都随之增加,幸运的是我们使用的是Luatable实现的,你可以访问数组的元素,通过使用下标从120,也可以16,777,216  16,777,236。另外,Lua使用双精度表示数字,假定你每秒钟执行100万次插入操作,在数值溢出以前你的程序可以运行200年。
11.5 集合和包
假定你想列出在一段源代码中出现的所有标示符,某种程度上,你需要过滤掉那些语言本身的保留字。一些C程序员喜欢用一个字符串数组来表示,将所有的保留字放在数组中,对每一个标示符到这个数组中查找看是否为保留字,有时候为了提高查询效率,对数组存储的时候使用二分查找或者hash算法。
Lua中表示这个集合有一个简单有效的方法,将所有集合中的元素作为下标存放在一个table里,下面不需要查找table,只需要测试看对于给定的元素,表的对应下标的元素值是否为nil。比如:
reserved = {
    ["while"] = true,    ["end"] = true,
    ["function"] = true, ["local"] = true,
}
 
for w in allwords() do
    if reserved[w] then
    -- `w' is a reserved word
    ...
还可以使用辅助函数更加清晰的构造集合:
function Set (list)
    local set = {}
    for _, l in ipairs(list) do set[l] = true end
    return set
end
 
reserved = Set{"while", "end", "function", "local", }
11.6 字符串缓冲
假定你要拼接很多个小的字符串为一个大的字符串,比如,从一个文件中逐行读入字符串。你可能写出下面这样的代码:
-- WARNING: bad code ahead!!
local buff = ""
for line in io.lines() do
    buff = buff .. line .. "/n"
end
尽管这段代码看上去很正常,但在Lua中他的效率极低,在处理大文件的时候,你会明显看到很慢,例如,需要花大概1分钟读取350KB的文件。(这就是为什么Lua专门提供了io.read(*all)选项,她读取同样的文件只需要0.02s
为什么这样呢?Lua使用真正的垃圾收集算法,但他发现程序使用太多的内存他就会遍历他所有的数据结构去释放垃圾数据,一般情况下,这个算法有很好的性能(Lua的快并非偶然的),但是上面那段代码loop使得算法的效率极其低下。
为了理解现象的本质,假定我们身在loop中间,buff已经是一个50KB的字符串,每一行的大小为20bytes,当Lua执行buff..line.."/n"时,她创建了一个新的字符串大小为50,020 bytes,并且从buff中将50KB的字符串拷贝到新串中。也就是说,对于每一行,都要移动50KB的内存,并且越来越多。读取100行的时候(仅仅2KB),Lua已经移动了5MB的内存,使情况变遭的是下面的赋值语句:
buff = buff .. line .. "/n"
老的字符串变成了垃圾数据,两轮循环之后,将有两个老串包含超过100KB的垃圾数据。这个时候Lua会做出正确的决定,进行他的垃圾收集并释放100KB的内存。问题在于每两次循环Lua就要进行一次垃圾收集,读取整个文件需要进行200次垃圾收集。并且它的内存使用是整个文件大小的三倍。
这个问题并不是Lua特有的:其它的采用垃圾收集算法的并且字符串不可变的语言也都存在这个问题。Java是最著名的例子,Java专门提供StringBuffer来改善这种情况。
在继续进行之前,我们应该做个注释的是,在一般情况下,这个问题并不存在。对于小字符串,上面的那个循环没有任何问题。为了读取整个文件我们可以使用io.read(*all),可以很快的将这个文件读入内存。但是在某些时候,没有解决问题的简单的办法,所以下面我们将介绍更加高效的算法来解决这个问题。
我 们最初的算法通过将循环每一行的字符串连接到老串上来解决问题,新的算法避免如此:它连接两个小串成为一个稍微大的串,然后连接稍微大的串成更大的 串。。。算法的核心是:用一个栈,在栈的底部用来保存已经生成的大的字符串,而小的串从栈定入栈。栈的状态变化和经典的汉诺塔问题类似:位于栈下面的串肯 定比上面的长,只要一个较长的串入栈后比它下面的串长,就将两个串合并成一个新的更大的串,新生成的串继续与相邻的串比较如果长于底部的将继续进行合并, 循环进行到没有串可以合并或者到达栈底。
function newStack ()
    return {""}   -- starts with an empty string
end
 
 
function addString (stack, s)
    table.insert(stack, s)   -- push 's' into the the stack
    for i=table.getn(stack)-1, 1, -1 do
       if string.len(stack[i]) > string.len(stack[i+1]) then
           break
       end
       stack[i] = stack[i] .. table.remove(stack)
    end
end
要想获取最终的字符串,我们只需要从上向下一次合并所有的字符串即可。table.concat函数可以将一个列表的所有串合并。
使用这个新的数据结构,我们重写我们的代码:
local s = newStack()
for line in io.lines() do
    addString(s, line .. "/n")
end
s = toString(s)
最终的程序读取350KB的文件只需要0.5s,当然调用io.read("*all")仍然是最快的只需要0.02s
实际上,我们调用io.read("*all")的时候,io.read就是使用我们上面的数据结构,只不过是用C实现的,在Lua标准库中,有些其他函数也是用C实现的,比如table.concat,使用table.concat我们可以很容易的将一个table的中的字符串连接起来,因为它使用C实现的,所以即使字符串很大它处理起来速度还是很快的。
Concat接受第二个可选的参数,代表插入的字符串之间的分隔符。通过使用这个参数,我们不需要在每一行之后插入一个新行:
local t = {}
for line in io.lines() do
    table.insert(t, line)
end
s = table.concat(t, "/n") .. "/n"
io.lines迭代子返回不带换行符的一行,concat在字符串之间插入分隔符,但是最后一字符串之后不会插入分隔符,因此我们需要在最后加上一个分隔符。最后一个连接操作复制了整个字符串,这个时候整个字符串可能是很大的。我们可以使用一点小技巧,插入一个空串:
table.insert(t, "")
s = table.concat(t, "/n")

发表于 @ 2008年05月08日 11:46:50|评论(0)|编辑


新一篇: (LUA教程)第14章 环境 | 旧一篇: (LUA教程)第11章 数据结构

当 我们处理数据文件的,一般来说,写文件比读取文件内容来的容易。因为我们可以很好的控制文件的写操作,而从文件读取数据常常碰到不可预知的情况。一个健壮 的程序不仅应该可以读取存有正确格式的数据还应该能够处理坏文件(译者注:对数据内容和格式进行校验,对异常情况能够做出恰当处理)。正因为如此,实现一 个健壮的读取数据文件的程序是很困难的。
正如我们在Section 10.1(译者:第10Complete Examples)中看到的例子,文件格式可以通过使用Lua中的table构造器来描述。我们只需要在写数据的稍微做一些做一点额外的工作,读取数据将变得容易很多。方法是:将我们的数据文件内容作为Lua代码写到Lua程序中去。通过使用table构造器,这些存放在Lua代码中的数据可以像其他普通的文件一样看起来引人注目。
为了更清楚地描述问题,下面我们看看例子。如果我们的数据是预先确定的格式,比如CSV(逗号分割值),我们几乎没得选择。(在第20章,我们介绍如何在Lua中处理CSV文件)。但是如果我们打算创建一个文件为了将来使用,除了CSV,我们可以使用Lua构造器来我们表述我们数据,这种情况下,我们将每一个数据记录描述为一个Lua构造器。将下面的代码
Donald E. Knuth,Literate Programming,CSLI,1992
Jon Bentley,More Programming Pearls,Addison-Wesley,1990
写成
Entry{"Donald E. Knuth",
"Literate Programming",
"CSLI",
1992}
 
Entry{"Jon Bentley",
"More Programming Pearls",
"Addison-Wesley",
1990}
记住Entry{...}Entry({...})等价,他是一个以表作为唯一参数的函数调用。所以,前面那段数据在Lua程序中表示如上。如果要读取这个段数据,我们只需要运行我们的Lua代码。例如下面这段代码计算数据文件中记录数:
local count = 0
function Entry (b) count = count + 1 end
dofile("data")
print("number of entries: " .. count)
下面这段程序收集一个作者名列表中的名字是否在数据文件中出现,如果在文件中出现则打印出来。(作者名字是Entry的第一个域;所以,如果b是一个entry的值,b[1]则代表作者名)
local authors = {}       -- a set to collect authors
function Entry (b) authors[b[1]] = true end
dofile("data")
for name in pairs(authors) do print(name) end
注意,在这些程序段中使用事件驱动的方法:Entry函数作为回调函数,dofile处理数据文件中的每一记录都回调用它。当数据文件的大小不是太大的情况下,我们可以使用name-value对来描述数据:
Entry{
author = "Donald E. Knuth",
title = "Literate Programming",
publisher = "CSLI",
year = 1992
}
 
Entry{
author = "Jon Bentley",
title = "More Programming Pearls",
publisher = "Addison-Wesley",
year = 1990
}
(如果这种格式让你想起BibTeX,这并不奇怪。Lua中构造器正是根据来自BibTeX的灵感实现的)这种格式我们称之为自描述数据格式,因为每一个数据段都根据他的意思简短的描述为一种数据格式。相对CSV和其他紧缩格式,自描述数据格式更容易阅读和理解,当需要修改的时候可以容易的手工编辑,而且不需要改动数据文件。例如,如果我们想增加一个域,只需要对读取程序稍作修改即可,当指定的域不存在时,也可以赋予默认值。使用name-value对描述的情况下,上面收集作者名的代码可以改写为:
local authors = {} -- a set to collect authors
function Entry (b) authors[b.author] = true end
dofile("data")
for name in pairs(authors) do print(name) end
现在,记录域的顺序无关紧要了,甚至某些记录即使不存在author这个域,我们也只需要稍微改动一下代码即可:
function Entry (b)
if b.author then authors[b.author] = true end
end
Lua不仅运行速度快,编译速度也快。例如,上面这段搜集作者名的代码处理一个2MB的数据文件时间不会超过1秒。另外,这不是偶然的,数据描述是Lua的主要应用之一,从Lua发明以来,我们花了很多心血使他能够更快的编译和运行大的chunks
12.1 序列化
我们经常需要序列化一些数据,为了将数据转换为字节流或者字符流,这样我们就可以保存到文件或者通过网络发送出去。我们可以在Lua代码中描述序列化的数据,在这种方式下,我们运行读取程序即可从代码中构造出保存的值。
通常,我们使用这样的方式varname = <exp>来保存一个全局变量的值。varname部分比较容易理解,下面我们来看看如何写一个产生值的代码。对于一个数值来说:
function serialize (o)
    if type(o) == "number" then
       io.write(o)
    else ...
end
对于字符串值而言,原始的写法应该是:
if type(o) == "string" then
    io.write("'", o, "'")
然而,如果字符串包含特殊字符(比如引号或者换行符),产生的代码将不是有效的Lua程序。这时候你可能用下面方法解决特殊字符的问题:
if type(o) == "string" then
    io.write("[[", o, "]]")
千万不要这样做!双引号是针对手写的字符串的而不是针对自动产生的字符串。如果有人恶意的引导你的程序去使用" ]]..os.execute('rm *')..[[ "这样的方式去保存某些东西(比如它可能提供字符串作为地址)你最终的chunk将是这个样子:
varname = [[ ]]..os.execute('rm *')..[[ ]]
如果你load这个数据,运行结果可想而知的。为了以安全的方式引用任意的字符串,string标准库提供了格式化函数专门提供"%q"选项。它可以使用双引号表示字符串并且可以正确的处理包含引号和换行等特殊字符的字符串。这样一来,我们的序列化函数可以写为:
function serialize (o)
    if type(o) == "number" then
       io.write(o)
    elseif type(o) == "string" then
       io.write(string.format("%q", o))
    else ...
end
12.1.1保存不带循环的table
我们下一个艰巨的任务是保存表。根据表的结构不同,采取的方法也有很多。没有一种单一的算法对所有情况都能很好地解决问题。简单的表不仅需要简单的算法而且输出文件也需要看起来美观。
我们第一次尝试如下:
function serialize (o)
    if type(o) == "number" then
       io.write(o)
    elseif type(o) == "string" then
       io.write(string.format("%q", o))
    elseif type(o) == "table" then
       io.write("{/n")
       for k,v in pairs(o) do
           io.write(" ", k, " = ")
           serialize(v)
           io.write(",/n")
       end
       io.write("}/n")
    else
       error("cannot serialize a " .. type(o))
    end
end
尽 管代码很简单,但很好地解决了问题。只要表结构是一个树型结构(也就是说,没有共享的子表并且没有循环),上面代码甚至可以处理嵌套表(表中表)。对于所 进不整齐的表我们可以少作改进使结果更美观,这可以作为一个练习尝试一下。(提示:增加一个参数表示缩进的字符串,来进行序列化)。前面的函数假定表中出 现的所有关键字都是合法的标示符。如果表中有不符合Lua语法的数字关键字或者字符串关键字,上面的代码将碰到麻烦。一个简单的解决这个难题的方法是将:
io.write(" ", k, " = ")
改为
io.write(" [")
serialize(k)
io.write("] = ")
这样一来,我们改善了我们的函数的健壮性,比较一下两次的结果:
-- result of serialize{a=12, b='Lua', key='another "one"'}
-- 第一个版本
{
a = 12,
b = "Lua",
key = "another /"one/"",
}
 
-- 第二个版本
{
["a"] = 12,
["b"] = "Lua",
["key"] = "another /"one/"",
}
我们可以通过测试每一种情况,看是否需要方括号,另外,我们将这个问题留作一个练习给大家。
12.1.2保存带有循环的table
针对普通拓扑概念上的带有循环表和共享子表的table,我们需要另外一种不同的方法来处理。构造器不能很好地解决这种情况,我们不使用。为了表示循环我们需要将表名记录下来,下面我们的函数有两个参数:table和对应的名字。另外,我们还必须记录已经保存过的table以防止由于循环而被重复保存。我们使用一个额外的table来记录保存过的表的轨迹,这个表的下表索引为table,而值为对应的表名。
我们做一个限制:要保存的table只有一个字符串或者数字关键字。下面的这个函数序列化基本类型并返回结果。
function basicSerialize (o)
    if type(o) == "number" then
       return tostring(o)
    else       -- assume it is a string
       return string.format("%q", o)
    end
end
关键内容在接下来的这个函数,saved这个参数是上面提到的记录已经保存的表的踪迹的table
function save (name, value, saved)
    saved = saved or {}         -- initial value
    io.write(name, " = ")
    if type(value) == "number" or type(value) == "string" then
       io.write(basicSerialize(value), "/n")
    elseif type(value) == "table" then
       if saved[value] then     -- value already saved?
           -- use its previous name
           io.write(saved[value], "/n")
       else
           saved[value] = name -- save name for next time
           io.write("{}/n")     -- create a new table
           for k,v in pairs(value) do -- save its fields
              local fieldname = string.format("%s[%s]", name,
                                        basicSerialize(k))
              save(fieldname, v, saved)
           end
       end
    else
       error("cannot save a " .. type(value))
    end
end
举个例子:
我们将要保存的table为:
a = {x=1, y=2; {3,4,5}}
a[2] = a      -- cycle
a.z = a[1]    -- shared sub-table
调用save('a', a)之后结果为:
a = {}
a[1] = {}
a[1][1] = 3
a[1][2] = 4
a[1][3] = 5
 
a[2] = a
a["y"] = 2
a["x"] = 1
a["z"] = a[1]
(实际的顺序可能有所变化,它依赖于table遍历的顺序,不过,这个算法保证了一个新的定义中需要的前面的节点都已经被定义过)
如果我们想保存带有共享部分的表,我们可以使用同样tablesaved参数调用save函数,例如我们创建下面两个表:
a = {{"one", "two"}, 3}
b = {k = a[1]}
保存它们:
save('a', a)
save('b', b)
结果将分别包含相同部分:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = {}
b["k"][1] = "one"
b["k"][2] = "two"
然而如果我们使用同一个saved表来调用save函数:
local t = {}
save('a', a, t)
save('b', b, t)
结果将共享相同部分:
a = {}
a[1] = {}
a[1][1] = "one"
a[1][2] = "two"
a[2] = 3
b = {}
b["k"] = a[1]
上面这种方法是Lua中常用的方法,当然也有其他一些方法可以解决问题。比如,我们可以不使用全局变量名来保存,即使用封包,用chunk构造一个local值然后返回之;通过构造一张表,每张表名与其对应的函数对应起来等。Lua给予你权力,由你决定如何实现。

发表于 @ 2008年05月08日 11:47:50|评论(0)|编辑


新一篇: (LUA教程)第14章 环境 | 旧一篇: (LUA教程)第14章 环境

Lua中的table由于定义的行为,我们可以对key-value对执行加操作,访问key对应的value,遍历所有的key-value。但是我们不可以对两个table执行加操作,也不可以比较两个表的大小。
Metatables允许我们改变table的行为,例如,使用Metatables我们可以定义Lua如何计算两个table的相加操作a+b。当Lua试图对两个表进行相加时,他会检查两个表是否有一个表有Metatable,并且检查Metatable是否有__add域。如果找到则调用这个__add函数(所谓的Metamethod)去计算结果。
Lua中的每一个表都有其Metatable。(后面我们将看到userdata也有Metatable),Lua默认创建一个不带metatable的新表
t = {}
print(getmetatable(t))      --> nil
可以使用setmetatable函数设置或者改变一个表的metatable
t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)
任何一个表都可以是其他一个表的metatable,一组相关的表可以共享一个metatable(描述他们共同的行为)。一个表也可以是自身的metatable(描述其私有行为)。
13.1 算术运算的Metamethods
这一部分我们通过一个简单的例子介绍如何使用metamethods。假定我们使用table来描述结合,使用函数来描述集合的并操作,交集操作,like操作。我们在一个表内定义这些函数,然后使用构造函数创建一个集合:
Set = {}
 
function Set.new (t)
    local set = {}
    for _, l in ipairs(t) do set[l] = true end
    return set
end
 
function Set.union (a,b)
    local res = Set.new{}
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end
 
function Set.intersection (a,b)
    local res = Set.new{}
    for k in pairs(a) do
       res[k] = b[k]
    end
    return res
end
为了帮助理解程序运行结果,我们也定义了打印函数输出结果:
function Set.tostring (set)
    local s = "{"
    local sep = ""
    for e in pairs(set) do
       s = s .. sep .. e
       sep = ", "
    end
    return s .. "}"
end
 
function Set.print (s)
    print(Set.tostring(s))
end
现在我们想加号运算符(+)执行两个集合的并操作,我们将所有集合共享一个metatable,并且为这个metatable添加如何处理相加操作。
第一步,我们定义一个普通的表,用来作为metatable。为避免污染命名空间,我们将其放在set内部。
Set.mt = {}       -- metatable for sets
第二步,修改set.new函数,增加一行,创建表的时候同时指定对应的metatable
function Set.new (t)     -- 2nd version
    local set = {}
    setmetatable(set, Set.mt)
    for _, l in ipairs(t) do set[l] = true end
    return set
end
这样一来,set.new创建的所有的集合都有相同的metatable了:
s1 = Set.new{10, 20, 30, 50}
s2 = Set.new{30, 1}
print(getmetatable(s1))     --> table: 00672B60
print(getmetatable(s2))     --> table: 00672B60
第三步,给metatable增加__add函数。
Set.mt.__add = Set.union
Lua试图对两个集合相加时,将调用这个函数,以两个相加的表作为参数。
通过metamethod,我们可以对两个集合进行相加:
s3 = s1 + s2
Set.print(s3)     --> {1, 10, 20, 30, 50}
同样的我们可以使用相乘运算符来定义集合的交集操作
Set.mt.__mul = Set.intersection
 
Set.print((s1 + s2)*s1)     --> {10, 20, 30, 50}
对于每一个算术运算符,metatable都有对应的域名与其对应,除了__add__mul外,还有__sub()__div()__unm()__pow(),我们也可以定义__concat定义连接行为。
当我们对两个表进行加没有问题,但如果两个操作数有不同的metatable例如:
s = Set.new{1,2,3}
s = s + 8
Lua选择metamethod的原则:如果第一个参数存在带有__add域的metatableLua使用它作为metamethod,和第二个参数无关;
否则第二个参数存在带有__add域的metatableLua使用它作为metamethod 否则报错。
Lua不关心这种混合类型的,如果我们运行上面的s=s+8的例子在Set.union发生错误:
bad argument #1 to `pairs' (table expected, got number)
如果我们想得到更加清楚地错误信息,我们需要自己显式的检查操作数的类型:
function Set.union (a,b)
    if getmetatable(a) ~= Set.mt or
                     getmetatable(b) ~= Set.mt then
       error("attempt to `add' a set with a non-set value", 2)
    end
       ... -- same as before
13.2 关系运算的Metamethods
Metatables也允许我们使用metamethods__eq(等于),__lt(小于),和__le(小于等于)给关系运算符赋予特殊的含义。对剩下的三个关系运算符没有专门的metamethod,因为Luaa ~= b转换为not (a == b)a > b转换为b < aa >= b转换为 b <= a
(直到Lua 4.0为止,所有的比较运算符被转换成一个,a <= b转为not (b < a)。然而这种转换并不一致正确。当我们遇到偏序(partial order)情况,也就是说,并不是所有的元素都可以正确的被排序情况。例如,在大多数机器上浮点数不能被排序,因为他的值不是一个数字(Not a NumberNaN)。根据IEEE 754的标准,NaN表示一个未定义的值,比如0/0的结果。该标准指出任何涉及到NaN比较的结果都应为false。也就是说,NaN <= x总是falsex < NaN也总是false。这样一来,在这种情况下a <= b 转换为 not (b < a)就不再正确了。)
在我们关于基和操作的例子中,有类似的问题存在。<=代表集合的包含:a <= b表示集合a是集合b的子集。这种意义下,可能a <= bb < a都是false;因此,我们需要将__le__lt的实现分开:
Set.mt.__le = function (a,b)    -- set containment
    for k in pairs(a) do
       if not b[k] then return false end
    end
    return true
end
 
Set.mt.__lt = function (a,b)
    return a <= b and not (b <= a)
end
最后,我们通过集合的包含来定义集合相等:
Set.mt.__eq = function (a,b)
    return a <= b and b <= a
end
有了上面的定义之后,现在我们就可以来比较集合了:
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)          --> true
print(s1 < s2)           --> true
print(s1 >= s1)          --> true
print(s1 > s1)           --> false
print(s1 == s2 * s1)     --> true
与算术运算的metamethods不同,关系元算的metamethods不支持混合类型运算。对于混合类型比较运算的处理方法和Lua的公共行为类似。如果你试图比较一个字符串和一个数字,Lua将抛出错误。相似的,如果你试图比较两个带有不同metamethods的对象,Lua也将抛出错误。
但相等比较从来不会抛出错误,如果两个对象有不同的metamethod,比较的结果为false,甚至可能不会调用metamethod。这也是模仿了Lua的公共的行为,因为Lua总是认为字符串和数字是不等的,而不去判断它们的值。仅当两个有共同的metamethod的对象进行相等比较的时候,Lua才会调用对应的metamethod
13.3 库定义的Metamethods
在一些库中,在自己的metatables中定义自己的域是很普遍的情况。到目前为止,我们看到的所有metamethods都是Lua核心部分的。有虚拟机负责处理运算符涉及到的metatables和为运算符定义操作的metamethods。但是,metatable是一个普通的表,任何人都可以使用。
tostring是一个典型的例子。如前面我们所见,tostring以简单的格式表示出table
print({})     --> table: 0x8062ac0
(注意:print函数总是调用tostring来格式化它的输出)。然而当格式化一个对象的时候,tostring会首先检查对象是否存在一个带有__tostring域的metatable。如果存在则以对象作为参数调用对应的函数来完成格式化,返回的结果即为tostring的结果。
在我们集合的例子中我们已经定义了一个函数来将集合转换成字符串打印出来。因此,我们只需要将集合的metatable__tostring域调用我们定义的打印函数:
Set.mt.__tostring = Set.tostring
这样,不管什么时候我们调用print打印一个集合,print都会自动调用tostring,而tostring则会调用Set.tostring
s1 = Set.new{10, 4, 5}
print(s1)     --> {4, 5, 10}
setmetatable/getmetatable函数也会使用metafield,在这种情况下,可以保护metatables。假定你想保护你的集合使其使用者既看不到也不能修改metatables。如果你对metatable设置了__metatable的值,getmetatable将返回这个域的值,而调用setmetatable 将会出错:
Set.mt.__metatable = "not your business"
 
s1 = Set.new{}
print(getmetatable(s1))     --> not your business
setmetatable(s1, {})
stdin:1: cannot change protected metatable
13.4 表相关的Metamethods
关于算术运算和关系元运算的metamethods都定义了错误状态的行为,他们并不改变语言本身的行为。针对在两种正常状态:表的不存在的域的查询和修改,Lua也提供了改变tables的行为的方法。
13.4.1 The __index Metamethod
前面说过,当我们访问一个表的不存在的域,返回结果为nil,这是正确的,但并不一定正确。实际上,这种访问触发lua解释器去查找__index metamethod:如果不存在,返回结果为nil;如果存在则由__index metamethod返回结果。
这 个例子的原型是一种继承。假设我们想创建一些表来描述窗口。每一个表必须描述窗口的一些参数,比如:位置,大小,颜色风格等等。所有的这些参数都有默认的 值,当我们想要创建窗口的时候只需要给出非默认值的参数即可创建我们需要的窗口。第一种方法是,实现一个表的构造器,对这个表内的每一个缺少域都填上默认 值。第二种方法是,创建一个新的窗口去继承一个原型窗口的缺少域。首先,我们实现一个原型和一个构造函数,他们共享一个metatable
-- create a namespace
Window = {}
-- create the prototype with default values
Window.prototype = {x=0, y=0, width=100, height=100, }
-- create a metatable
Window.mt = {}
-- declare the constructor function
function Window.new (o)
    setmetatable(o, Window.mt)
    return o
end
现在我们定义__index metamethod
Window.mt.__index = function (table, key)
    return Window.prototype[key]
end
这样一来,我们创建一个新的窗口,然后访问他缺少的域结果如下:
w = Window.new{x=10, y=20}
print(w.width)       --> 100
Lua发现w不存在域width时,但是有一个metatable带有__index域,Lua使用wthe table)和width(缺少的值)来调用__index metamethodmetamethod则通过访问原型表(prototype)获取缺少的域的结果。
__index metamethod在继承中的使用非常常见,所以Lua提供了一个更简洁的使用方式。__index metamethod不需要非是一个函数,他也可以是一个表。但它是一个函数的时候,Luatable和缺少的域作为参数调用这个函数;当他是一个表的时候,Lua将在这个表中看是否有缺少的域。所以,上面的那个例子可以使用第二种方式简单的改写为:
Window.mt.__index = Window.prototype
现在,当Lua查找metatable__index域时,他发现window.prototype的值,它是一个表,所以Lua将访问这个表来获取缺少的值,也就是说它相当于执行:
Window.prototype["width"]
将一个表作为__index metamethod使用,提供了一种廉价而简单的实现单继承的方法。一个函数的代价虽然稍微高点,但提供了更多的灵活性:我们可以实现多继承,隐藏,和其他一些变异的机制。我们将在第16章详细的讨论继承的方式。
当我们想不通过调用__index metamethod来访问一个表,我们可以使用rawget函数。Rawget(t,i)的调用以raw access方式访问表。这种访问方式不会使你的代码变快(the overhead of a function call kills any gain you could have),但有些时候我们需要他,在后面我们将会看到。
13.4.2 The __newindex Metamethod
__newindex metamethod用来对表更新,__index则用来对表访问。当你给表的一个缺少的域赋值,解释器就会查找__newindex metamethod:如果存在则调用这个函数而不进行赋值操作。像__index一样,如果metamethod是一个表,解释器对指定的那个表,而不是原始的表进行赋值操作。另外,有一个raw函数可以绕过metamethod:调用rawset(t,k,v)不掉用任何metamethod对表tk域赋值为v__index__newindex metamethods的混合使用提供了强大的结构:从只读表到面向对象编程的带有继承默认值的表。在这一张的剩余部分我们看一些这些应用的例子,面向对象的编程在另外的章节介绍。
13.4.3有默认值的表
在一个普通的表中任何域的默认值都是nil。很容易通过metatables来改变默认值:
function setDefault (t, d)
    local mt = {__index = function () return d end}
    setmetatable(t, mt)
end
 
tab = {x=10, y=20}
print(tab.x, tab.z)      --> 10   nil
setDefault(tab, 0)
print(tab.x, tab.z)      --> 10   0
现在,不管什么时候我们访问表的缺少的域,他的__index metamethod被调用并返回0setDefault函数为每一个需要默认值的表创建了一个新的metatable。在有很多的表需要默认值的情况下,这可能使得花费的代价变大。然而metatable有一个默认值d和它本身关联,所以函数不能为所有表使用单一的一个metatable。为了避免带有不同默认值的所有的表使用单一的metatable,我们将每个表的默认值,使用一个唯一的域存储在表本身里面。如果我们不担心命名的混乱,我可使用像"___"作为我们的唯一的域:
local mt = {__index = function (t) return t.___ end}
function setDefault (t, d)
    t.___ = d
    setmetatable(t, mt)
end
如果我们担心命名混乱,也很容易保证这个特殊的键值唯一性。我们要做的只是创建一个新表用作键值:
local key = {}    -- unique key
local mt = {__index = function (t) return t[key] end}
function setDefault (t, d)
    t[key] = d
    setmetatable(t, mt)
end
另外一种解决表和默认值关联的方法是使用一个分开的表来处理,在这个特殊的表中索引是表,对应的值为默认值。然而这种方法的正确实现我们需要一种特殊的表:weak table,到目前为止我们还没有介绍这部分内容,将在第17章讨论。
为了带有不同默认值的表可以重用相同的原表,还有一种解决方法是使用memoize metatables,然而这种方法也需要weak tables,所以我们再次不得不等到第17章。
13.4.4监控表
__index__newindex都是只有当表中访问的域不存在时候才起作用。捕获对一个表的所有访问情况的唯一方法就是保持表为空。因此,如果我们想监控一个表的所有访问情况,我们应该为真实的表创建一个代理。这个代理是一个空表,并且带有__index__newindex metamethods,由这两个方法负责跟踪表的所有访问情况并将其指向原始的表。假定,t是我们想要跟踪的原始表,我们可以:
t = {}     -- original table (created somewhere)
 
-- keep a private access to original table
local _t = t
 
-- create proxy
t = {}
 
-- create metatable
local mt = {
    __index = function (t,k)
    print("*access to element " .. tostring(k))
    return _t[k] -- access the original table
    end,
 
    __newindex = function (t,k,v)
    print("*update of element " .. tostring(k) ..
               " to " .. tostring(v))
    _t[k] = v     -- update original table
    end
}
setmetatable(t, mt)
这段代码将跟踪所有对t的访问情况:
> t[2] = 'hello'
*update of element 2 to hello
> print(t[2])
*access to element 2
hello
(注意:不幸的是,这个设计不允许我们遍历表。Pairs函数将对proxy进行操作,而不是原始的表。)如果我们想监控多张表,我们不需要为每一张表都建立一个不同的metatable。我们只要将每一个proxy和他原始的表关联,所有的proxy共享一个公用的metatable即可。将表和对应的proxy关联的一个简单的方法是将原始的表作为proxy的域,只要我们保证这个域不用作其他用途。一个简单的保证它不被作他用的方法是创建一个私有的没有他人可以访问的key。将上面的思想汇总,最终的结果如下:
-- create private index
local index = {}
 
-- create metatable
local mt = {
    __index = function (t,k)
       print("*access to element " .. tostring(k))
       return t[index][k]   -- access the original table
    end
 
    __newindex = function (t,k,v)
    print("*update of element " .. tostring(k) .. " to "
                  .. tostring(v))
    t[index][k] = v          -- update original table
    end
}
 
function track (t)
    local proxy = {}
    proxy[index] = t
    setmetatable(proxy, mt)
    return proxy
end
现在,不管什么时候我们想监控表t,我们要做得只是t=track(t)
13.4.5只读表
采用代理的思想很容易实现一个只读表。我们需要做得只是当我们监控到企图修改表时候抛出错误。通过__index metamethod,我们可以不使用函数而是用原始表本身来使用表,因为我们不需要监控查寻。这是比较简单并且高效的重定向所有查询到原始表的方法。但是,这种用法要求每一个只读代理有一个单独的新的metatable,使用__index指向原始表:
function readOnly (t)
    local proxy = {}
    local mt = {         -- create metatable
       __index = t,
       __newindex = function (t,k,v)
           error("attempt to update a read-only table", 2)
       end
    }
 
    setmetatable(proxy, mt)
    return proxy
end
(记住:error的第二个参数2,将错误信息返回给企图执行update的地方)作为一个简单的例子,我们对工作日建立一个只读表:
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday",
       "Thursday", "Friday", "Saturday"}
 
print(days[1])       --> Sunday
days[2] = "Noday"
stdin:1: attempt to update a read-only table

发表于 @ 2008年05月08日 11:49:31|评论(0)|编辑


新一篇: (LUA教程)第13章 Metatables and Metamethods | 旧一篇: (LUA教程)第13章 Metatables and Metamethods

Lua用一个名为environment普通的表来保存所有的全局变量。(更精确的说,Lua在一系列的environment中保存他的“global”变量,但是我们有时候可以忽略这种多样性)这种结果的优点之一是他简化了Lua的内部实现,因为对于所有的全局变量没有必要非要有不同的数据结构。另一个(主要的)优点是我们可以像其他表一样操作这个保存全局变量的表。为了简化操作,Lua将环境本身存储在一个全局变量_G中,(_G._G等于_G)。例如,下面代码打印在当前环境中所有的全局变量的名字:
for n in pairs(_G) do print(n) end
这一章我们将讨论一些如何操纵环境的有用的技术。
14.1 使用动态名字访问全局变量
通常,赋值操作对于访问和修改全局变量已经足够。然而,我们经常需要一些原编程(meta-programming)的方式,比如当我们需要操纵一个名字被存储在另一个变量中的全局变量,或者需要在运行时才能知道的全局变量。为了获取这种全局变量的值,有的程序员可能写出下面类似的代码:
    loadstring("value = " .. varname)()
or
    value = loadstring("return " .. varname)()
如果varnamex,上面连接操作的结果为:"return x"(第一种形式为 "value = x"),当运行时才会产生最终的结果。然而这段代码涉及到一个新的chunk的创建和编译以及其他很多额外的问题。你可以换种方式更高效更简洁的完成同样的功能,代码如下:
value = _G[varname]
因为环境是一个普通的表,所以你可以使用你需要获取的变量(变量名)索引表即可。
也可以用相似的方式对一个全局变量赋值:_G[varname] = value。小心:一些程序员对这些函数很兴奋,并且可能写出这样的代码:_G["a"] = _G["var1"],这只是a = var1的复杂的写法而已。
对前面的问题概括一下,表域可以是型如"io.read" or "a.b.c.d"的动态名字。我们用循环解决这个问题,从_G开始,一个域一个域的遍历:
function getfield (f)
    local v = _G      -- start with the table of globals
    for w in string.gfind(f, "[%w_]+") do
       v = v[w]
    end
    return v
end
我们使用string库的gfind函数来迭代f中的所有单词(单词指一个或多个子母下划线的序列)。相对应的,设置一个域的函数稍微复杂些。赋值如:
a.b.c.d.e = v
实际等价于:
local temp = a.b.c.d
temp.e = v
也就是说,我们必须记住最后一个名字,必须独立的处理最后一个域。新的setfield函数当其中的域(译者注:中间的域肯定是表)不存在的时候还需要创建中间表。
function setfield (f, v)
    local t = _G         -- start with the table of globals
    for w, d in string.gfind(f, "([%w_]+)(.?)") do
       if d == "." then -- not last field?
           t[w] = t[w] or {}    -- create table if absent
           t = t[w]          -- get the table
       else                 -- last field
           t[w] = v          -- do the assignment
       end
    end
end
这个新的模式匹配以变量w加上一个可选的点(保存在变量d中)的域。如果一个域名后面不允许跟上点,表明它是最后一个名字。(我们将在第20章讨论模式匹配问题)。使用上面的函数
setfield("t.x.y", 10)
创建一个全局变量表t,另一个表t.x,并且对t.x.y赋值为10
print(t.x.y)                --> 10
print(getfield("t.x.y"))     --> 10
14.2声明全局变量
全局变量不需要声明,虽然这对一些小程序来说很方便,但程序很大时,一个简单的拼写错误可能引起bug并且很难发现。然而,如果我们喜欢,我们可以改变这种行为。因为Lua所有的全局变量都保存在一个普通的表中,我们可以使用metatables来改变访问全局变量的行为。
第一个方法如下:
setmetatable(_G, {
    __newindex = function (_, n)
       error("attempt to write to undeclared variable "..n, 2)
    end,
   
    __index = function (_, n)
       error("attempt to read undeclared variable "..n, 2)
    end,
})
这样一来,任何企图访问一个不存在的全局变量的操作都会引起错误:
> a = 1
stdin:1: attempt to write to undeclared variable a
但是我们如何声明一个新的变量呢?使用rawset,可以绕过metamethod
function declare (name, initval)
    rawset(_G, name, initval or false)
end
or 带有 false 是为了保证新的全局变量不会为 nil。注意:你应该在安装访问控制以前(before installing the access control)定义这个函数,否则将得到错误信息:毕竟你是在企图创建一个新的全局声明。只要刚才那个函数在正确的地方,你就可以控制你的全局变量了:
> a = 1
stdin:1: attempt to write to undeclared variable a
> declare "a"
> a = 1       -- OK
但是现在,为了测试一个变量是否存在,我们不能简单的比较他是否为nil。如果他是nil访问将抛出错误。所以,我们使用rawget绕过metamethod
if rawget(_G, var) == nil then
    -- 'var' is undeclared
    ...
end
改变控制允许全局变量可以为nil也不难,所有我们需要的是创建一个辅助表用来保存所有已经声明的变量的名字。不管什么时候metamethod被调用的时候,他会检查这张辅助表看变量是否已经存在。代码如下:
local declaredNames = {}
function declare (name, initval)
    rawset(_G, name, initval)
    declaredNames[name] = true
end
setmetatable(_G, {
    __newindex = function (t, n, v)
    if not declaredNames[n] then
       error("attempt to write to undeclared var. "..n, 2)
    else
       rawset(t, n, v)   -- do the actual set
    end
end,
    __index = function (_, n)
    if not declaredNames[n] then
       error("attempt to read undeclared var. "..n, 2)
    else
       return nil
    end
end,
})
两种实现方式,代价都很小可以忽略不计的。第一种解决方法:metamethods在平常操作中不会被调用。第二种解决方法:他们可能被调用,不过当且仅当访问一个值为nil的变量时。
14.3 非全局的环境
全局环境的一个问题是,任何修改都会影响你的程序的所有部分。例如,当你安装一个metatable去控制全局访问时,你的整个程序都必须遵循同一个指导方针。如果你想使用标准库,标准库中可能使用到没有声明的全局变量,你将碰到坏运。
Lua 5.0允许每个函数可以有自己的环境来改善这个问题,听起来这很奇怪;毕竟,全局变量表的目的就是为了全局性使用。然而在Section 15.4我们将看到这个机制带来很多有趣的结构,全局的值依然是随处可以获取的。
可以使用setfenv函数来改变一个函数的环境。Setfenv接受函数和新的环境作为参数。除了使用函数本身,还可以指定一个数字表示栈顶的活动函数。数字1代表当前函数,数字2代表调用当前函数的函数(这对写一个辅助函数来改变他们调用者的环境是很方便的)依此类推。下面这段代码是企图应用setfenv失败的例子:
a = 1     -- create a global variable
-- change current environment to a new empty table
setfenv(1, {})
print(a)
导致:
stdin:5: attempt to call global `print' (a nil value)
(你必须在单独的chunk内运行这段代码,如果你在交互模式逐行运行他,每一行都是一个不同的函数,调用setfenv只会影响他自己的那一行。)一旦你改变了你的环境,所有全局访问都使用这个新的表,如果她为空,你就丢失所有你的全局变量,甚至_G,所以,你应该首先使用一些有用的值封装(populate)她,比如老的环境:
a = 1 -- create a global variable
-- change current environment
setfenv(1, {_G = _G})
_G.print(a)       --> nil
_G.print(_G.a)    --> 1
现在,当你访问"global" _G,他的值为旧的环境,其中你可以使用print函数。
你也可以使用继承封装(populate)你的新的环境:
a = 1
local newgt = {}     -- create new environment
setmetatable(newgt, {__index = _G})
setfenv(1, newgt)    -- set it
print(a)             --> 1
在这段代码新的环境从旧的环境中继承了printa;然而,任何赋值操作都对新表进行,不用担心误操作修改了全局变量表。另外,你仍然可以通过_G修改全局变量:
-- continuing previous code
a = 10
print(a)      --> 10
print(_G.a)   --> 1
_G.a = 20
print(_G.a)   --> 20
当你创建一个新的函数时,他从创建他的函数继承了环境变量。所以,如果一个chunk改变了他自己的环境,这个chunk所有在改变之后定义的函数都共享相同的环境,都会受到影响。这对创建命名空间是非常有用的机制,我们下一章将会看到。

发表于 @ 2008年05月08日 11:50:27|评论(0)|编辑

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值