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 类Person={name="",age=0}-- Person 的构造函数functionPerson:new(name,age)localobj={}-- 创建一个新的表作为对象setmetatable(obj,self)-- 设置元表,使其成为 Person 的实例self.__index=self-- 设置索引元方法,指向 Personobj.name=nameobj.age=agereturnobjend-- 添加方法:打印个人信息functionPerson: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}-- 创建矩形对象的构造函数functionRectangle:new(o,length,breadth)o=oor{}-- 如果未传入对象,创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Rectangle 的方法self.__index=self-- 确保在访问时能找到方法和属性o.length=lengthor0-- 设置长度,默认为 0o.breadth=breadthor0-- 设置宽度,默认为 0o.area=o.length*o.breadth-- 计算面积returnoend-- 打印矩形的面积functionRectangle:printArea()print("矩形面积为 ",self.area)end
创建对象
创建对象是为类的实例分配内存的过程,每个类都有属于自己的内存并共享公共数据:
r = Rectangle:new(nil,10,20)
访问属性
我们可以使用点号 . 来访问类的属性:
print(r.length)
访问成员函数
我们可以使用冒号 : 来访问类的成员函数:
r:printArea()
内存在对象初始化时分配。
完整实例
以下我们演示了 Lua 面向对象的完整实例:
-- 定义矩形类Rectangle={area=0,length=0,breadth=0}-- 创建矩形对象的构造函数functionRectangle:new(o,length,breadth)o=oor{}-- 如果未传入对象,创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Rectangle 的方法self.__index=self-- 确保在访问时能找到方法和属性o.length=lengthor0-- 设置长度,默认为 0o.breadth=breadthor0-- 设置宽度,默认为 0o.area=o.length*o.breadth-- 计算面积returnoend-- 打印矩形的面积functionRectangle:printArea()print("矩形面积为 ",self.area)end-- 运行实例:localrect1=Rectangle:new(nil,5,10)-- 创建一个长为 5,宽为 10 的矩形rect1:printArea()-- 输出 "矩形面积为 50"localrect2=Rectangle:new(nil,7,3)-- 创建一个长为 7,宽为 3 的矩形rect2:printArea()-- 输出 "矩形面积为 21"
执行以上程序,输出结果为:
矩形面积为 50 矩形面积为 21
Lua 继承
继承是指一个对象直接使用另一对象的属性和方法,可用于扩展基础类的属性和方法。
Lua 中的继承通过设置子类的元表来实现。
我们可以创建一个新表,并将其元表设置为父类。
以下实例 Square 类将继承 Rectangle 类的属性和方法,并在其基础上做出改动。
-- 定义矩形类Rectangle={area=0,length=0,breadth=0}-- 创建矩形对象的构造函数functionRectangle:new(o,length,breadth)o=oor{}-- 如果未传入对象,创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Rectangle 的方法self.__index=self-- 确保在访问时能找到方法和属性o.length=lengthor0-- 设置长度,默认为 0o.breadth=breadthor0-- 设置宽度,默认为 0o.area=o.length*o.breadth-- 计算面积returnoend-- 打印矩形的面积functionRectangle:printArea()print("矩形面积为 ",self.area)end-- 定义正方形类,继承自矩形类Square=Rectangle:new()-- Square 继承 Rectangle 类-- 重写构造函数(正方形的边长相等)functionSquare:new(o,side)o=oor{}-- 如果未传入对象,创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Rectangle 的方法self.__index=self-- 确保在访问时能找到方法和属性o.length=sideor0-- 设置边长o.breadth=sideor0-- 正方形的宽度和长度相等o.area=o.length*o.breadth-- 计算面积returnoend-- 运行实例:localrect=Rectangle:new(nil,5,10)-- 创建一个长为 5,宽为 10 的矩形rect:printArea()-- 输出 "矩形面积为 50"localsquare=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)Animal={name="Unknown"}-- Animal 类的构造函数functionAnimal:new(o,name)o=oor{}-- 如果没有传入对象,则创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Animal 的方法self.__index=self-- 让对象可以访问 Animal 的方法o.name=nameor"Unknown"-- 设置名称,默认为 "Unknown"returnoend-- Animal 类的方法:叫声functionAnimal:speak()print(self.name.." makes a sound.")end-- 定义狗类(Dog),继承自 AnimalDog=Animal:new()-- Dog 继承 Animal 类-- 重写狗类的构造函数functionDog:new(o,name,breed)o=oor{}-- 如果没有传入对象,则创建一个新的空表setmetatable(o,self)-- 设置元表,使其继承 Dog 和 Animal 的方法self.__index=self-- 让对象可以访问 Dog 的方法o.name=nameor"Unknown"o.breed=breedor"Unknown"returnoend-- 重写狗类的叫声方法(重写 Animal 的 speak 方法)functionDog:speak()print(self.name.." barks.")end-- 创建 Animal 对象localanimal=Animal:new(nil,"Generic Animal")animal:speak()-- 输出 "Generic Animal makes a sound."-- 创建 Dog 对象localdog=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={}-- 为"类"添加一个构造函数functionPerson:new(name,age)localobj={}-- 创建一个新的表作为对象setmetatable(obj,self)-- 设置元表,表示它是Person类的实例self.__index=self-- 设置索引元方法,指向Personobj.name=nameobj.age=agereturnobjend-- 添加方法functionPerson:greet()print("Hello, my name is "..self.name)end-- 定义一个子类 Student 继承自 PersonStudent=Person:new()-- 子类重写父类的方法functionStudent:greet()print("Hi, I'm a student and my name is "..self.name)endlocalperson2=Person:new("Charlie",25)localstudent2=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={}-- 添加封装:隐藏属性functionPerson:new(name,age)localobj={}setmetatable(obj,self)self.__index=selfobj.name=nameobj.age=agereturnobjendfunctionPerson:setName(name)self.name=name-- 提供方法来修改 nameendfunctionPerson:getName()returnself.name-- 提供方法来获取 nameend
通过这种方式,我们可以控制属性的访问,模拟封装。
抽象
抽象指的是简化复杂的事物,将不需要的细节隐藏。虽然 Lua 本身没有类的概念,但我们可以通过封装来达到抽象的目的。
-- 只暴露接口,不暴露实现细节functionPerson:showInfo()print("Name: "..self.name)print("Age: "..self.age)end