lua总结1
1.交互模式
直接执行lua进入。
此模式输入每条指令立即执行。
dofile可用于加载文件内容到执行环境。
全局变量无需声明可直接用。
2.如何看待交互模式下,执行dofile,dostring
可以等价为把文件或字符串内容展开到交互上下文。
这样,其中的指令会被执行,其中的非局部变量也会在运行环境持续存在。
交互式上下文,通过dofile,dostring会进入新的作用域。
在新的作用域,只可访问此作用域内,和全局作用域内的变量。
一旦dofile,dostring结束,作用域内局部变量也将无法在后续访问到。但作用域内定义的全局变量,可以在后续访问到。
也可简单总结为:
局部作用域内变量,一旦离开所在的局部作用域就不可被访问。
在局部作用域定义的全局变量,在离开局部作用域后,依然可被访问。
交互模式指令和dofile/dostring内指令的关键区别在于:
交互模式下,执行的每一行指令,自动被放在一个局部作用域。不同行位于不同的局部作用域。
dofile/dostring内所有指令处于同一个局部作用域。
3.对作用域,局部变量,全局变量,函数闭包的认识
可以认为所用作用域中定义的全局变量存在同一个table。
每个作用域的局部变量存在一个此作用域相关的匿名table。
在作用域内访问变量时,先在作用域自身匿名table中查找,找不到,再到全局作用域table查找。
依然找不到,返回nil。
函数闭包意思是定义一个函数时候,如果函数内部访问了函数定义所在的局部作用域内的变量。
会将这些变量,作为闭包目标变量和此函数绑定。
实现层面可以用c++来对等表达为:
函数定义时候,产生一个c++可调用类型。
将定义存储到一个变量时候,即得到可调用类型的一个新的实例对象。
闭包即为,针对定义时用到的同一作用域的局部变量,分别在可调用类型中存在对应成员字段。产生实例对象时,用定义时局部变量值来初始化这些对应成员字段的值。
注意,用到的局部对象时对象时,新的实力对象中对应字段是一个指向此对象的引用。
这样,后续通过实例对象执行方法调用修改了引用对象值时,局部作用域再次访问局部变量时,可以看到所引用对象的改变。
4.作用域嵌套
执行一个lua文件,此时某行语句执行了dofile/dostring,将调到另一个作用域。dofile/dostring执行完毕后,再次回到原来lua文件所在的作用域。
将上述过程分为阶段1,阶段2,阶段3
阶段2无法访问阶段1的局部变量
阶段3无法访问阶段2的局部变量
这时,不同lua文件所对应的局部作用域是相互独立的。其中的局部变量无法跨作用域访问。
但在同一个lua文件内,由于while,for等语句的存在,就可能将这个局部作用域,进一步细分为顶级局部,嵌套局部,…,这样一个层次。针对这样细分后的层次作用域,内层作用域是可以访问到外层作用域在此之前定义的局部变量的。
5.dofile
5.1.可分解为从文件加载lua代码,此时lua代码段作为一个函数类型被返回。
5.2.执行返回的函数
dostring则是从字符串加载lua代码,并将lua代码段作为一个函数类型返回+执行返回的函数。
6.以dofile/dostring将来自文件或提供的字符串当成lua代码编译并返回一个函数+执行函数。
-- 方式1
-- 假设1.lua内容为:i=i+1
dofile("1.lua")
-- 方式2
dostring("i=i+1")
-- 方式3
f = function() i = i+1 end
f()
原则上上述三种方式是等价的。但实际不等价。
i=32
local i=0
f = load("i=i+1;print(i)")
g = function() i=i+1;print(i) end
f() -->33
g() -->1
原因是dofile/dostring基于的loadfile/load总是在全局环境中编译代码段。
所以,dofile/dostring中的代码除了访问自身作用域中的变量和全局变量,只能访问到外围作用域的全局变量。
lua将所有独立的代码段当作匿名可变长参数函数的函数体。所以有:
load("a=1")
-- 等价的函数形式为
function(...) a=1 end
7.dofile/dostring/loadfile/load中加载的lua代码被作为匿名函数返回。代码段中的变量和函数定义的生效时机?
执行loadfile/load时,仅仅对代码段执行编译,并返回一个匿名函数。
只有后续实际执行了返回的匿名函数后,代码段中的变量,函数才会实际被定义,进而针对其中的全局变量,才可被外部访问。
local ff = loadfile("my.lua")
print(t)
print(foo)
print(ff)
if ff then
ff()
else
print("Error: File could not be loaded.")
end
print(t)
print(foo)
foo(t)
其中my.lua内容如下
t={name="xbh"}
function foo(x)
if(type(x) ~= "table") then
print(x)
else
for k,v in pairs(x) do
print(k,v)
end
end
end
运行输出为:
8.lua中的异常和错误处理机制
针对异常:
lua中可通过error来抛出异常,可通过pcall来捕获异常。error中的参数是异常对象,可以是字符串,也可以是对象类型。
针对错误:
直接执行lua支持的库调时,一般非法情况发生时,通过返回值来判断成功与否。
9.模块和包
一个模块就是一些代码,要么由lua编写,要么由c编写,这些代码可通过require加载,然后创建和返回一个表。
这个表就像是某种命名空间,其中定义的内容是模块中导出的东西,比如函数和常量。
独立解释器会使用如下代码等价的方式提前加载所有标准库:
math=require "math"
string=require "string"
...
模块加载:
首先,require在表package.loaded检查模块是否已经被加载。如是,直接返回相应的值。
如尚未加载,则搜索具有指定模块名的lua文件(搜索路径由变量package.path指定)。如找到了相应的文件,就用loadfile将其进行加载,结果是一个称为加载器的函数。
如require找不到指定模块名的lua文件,就搜索相应名称的c标准库(搜索路径由package.cpath指定)。如找到了一个c标准库,则使用底层函数package.loadlib进行加载,这个底层函数会查找名为luaopen_modname的函数。这种情况下,加载函数就是loadlib的执行结果,也就是一个被表示为lua函数的c语言函数:luaopen_modname。
无论模块是在lua文件还是c标准库找到的,函数require此时都具有了用于加载它的加载函数。为了最终加载模块,函数require带着两个参数调用加载函数:模块名和加载函数所在文件的名称。如果加载函数有返回值,则require会返回这个值,然后将其保存在表package.loaded中。如加载函数没有返回值且表中的package.loaded[@rep{modname}]为空,函数require就假设模块的返回值是true。
其他:
如果一个模块名中包含连字符,那么函数require会用连字符之前的内容来创建luaopen_*函数的名称。如,如果一个模块的名称为mod-v3.4,则函数require会认为该模块的加载函数应该是luaopen_mod,而不是luaopen_mod-v3.4。
搜索路径:
函数require使用的路径是一组模板,其中的每项都指定了将模块名(函数require的参数)转换为文件名的方式。更准确地说,这种路径中的每个模板都是一个包含可选问号的文件名。对每个模板,函数require会用模块名来替换每一个问号,然后检查结果是否存在对应的文件;如不存在,则尝试下一个模板。
路径中的模板以在大多数操作系统中很少被用于文件名的分号隔开。如考虑如下路径:
?;?.lua;c:\windows\?;/usr/local/lua/?/?.lua
使用上述路径时,调用require "sql"将尝试打开如下的lua文件:
sql
sql.lua
c:\windows\sql
/usr/local/lua/sql/sql.lua
函数require用于搜索lua文件的路径是变量package.path的当前值。当package模块被初始化后,它就把变量package.path设置成环境变量LUA_PATH_5_3的值。如此环境变量未被定义,则尝试另一个环境变量LUA_PATH。如这两个环境变量都没有被定义,则lua语言使用一个编译时定义的默认路径。
在使用一个环境变量的值时,lua语言会将其中所有的";;“替换成默认路径。如,如果将LUA_PATH_5_3设为"mydir/?.lua;;”。
搜索C标准库逻辑类似。路径来自变量package.cpath。这个变量的初始值来自环境变量LUA_CPATH_5_3或LUA_PATH。
在posix系统上此路径典型值为:
./?.so;/usr/local/lib/lua/5.2/?.so
在windows系统上此路径典型值为:
.\?.dll;C:\Program Files\Lua502\dll\?.dll
函数package.searchpath中实现了搜索库的所有规则。该函数的参数包括模块名,路径,然后按上述规则来搜索文件。函数要么返回第一个存在的文件的文件名,要么返回nil外加描述所有文件都无法成功打开的错误信息。
10.搜索器
现实中,搜索lua文件和c标准库的方式只是更加通用的搜索器的两个实例。一个搜索器是一个以模块名为参数,以对应模块的加载器或nil(如果找不到加载器)为返回值的简单函数。
数组package.searchers列出了函数require使用的所有搜索器。在寻找模块时,函数require传入模块名并调用列表中的每一个搜索器直到他们其中的一个找到了指定模块的加载器。如果所有搜索器都被调用完后还找不到,则函数require就抛出一个异常。
在默认配置中,我们此前学习过的用于搜索lua文件和c标准库的搜索器排在列表的第二,三位,在他们之前说预加载搜索器。
预加载搜索器使得我们能够为要加载的模块定义任意的加载函数。预加载搜索器使用一个名为package.preload的表来映射模块名称和加载函数。当搜索指定的模块名时,该搜索器只是简单地在表中搜索指定的名称。如果他找到了对应的函数,就将该函数作为相应模块的加载函数返回。否则,返回nil。
预加载搜索器为处理非标场景提供了一种通用的方式。如,一个静态链接到lua中的c标准库可将其"luaopen_"注册到表preload中,这样"luaopen_"只有当用户加载这个模块时才会被调用。
11.模块实例
-- m.lua
local M={}
local function new(r,i)
return {r=r,i=i}
end
local function p()
for k,v in pairs(M) do
print(k,v)
end
end
M.new=new
M.i=new(0,1)
M.p = p
return M
-- main.lua
local m = require "m"
m.p()
执行dofile(“main.lua”)输出如下:
12.子模块和包
一个包是一棵由模块组成的完整的树,是lua语言中用于发行程序的单位。
加载一个名为mod.sub的模块时,函数require依次用原始"mod.sub"作为键来查询表package.loaded和package.preload。
当搜索一个定义子模块的文件时,函数require会将点转换为另一个字符,通常就是操作系统的目录分隔符。
假设目录分隔符是斜杠,且有如下路径:
./?.lua;/usr/local/lua/?.lua;/usr/local/lua/?/init.lua
调用require "a.b"会尝试打开以下文件:
./a/b.lua
/usr/local/lua/a/b.lua
/usr/local/lua/a/b/init.lua
c语言的名称不能包含点。这时require会将点转换为其他字符,即下划线。因此,一个名为a.b的c标准库应将其加载函数命名为luaopen_a_b。
函数require在加载c语言编写的子模块时,还有另外一个搜索器。当该函数找不到子模块对应的lua文件或c文件时,它会再次搜索c文件所在的路径,不过这次将搜索包的名称。如,一个程序要加载子模块a.b.c,搜索器会搜索文件a。如找到了c标准库a,则函数require会在该库中搜索对应的加载函数luaopen_a_b_c。这样,可以将一个发行包的几个子模块组织为一个c标准库,每个子模块有各自的加载函数。
12.元表和元方法
元表定义的是实例的行为。不支持继承。
lua语言中的每一个值都可以有元表。每一个表和用户数据类型都有各自独立的元表,其他类型的值则共享对应类型所属的同一个元表。
lua语言在创建新表时不带元表。
t = {}
print(getmetatable(t)) -->nil
可使用setmetatable来设置或修改任意表的元表:
t1 = {}
setmetatable(t, t1)
print(getmetatable(t) == t1) -->true
在lua语言中,只能为表设置元表;如要为其他类型的值设置元表,必须通过c代码或调试库完成。
字符串标准库为所有的字符串都设置了同一个元表,而其他类型在默认情况中没有元表。
print(getmetatable("h1")) -->table:0x80772e0
print(getmetatable("h2")) -->table:0x80772e0
// meta.lua
local Set = {}
local mt = {}
function Set.new(l)
local set = {}
setmetatable(set, mt)
for _,v in ipairs(l) do set[v] = 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 l = {}
for e in pairs(set) do
l[#l+1] = tostring(e)
end
return "{" .. table.concat(l, ", ") .. "}" -- 返回的是字符串
end
-- + -> __add
-- * -> __mul
-- - -> __sub
-- / -> __div
-- -> __idiv
-- - -> __unm
-- mod -> __mod
-- -> __pow
-- & -> __band
-- | -> __bor
-- ^ -> __bxor
-- ~ -> __bnot
-- << -> __shl
-- >> -> __shr
-- -> __concat
-- == -> __eq
-- < -> __lt
-- <= -> __le
-- 针对~=,a>b,a>=b没有对应方法,可用上述三个方法来替代
-- -> __tostring
-- 如果在元表中设置__metatable,则getmetatable会返回此字段。setmetatable会引发错误。
mt.__add = Set.union
return Set
此时执行
s = dofile("meta.lua")
s1 = s.new({1,2,3})
s2 = s.new({2,3,4})
s3 = s1 + s2
print(s.tostring(s3))
输出结果可能为:{2,3,1,4}
从上述可见,元表示lua中面向实例对象执行运算符重载的一种机制。
s = dofile("meta.lua")
s1 = s.new({1,2,3})
s3 = s1+8
我们分析上述s1+8:
涉及运算符的表达式,如果第一个值有元表且元表中存在所需的元方法,则lua语言就使用这个元方法。
如果第二个值有元表且元表中存在所需的元方法,lua语言就使用这个元方法。
否则,lua语言就抛出异常。
13.元方法的__index
prototype = {x=0, y=0, width=100, height=100}
local mt = {}
function new(o)
setmetatable(o, mt)
return o
end
mt.__index = function (_, key)
return prototype[key]
end
w = new{x=10, y=20}
print(w.width) -->100
lua中访问表的字段不存在时,继续尝试访问对象元表的__index元方法。
上述
mt._index = prototype
这种直接用表来设置__index,也是支持的。
若希望访问表对象时,key不存在直接返回nil,可用rawget(t, i)来访问。
14.元方法的__newindex
向表对象设置一个新key的value时,若表对象提供了元表,且存在__newindex。
则通过__newindex完成设置。
若希望绕过__newindex,可用rawset(t, k, v)。
15.面向对象编程
function fun(self)
for k,v in pairs(self) do
print(k,v)
end
end
t={}
t[1]=1
t[2]=2
a = t
fun(t)
fun(a)
t2 = {
fun = function(self)
for k,v in pairs(self) do
print(k, v)
end
end
}
t2[1] = 1
t2[2] = 2
t2.fun(t2)
t2:fun()
上述执行后输出为:
这样函数可以用来模拟c++中成员函数,上述self相当于成员函数的this指针。
lua中没有类的概念。在Lua中可以采用原型(也是一个对象)+元表元方法模拟类。
setmetatable(A, {__index = B})
这样对A执行某个不存在键的访问时,将向B执行此访问。
16.继承机制
Account = {balance = 0}
function Account:new(o)
o = o or {}
self.__index = self
setmetatable(o, self)
return o
end
function Account:deposit(v)
self.balance = self.balance + v
end
function Account:withdraw(v)
if v > self.balance then error"insufficient funds" end
self.balance = self.balance - v
end
for k,v in pairs(Account) do
print(k,v)
end
-- 一个新表实例
-- 该实例以Account作为其元表,设置元表的__index指向Account
SpecialAccount = Account:new()
-- 先从SpecialAccount自身访问new方法,访问不到
-- 因为其元表,和元方法__index均已设置。则依据元方法针对Account执行new访问
-- 特殊的地方在于,此时Account:new中的self会指向SpecialAccount实例,而非Account实例
-- 这样,我们为表的实例{limit=1000.0},设置的元表指向SpecialAccount。
-- 该元表的元方法__index也指向SpecialAccount。
s = SpecialAccount:new{limit=1000.0}
function SpecialAccount:withdraw(v)
if v - self.balance >= self:getLimit() then
error"insufficient funds"
end
self.balance = self.balance - v
end
function SpecialAccount:getLimit()
return self.limit or 0
end
-- 先在s中找withdraw
-- 找不到时,在s的元表下的__index指示的对象里面找。也即在SpecialAccount找。
-- 找到。
-- 若找不到,依然继续向SpecialAccount的元表的__index指示的对象里面找。
s:withdraw(200.0)
用了self下,self总是指向动态类型。(套用c++中基类this的动态类型,静态类型)
16.多重继承
-- 参数1为要访问的key,参数2为createClass的入参
local function search(k, plist)
for i = 1, #plist do
-- 依次在每个入参对象中找k,找到则返回
local v = plist[i][k]
if v then return v end
end
end
function createClass(...)
-- 自己
local c = {}
-- 入参
local parents = {...}
-- 为自己设置元表,设置__index为函数
setmetatable(c, {__index = function(t, k)
return search(k, parents)
end})
-- 设置自己的__index
c.__index = c
-- 为自己新增new字段
function c:new(o)
o = o or {}
-- 将从new返回的表对象的元表设置为自己。而自己的__index也是自己。
setmetatable(o, c)
return o
end
return c
end
Named = {}
function Named:getname()
return self.name
end
function Named:setname(n)
self.name = n
end
-- 分析NamedAccount
NamedAccount = createClass(Account, Named)
-- 这样得到的表对象的元表是NamedAccount
account = NamedAccount:new{name = "Paul"}
-- 向account访问getname失败时
-- 继续向account的元表NamedAccount的__index访问
-- 即向NamedAccount访问getname
-- 访问失败时,向NamedAccount的元表的__index访问
-- 通过search函数来实现。此时会依次针对Account, Named来访问实现类似多重继承的效果
print(account:getname())
17.私有性
-- 这个函数创建了一个用于保存对象内部状态的表
-- 并将其存储在局部变量self中
-- 然后,这个函数创建了对象的方法,这些方法无需self
function newAccount(initialBalance)
local self = {balance = initialBalance}
local withdraw = function(v)
self.balance = self.balance - v
end
local deposit = function(v)
self.balance = self.balance + v
end
local getBalance = function() return self.balance end
return {
withdraw = withdraw,
deposit = deposit,
getBalance = getBalance
}
end
-- 我们只能通过暴露出来的方法来访问内部表
acc1 = newAccount(100.0)
acc1.withdraw(40.0)
print(acc1.getBalance())
上面演示里,self表只放了balance,继续放入很多其他的字段,及方法字段也是可以的。
18.单方法对象
function newObject(value)
return function (action, v)
if action == "get" then return value
elseif action == "set" then value = v
else error("invalid action")
end
end
end
-- 指向一个函数
d = newObject(0)
-- 引发函数调用,返回值由于闭包,此时为0
print(d("get"))
-- 引发函数调用,由于闭包,将闭包内值设置为10
d("set", 10)
-- 引发函数调用,闭包内值此时为10
print(d("get"))
上述过程将function看成c++可调用类型,该类型只实例化了一次。实例化对象是d。
19.对偶
local balance = {}
Account = {}
function Account:withdraw(v)
balance[self] = balance[self] - v
end
function Account:deposit(v)
balance[self] = balance[self] + v
end
function Account:balance()
return balance[self]
end
function Account:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
balance[o] = 0
return o
end
-- 得到的表的实例,此实例元表和__index均指向Account
a = Account:new{}
-- 从a访问deposit,
-- 访问不到时,继续从元表的__index访问,即从Account访问
-- 执行balance[self] = balance[self] + v,其中self是实例位置
a:deposit(100.0)
-- 最终从Account访问时
-- balance[self]会取出合适的值
print(a.balance())
-- 弊端为
-- 即使执行了a = nil,由于之前的实例作为键存储到了balance中
-- 所以,之前的实例无法被释放
上述过程将function看成c++可调用类型,该类型只实例化了一次。实例化对象是d。
19.对偶
local balance = {}
Account = {}
function Account:withdraw(v)
balance[self] = balance[self] - v
end
function Account:deposit(v)
balance[self] = balance[self] + v
end
function Account:balance()
return balance[self]
end
function Account:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
balance[o] = 0
return o
end
-- 得到的表的实例,此实例元表和__index均指向Account
a = Account:new{}
-- 从a访问deposit,
-- 访问不到时,继续从元表的__index访问,即从Account访问
-- 执行balance[self] = balance[self] + v,其中self是实例位置
a:deposit(100.0)
-- 最终从Account访问时
-- balance[self]会取出合适的值
print(a.balance())
-- 弊端为
-- 即使执行了a = nil,由于之前的实例作为键存储到了balance中
-- 所以,之前的实例无法被释放