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 的构造函数
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)
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