Lua 面向对象
面向对象编程(Object Oriented Programming,OOP)是一种非常流行的计算机编程架构,通过创建和操作对象来设计应用程序。
以下几种编程语言都支持面向对象编程:
- C++
- Java
- Objective-C
- Smalltalk
- C#
- Ruby
Lua 是一种轻量级的脚本语言,虽然它不像 Java 或 C++ 那样内置强大的面向对象(OO)特性,但它非常灵活,可以通过一些技巧实现面向对象编程。
面向对象特征
封装:将数据和方法捆绑在一起,隐藏实现细节,只暴露必要的接口,提高安全性和可维护性。
继承:通过派生新类复用和扩展现有代码,减少重复编码,提高开发效率和可扩展性。
多态:同一操作作用于不同对象时表现不同,支持统一接口调用,增强灵活性和扩展性。
抽象:简化复杂问题,定义核心类和接口,隐藏不必要的细节,便于管理复杂性。
Lua 中面向对象
我们知道,对象由属性和方法组成。
Lua 中的类可以通过 table + function 模拟出来。
至于继承,可以通过 metetable 模拟出来(不推荐用,只模拟最基本的对象大部分实现够用了)。
在 Lua 中,最基本的结构是 table,我们可以使用表(table)来创建对象。
ClassName = {} -- 创建一个表作为类
通过 new 方法(或其他名称)创建对象,并初始化对象的属性。
function ClassName:new(...)
local obj = {} -- 创建一个新的空表作为对象
setmetatable(obj, self) -- 设置元表,使对象继承类的方法
self.__index = self -- 设置索引元方法
-- 初始化对象的属性
obj:init(...) -- 可选:调用初始化函数
return obj
end
表(table)是 Lua 中最基本的复合数据类型,可以用来表示对象的属性。
Lua 中的 function 可以用来表示方法:
function ClassName:sayHello()
print("Hello, my name is " .. self.name)
end
使用 new 方法来创建对象,并通过对象调用类的方法。
local obj = ClassName:new("Alice") -- 创建对象
obj:sayHello() -- 调用对象的方法
在 Lua 中,表(table)可以视为对象的一种变体。和对象一样,表具有状态(成员变量),并且可以代表独立的实体。
表不仅具有数据成员,还可以包含与对象方法类似的成员函数:
实例
Person = {name = "", age = 0}
-- Person 的构造函数
function Person:new(name, age)
local obj = {} -- 创建一个新的表作为对象
setmetatable(obj, self) -- 设置元表,使其成为 Person 的实例
self.__index = self -- 设置索引元方法,指向 Person
obj.name = name
obj.age = age
return obj
end
-- 添加方法:打印个人信息
function Person:introduce()
print("My name is " .. self.name .. " and I am " .. self.age .. " years old.")
end
代码说明:
- Person 是一个表,它有两个属性:name 和 age,这两个属性是类的默认属性。
- Person:new(name, age) 是一个构造函数,用来创建新的 Person 对象。
- local obj = {} 创建一个新的表作为对象,setmetatable(obj, self) 设置元表,使得该表成为 Person 类的实例。
- self.__index = self 设置索引元方法,使得 obj 可以访问 Person 类的属性和方法。
- introduce 是 Person 类的方法,打印该 Person 对象的名字和年龄。
调用方法:
-- 创建一个 Person 对象
local person1 = Person:new("Alice", 30)
-- 调用对象的方法
person1:introduce() -- 输出 "My name is Alice and I am 30 years old."
一个简单实例
以下简单的类包含了三个属性: area, length 和 breadth,printArea方法用于打印计算结果:
实例
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
创建对象
创建对象是为类的实例分配内存的过程,每个类都有属于自己的内存并共享公共数据:
r = Rectangle:new(nil,10,20)
访问属性
我们可以使用点号 .来访问类的属性:
print(r.length)
访问成员函数
我们可以使用冒号 : 来访问类的成员函数:
r:printArea()
内存在对象初始化时分配。
完整实例
以下我们演示了 Lua 面向对象的完整实例:
实例
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
-- 运行实例:
local rect1 = Rectangle:new(nil, 5, 10) -- 创建一个长为 5,宽为 10 的矩形
rect1:printArea() -- 输出 "矩形面积为 50"
local rect2 = Rectangle:new(nil, 7, 3) -- 创建一个长为 7,宽为 3 的矩形
rect2:printArea() -- 输出 "矩形面积为 21"
执行以上程序,输出结果为:
矩形面积为 50 矩形面积为 21
Lua 继承
继承是指一个对象直接使用另一对象的属性和方法,可用于扩展基础类的属性和方法。
Lua 中的继承通过设置子类的元表来实现。
我们可以创建一个新表,并将其元表设置为父类。
以下实例 Square 类将继承 Rectangle 类的属性和方法,并在其基础上做出改动。
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
-- 定义正方形类,继承自矩形类
Square = Rectangle:new() -- Square 继承 Rectangle 类
-- 重写构造函数(正方形的边长相等)
function Square:new(o, side)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = side or 0 -- 设置边长
o.breadth = side or 0 -- 正方形的宽度和长度相等
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 运行实例:
local rect = Rectangle:new(nil, 5, 10) -- 创建一个长为 5,宽为 10 的矩形
rect:printArea() -- 输出 "矩形面积为 50"
local square = Square:new(nil, 4) -- 创建一个边长为 4 的正方形
square:printArea() -- 输出 "矩形面积为 16"
Rectangle 类:依然是矩形的基本类,拥有 length、breadth 和 area 属性,以及计算和打印面积的方法。
Square 类继承自 Rectangle:Square 类通过 Rectangle:new() 来继承 Rectangle 类的方法和属性。由于正方形的长度和宽度相等,我们在 Square:new 方法中重写了构造函数,将 length 和 breadth 设置为相同的值(即 side)。
重写构造函数:Square:new(o, side) 方法创建正方形对象时,使用传入的边长 side 初始化 length 和 breadth 属性,并计算面积。
运行结果:
矩形面积为 50 矩形面积为 16
函数重写
在 Lua 中,函数重写(也称为方法重写)指的是在继承过程中,子类对父类中已有方法的重新定义或替换。
子类可以根据需要修改或扩展父类的方法行为。
以上实例中 Square 类重写了 Rectangle 类的构造函数,从而改变了对象的初始化方式,特别是将矩形的 length 和 breadth 设为相同的值,因为正方形的特性是边长相等。
接下来我们通过一个 Animal 类和一个继承自它的 Dog 类,展示如何重写方法。
Animal = {name = "Unknown"}
-- Animal 类的构造函数
function Animal:new(o, name)
o = o or {} -- 如果没有传入对象,则创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Animal 的方法
self.__index = self -- 让对象可以访问 Animal 的方法
o.name = name or "Unknown" -- 设置名称,默认为 "Unknown"
return o
end
-- Animal 类的方法:叫声
function Animal:speak()
print(self.name .. " makes a sound.")
end
-- 定义狗类(Dog),继承自 Animal
Dog = Animal:new() -- Dog 继承 Animal 类
-- 重写狗类的构造函数
function Dog:new(o, name, breed)
o = o or {} -- 如果没有传入对象,则创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Dog 和 Animal 的方法
self.__index = self -- 让对象可以访问 Dog 的方法
o.name = name or "Unknown"
o.breed = breed or "Unknown"
return o
end
-- 重写狗类的叫声方法(重写 Animal 的 speak 方法)
function Dog:speak()
print(self.name .. " barks.")
end
-- 创建 Animal 对象
local animal = Animal:new(nil, "Generic Animal")
animal:speak() -- 输出 "Generic Animal makes a sound."
-- 创建 Dog 对象
local dog = Dog:new(nil, "Buddy", "Golden Retriever")
dog:speak() -- 输出 "Buddy barks."
Animal类:定义了一个基础类Animal,具有name属性和speak方法。speak方法是一个默认的实现,输出"某个动物发出声音"。Dog类继承Animal:Dog类继承自Animal,并通过Dog:new()方法创建自己的实例。重写
speak方法:在Dog类中,重写了speak方法,将其行为从父类的"发出声音"改为"狗狗叫"。这就是方法重写的体现,子类(Dog)改变了父类(Animal)方法的行为。
运行结果:
Generic Animal makes a sound. Buddy barks.
多态
Lua 的多态性通过元表和方法重写实现。当不同类型的对象调用相同的方法时,Lua 会根据对象的实际类型执行不同的方法。
实例
Person = {}
-- 为"类"添加一个构造函数
function Person:new(name, age)
local obj = {} -- 创建一个新的表作为对象
setmetatable(obj, self) -- 设置元表,表示它是Person类的实例
self.__index = self -- 设置索引元方法,指向Person
obj.name = name
obj.age = age
return obj
end
-- 添加方法
function Person:greet()
print("Hello, my name is " .. self.name)
end
-- 定义一个子类 Student 继承自 Person
Student = Person:new()
-- 子类重写父类的方法
function Student:greet()
print("Hi, I'm a student and my name is " .. self.name)
end
local person2 = Person:new("Charlie", 25)
local student2 = Student:new("David", 18)
-- 多态:不同类型的对象调用相同的方法
person2:greet() -- 输出 "Hello, my name is Charlie"
student2:greet() -- 输出 "Hi, I'm a student and my name is David"
尽管 person2 和 student2 调用了同一个 greet 方法,但由于它们的类型不同,Lua 会调用各自适合的版本。
运行结果:
Hello, my name is Charlie Hi, I'm a student and my name is David
其他面向对象的概念
封装
封装通常通过将数据和方法封装在一个表中实现。我们可以通过控制表的访问权限来模拟封装,例如使用 metamethods 来限制外部访问。
实例
Person = {}
-- 添加封装:隐藏属性
function Person:new(name, age)
local obj = {}
setmetatable(obj, self)
self.__index = self
obj.name = name
obj.age = age
return obj
end
function Person:setName(name)
self.name = name -- 提供方法来修改 name
end
function Person:getName()
return self.name -- 提供方法来获取 name
end
通过这种方式,我们可以控制属性的访问,模拟封装。
抽象
抽象指的是简化复杂的事物,将不需要的细节隐藏。虽然 Lua 本身没有类的概念,但我们可以通过封装来达到抽象的目的。
实例
function Person:showInfo()
print("Name: " .. self.name)
print("Age: " .. self.age)
end
达也酱
jja***@163.com
按实例的写法,每次new新实例的时候都需要将第一个变量的值设为nil,很不方便。
可以稍做变形,把变量o放在函数里创建,免去麻烦。
--创建一个类,表示四边形 local RectAngle = { length, width, area} --声明类名和类成员变量 function RectAngle: new (len,wid) --声明新建实例的New方法 local o = { --设定各个项的值 length = len or 0, width = wid or 0, area =len*wid } setmetatable(o,{__index = self} )--将自身的表映射到新new出来的表中 return o end function RectAngle:getInfo()--获取表内信息的方法 return self.length,self.width,self.area end a = RectAngle:new(10,20) print(a:getInfo()) -- 输出:10 20 200 b = RectAngle:new(10,10) print(b:getInfo()) -- 输出:10 10 100 print(a:getInfo()) -- 输出:10 20 200达也酱
jja***@163.com
gray.yang
gra***ang@fibocom.com
补充: . 与 : 的区别在于使用 : 定义的函数隐含 self 参数,使用 : 调用函数会自动传入 table 至 self 参数,示例:
classA={} function classA:getob(name) print(self) ob={} setmetatable(ob,self) self.__index=self self.name=name return ob end function classA:getself() return self end c1=classA:getob("A") c2=classA:getob("B") print(string.rep("*",30)) print(c1:getself()) print(c2:getself()) print(string.rep("*",30)) ----------------------继承------------------------ classB=classA:getob() ----非常重要,用于获取继承的self function classB:getob(name,address) ob=classA:getob(name) setmetatable(ob,self) self.__index=self self.address=address return ob end c3=classB:getob("gray.yang","shenzhen") print(c3:getself())输出结果:
gray.yang
gra***ang@fibocom.com
miaosu5cm
mia***5cm@163.com
模拟类和继承
classA={} function classA.new(cls,...) --定义类方法时使用"."号,不适用隐式传参 this={} setmetatable(this,cls) cls.__index=cls --将元表的__index设为自身,访问表的属性不存在时会搜索元表 cls.init(this,...) --初始化表,注意访问类的方法都是".",此时不会隐式传入参数 return this end function classA.init(self,name) self.name=name end function classA.getname(self) return self.name end p=classA:new("gray.yang") print(p:getname()) print(string.rep("*",50))模拟继承
classB=classA:new() --获得实例 function classB.new(cls,...) this={} setmetatable(this,cls) cls.__index=cls cls.init(this,...) return this end function classB.init(self,name,address) super=getmetatable(self) super:init(name) --使用父类初始化 self.address=address end function classB.getaddress(self) return self.address end b=classB:new("tom.li","shenzhen") print("getbname==============>",b:getname()) print("getbaddress===========>",b:getaddress())miaosu5cm
mia***5cm@163.com
Ives
571***385@qq.com
多重继承
-- 在table 'plist'中查找'k' local function search(k, plist) for i = 1, #plist do local v = plist[i][k] -- 尝试第i个基类 if v then return v end end end function createClass(...) local c = {} -- 新类 local parents = {...} -- 类在其父类列表中的搜索方法 setmetatable(c, {__index = function(t, k) return search(k, parents) end}) -- 将'c'作为其实例的元表 c.__index = c -- 为这个新类定义一个新的构造函数 function c:new(o) o = o or {} setmetatable(o, c) return o end return c -- 返回新类 end -- 类Named Named = {} function Named:getname() return self.name end function Named:setname(n) self.name = n end -- 类Account Account = {balance = 0} function Account:withdraw(w) self.balance = self.balance - v end -- 创建一个新类NamedAccount,同时从Account和Named派生 NamedAccount = createClass(Account, Named) account = NamedAccount:new() account:setname("Ives") print(account:getname()) -- 输出 IvesIves
571***385@qq.com
大轩
tol***uan@163.com
参考地址
一个简单的面向对象实现
--[[ Lua 中使用":"实现面向对象方式的调用。":"只是语法糖,它同时在方法的声明与实现中增加了一个 名为 self 的隐藏参数,这个参数就是对象本身。 ]] --实例: Account = {balance = 0}; --生成对象 function Account:new(o) o = o or {}; --如果用户没有提供对象,则创建一个。 setmetatable(o, self); --将 Account 作为新创建的对象元表 self.__index = self; --将新对象元表的 __index 指向为 Account(这样新对象就可以通过索引来访问 Account 的值了) return o; --将新对象返回 end --存款 function Account:deposit(v) self.balance = self.balance + v; end --取款 function Account:withdraw(v) self.balance = self.balance - v; end --查询 function Account:demand() print(self.balance); end --创建对象 myAccount = Account:new(); --通过索引访问 print(myAccount.balance); --调用函数 myAccount:deposit(100); myAccount:withdraw(50); myAccount:demand();执行结果:
大轩
tol***uan@163.com
参考地址
wildwolf
wil***lf12@qq.com
其中 A 为抽象类,B 为矩形类,C 为立方体类。
C 继承 B,B 继承 A。
类对象各自独立,不影响类默认成员属性值。
B = {length, width, area} function B:new(len,wid) local A = {length=0,width=0} local o = {} setmetatable(o,A) B.__index=A o.length=len or A.length o.width=wid or A.width o.area=o.length*o.width return o end a=B:new(2,3) b=B:new(3,4) print("长方形a的面积为"..a.area) print("长方形b的面积为"..b.area) print("长方形a的面积仍然为"..a.area..", a与b独立存在") c=B:new() print("长方形c根据默认构造函数的面积为"..c.area..", c的长宽分别为",c.length,c.width) --立方体C,继承长方形类B C = {high=0, volume=0, rectangle=B.new()} --增加体积值和高度 C.__index=C function C:new(len, wid, hig) local o={} setmetatable(o,C) --将原始类C作为它对象的原表 o.rectangle=B:new(len,wid) o.high=hig or C.high o.volume=o.high*o.rectangle.area return o end cubeA=C:new(2,3,4) cubeB=C:new(3,4,5) print("立方体A的体积为"..cubeA.volume) print("立方体B的体积为"..cubeB.volume) print("立方体A的体积仍然为"..cubeA.volume..", A与B独立存在") print("立方体A底面长方体的长与宽分别为" ,cubeA.rectangle.length ,cubeA.rectangle.width) print("立方体B底面长方体的长与宽分别为" ,cubeB.rectangle.length ,cubeB.rectangle.width) print("cubeA和cubeB的底边长方形同样独立存在")wildwolf
wil***lf12@qq.com
小糊涂仙
128***0653@qq.com
我的实测结果与作者的理论有出入,先创建多个对象,然后再依次输出,会发现结果都是最后一个对象的值。
Rectangle 的封装:
Rectangle = {area = 0, length = 0, breadth = 0} function Rectangle:new (o,length,breadth) o = o or {} setmetatable(o, self) self.__index = self self.length = length or 0 self.breadth = breadth or 0 self.area = length*breadth; return o end function Rectangle:printArea () print("矩形面积为 ",self.area) end -- 创建 local r = Rectangle:new(nil, 2, 3); local p = Rectangle:new(nil, 4, 5); -- 输出 r:printArea(); p:printArea();结果:
也就是说,p 和 r 其实不是两个完全无关的对象。
小糊涂仙
128***0653@qq.com
sgjz1973
163***2445@qq.com
回楼上,两个新建实例并没有关系,只是新建实例时修改了元表,第二次新建覆盖了值:
local Rect = {area = 0,length = 0,windth = 0}; function Rect:new(length,windth) local t = {}; setmetatable(t,self); self.__index = self; t.length = length; t.windth = windth; t.area = t.length * t.windth; return t; end function Rect:ShowArea() print(self.area); end local a = Rect:new(1,2); local b = Rect:new(3,4); a:ShowArea(); b:ShowArea(); print(a); print(b);输出结果:
sgjz1973
163***2445@qq.com
dalige
459***786@qq.com
对楼上补充说明:
Rect 作为 new 出来的表(楼上代码写的 a 和 b)的元表:由于没有设置 __newindex 元方法。所以 a 和 b 在赋值的时候把长和宽的值赋给了自己,并没有把值赋给 Rect (也就是说 Rect 表中的数据一直都没有改变过)。
a 和 b 在调用 ShowArea 方法的时候,自己的表里没有这个方法,会到元表中寻找这个方法,元表(Rect)中设置了 __index 元方法,所以能找到 ShowArea 这个方法,然后调用。(注意这里是 a 和 b 调用的 ShowArea 方法,所以 ShowArea 方法中 self 指向的是 a 和 b,而不是 Rect)。
dalige
459***786@qq.com
RUNOOB
429***967@qq.com
简化了 Rectangle 的定义,只需一个空的表格即可:
-- 定义矩形类 Rectangle = {} -- 初始化矩形对象 function Rectangle:new(length, breadth) -- 创建一个新的对象 local newObj = { length = length or 0, breadth = breadth or 0, area = 0 } -- 设置新对象的元表为 Rectangle,以便继承 Rectangle 的方法 setmetatable(newObj, self) self.__index = self -- 计算矩形的面积 newObj.area = length * breadth -- 返回创建的对象 return newObj end -- 打印矩形的面积 function Rectangle:printArea() print("矩形面积为", self.area) end -- 示例用法 -- 创建一个矩形对象 local myRectangle = Rectangle:new(5, 10) -- 打印矩形的面积 myRectangle:printArea()RUNOOB
429***967@qq.com