九.面向对象编程
在lua中,可以使用table来表示"类":
--基类Account
--定义变量balance
--定义方法new,deposit,withdraw
--self是lua的关键字,类似于this,指调用者本身,如Account:new,self就是指Account;如a:deposit,self就是指a
--Account:deposit(v)的另一种写法为:Account.deposit(self, v)
--调用时,就要写成:a.deposit(a, v)的形式
Account = {balance = 0} --设置balance默认值为0
function Account:new(o) --构造函数,名字可随意取
o = o or {} --若o为nil,则创建一个table
setmetatable(o,self) --设置o的元表为self,当调用Account:new时,self等于Account
self.__index = self --当在o中找不到key时,lua会找元表中__index对应的函数或者table
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"
else
self.balance = self.balance - v
end
end
----------------------------------------------------------------
a = Account:new({balance = 200})
a:deposit(100) --a没有deposit字段,所以会使用Account的deposit方法
print(a.balance) --300
b = Account:new()
print(b.balance) --0
c = b:new({limit = 1000}) --b从Account中继承了new,而b中的new方法,self代表的是b
--因此,c的元表为b,b中的__index字段为b,c继承b,b继承Acount
c:deposit(500)
print(c.balance) --500
----------------------------------------------------------------
--[[
b可以重写从Acount中继承的方法
]]
function b:getLimit()
return self.limit or 0
end
function b:withdraw(v)
if v - self.balance >= self:getLimit() then
error "insufficient funds"
else
self.balance = self.balance - v
end
end
c:withdraw(700) --c会先从b中找withdraw
print(c.balance) -- -200
print(c:getLimit()) --1000
----------------------------------------------------------------
function c:getLimit()
return self.balance * 0.1
end
print(c:getLimit()) -- -20
Account = {balance = 100} --设置balance默认值为0
function Account:new() --构造函数,名字可随意取
local o = {} --若o为nil,则创建一个table
setmetatable(o,self) --设置o的元表为self,当调用Account:new时,self等于Account
self.__index = self --当在o中找不到key时,lua会找元表中__index对应的函数或者table
return o
end
a = Account:new();
b = Account:new();
print(a.balance); --100
print(b.balance); --100
a.balance = 50;
print(a.balance); --50
print(b.balance); --100
十.模块与包
require函数有两种特性:
a.搜索目录加载文件
b.判断文件是否已经加载,避免重复加载同一文件
require函数的使用
1.使用的编辑器是LuaEditor(v6.30),需要设置选项 / 编译调试设置,先取消勾选"直接以xxx",然后设置脚本工作目录(即lua脚本的目录),最后重新勾选"直接以xxx"
2.在桌面新建一个文件夹,然后如图,然后在another下建一个Test2.lua
3.
--Test.lua
print("Test");
function hehe()
print("hello");
end
--Test2.lua
print("Test2");
function hehe()
print("hello2");
end
--TestRequire.lua
print(package.path) --输出默认的搜索模式
require("Test")
package.path=package.path..";.\\another\\?.lua;"
require("Test2")
require("Test") --不再执行
.\?.lua;D:\Program Files\LuaEditor6.30\lua\?.lua;后面略,其中分号是分隔符
当require("Test")时,lua会用Test来替代上面的问号,从而变成:
.\Test.lua;D:\Program Files\LuaEditor6.30\lua\Test.lua;
意思就是从当前目录、D:\Program Files\LuaEditor6.30\lua等路径逐一搜索Test.lua这个文件
其中require会在初次加载时执行一次该lua文件,require第二次时,是不会再加载的
那么,如何理解require的不重复加载呢?实际上,当require一个文件时,lua会将其保存在一个table中,对应的key就是文件名。而这个table,就是package.loaded。package.loaded这个table保存了已经加载的所有文件,因此可以得出require的机制:先判断package.loaded有没有要加载的那个文件,如果有则直接返回那个文件,否则就去加载。
--TestRequire.lua
require("Test") --Test
require("Test2") --Test2
require("Test") --不再执行
package.loaded["Test"] = nil
require("Test") --Test(再次执行)
相对路径:
";.\\another\\?.lua;" .\的意思是当前目录,..\是上一层目录
绝对路径:
package.path=package.path..";D:\?.lua;"
4.编写模块
a.在lua中,变量有两种:全局变量和局部变量。局部变量用local修饰,没用local修饰的均为全局变量。而全局变量默认都是放在_G的table中的。
str = "啊"
print(str) --啊
print(_G.str) --啊
print(_G["str"]) --啊
那么问题来了,因为全局变量的这种存储方式,如果不同的lua文件中有相同的全局变量,就很容易出现覆盖的情况。
--UntitledA.lua
str = "a"
--UntitledB.lua
str = "b"
require "UntitledA"
require "UntitledB"
print(str)
print(_G.str)
print(_G["str"])
在lua中,_G被称为全局环境,默认所有全局变量都放在全局环境(_G)中(例如print)。那么,为了避免造成上述情况,可以将UntitledA.lua中的str和UntitledB.lua中的str分别放在不同的环境中,即所谓的非全局的环境。要改变成非全局的环境,需要用到setfenv函数。setfenv应该是set function environment的简写,即设置函数的环境。
--UntitledA.lua
str = "a"
print(str) --a
_G.print(str) --a
如下,setfenv函数会将_G这个全局环境切换成一个空table的非全局环境。一般情况下,如果不改变环境,那么访问全局变量时,实际上就是访问_G.全局变量;而改变环境后,访问全局变量时,实际上就是访问那个环境.全局变量。所谓的环境,可以认为是一个table。因此,下面的代码会报错,因为{}中并没有print这个全局变量。
--UntitledA.lua
str = "a"
setfenv(1, {})
print(str) --error:attempt to call global 'print' (a nil value)
那么为了解决上面的问题,可以将_G保留在新的环境中。
--UntitledA.lua
str = "a"
setfenv(1, {asd = _G})
asd.print(str) --nil
str = "b"
asd.print(str) --b
asd.print(asd.str) --a
第一个输出的是新表的str,因为访问全局变量实际上就是访问当前环境的全局变量。
第二个输出的是新表的str,这个str已经进行了赋值操作了。
第三个输出的是_G的str,因为asd代表的是之前的全局环境。
那么,运用元表的概念,上面的代码可以改写成这样:
--UntitledA.lua
str = "a"
local new = {}
setmetatable(new, {__index = _G})
setfenv(1, new)
print(str) --a
str = "b"
print(str) --b
print(_G.str) --a
b.在lua中,可以使用module函数方便地在不同的lua文件中创建不同的环境,称之为"模块"。因为环境是不同的,所以即使两个环境中存在相同的全局变量,也不会出现_G这种覆盖的情况。在了解module函数之前,需要先了解"..."这个东西,它不仅仅是指不定参数,还有其他的含义。
如下,如果直接运行UntitledA.lua,会输出nil和nil;如果运行UntitledB.lua,就会输出UntitledA和string。由此可见,当require时,...就是该lua文件的名字。
--UntitledA.lua
local name = ...
print(name, type(name))
--UntitledB.lua
require "UntitledA"
对上面的代码进行修改,如下。_G["UntitledA"]即_G.UntitledA。从中可以看到,UntitledA这个东西并没有定义的,但可以require结合_G进行隐式地定义。
--UntitledA.lua
local name = ...
print(name, type(name))
_G[name] = "aaa"
--UntitledB.lua
require "UntitledA" --UntitledA string
print(UntitledA) --aaa
print(_G.UntitledA) --aaa
那么,结合之前的setfenv函数,对上面的代码进行修改,如下。在UntitledA.lua中,value1被保存在M这个table中,M也就是UntitledA,此时在_G中不存在value1这个东西。通过这种机制,可以将UntitledA.lua中定义的全局变量绑定在UntitledA这个table中,形成一个"模块"。这样,就算其他模块存在value1这个东西,也互不影响,因为value1都在不同的table中。
--UntitledA.lua
local M = {}
local name = ...
_G[name] = M
setfenv(1, M)
value1 = "123"
--UntitledB.lua
require "UntitledA"
print(UntitledA.value1) --123
print(value1) --nil
print(_G.value1) --nil
在lua中,提供module函数,其作用等价于下面的代码:
local M = {}
local name = ...
_G[name] = M
setmetatable(M, {__index = _G})
setfenv(1, M)
因此,我们可以将UntitledA.lua改写成下面的代码。package.seeall的作用就是相当于调用了setmetatable(M, {__index = _G});
--UntitledA.lua
module(..., package.seeall)
value1 = "123"
最后,总结一下就是:通过module函数来定义模块,通过require函数来使用模块。