现在位置: 首页 > Lua 教程 > 正文

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:依然是矩形的基本类,拥有 lengthbreadtharea 属性,以及计算和打印面积的方法。

Square 类继承自 RectangleSquare 类通过 Rectangle:new() 来继承 Rectangle 类的方法和属性。由于正方形的长度和宽度相等,我们在 Square:new 方法中重写了构造函数,将 lengthbreadth 设置为相同的值(即 side)。

重写构造函数Square:new(o, side) 方法创建正方形对象时,使用传入的边长 side 初始化 lengthbreadth 属性,并计算面积。

运行结果:

矩形面积为  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 类继承 AnimalDog 类继承自 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