Lua


9.1 面向对象基本概念


文档摘要

9.1 面向对象基本概念 Lua 面向对象编程基础概念详解 面向对象编程 (Object-Oriented Programming, OOP) 是一种广泛应用于软件开发中的编程范式。它以“对象”作为基本单元,强调数据和行为的封装、继承、多态等特性,旨在提高代码的可重用性、可维护性和可扩展性。虽然 Lua 语言本身并非天生面向对象,但它提供了强大的元表 (metatable) 和原型 (prototype) 机制,使得在 Lua 中实现面向对象编程成为可能,并且具有 Lua 特有的灵活性和简洁性。 面向对象的基本概念 在深入 Lua 的面向对象实现之前,我们先回顾一下面向对象编程的核心概念: 1.1 对象 (Object) 对象是面向对象编程的核心概念。

9.1 面向对象基本概念

Lua 面向对象编程基础概念详解

面向对象编程 (Object-Oriented Programming, OOP) 是一种广泛应用于软件开发中的编程范式。它以“对象”作为基本单元,强调数据和行为的封装、继承、多态等特性,旨在提高代码的可重用性、可维护性和可扩展性。虽然 Lua 语言本身并非天生面向对象,但它提供了强大的元表 (metatable) 和原型 (prototype) 机制,使得在 Lua 中实现面向对象编程成为可能,并且具有 Lua 特有的灵活性和简洁性。

1. 面向对象的基本概念

在深入 Lua 的面向对象实现之前,我们先回顾一下面向对象编程的核心概念:

1.1 对象 (Object)

对象是面向对象编程的核心概念。它可以被看作是现实世界中一个具体事物的抽象表示,例如一辆汽车、一个人、一个账户等。在编程中,对象将 数据 (属性/状态)行为 (方法/操作) 封装在一起。

  • 属性 (Attribute/State): 描述对象特征的数据,例如汽车的颜色、型号、速度;人的姓名、年龄、身高;账户的余额、账户类型等。在 Lua 中,属性通常以变量的形式存储在对象内部。

  • 方法 (Method/Operation): 对象能够执行的操作或行为,例如汽车的启动、加速、刹车;人的说话、行走、吃饭;账户的存款、取款、查询余额等。在 Lua 中,方法通常是以函数形式定义,并且与对象关联。

代码实践:Lua 中模拟对象

在 Lua 中,最常用的数据结构是 table。我们可以使用 table 来模拟对象,将对象的属性存储为 table 的字段,将对象的方法存储为 table 的函数。

-- 定义一个表示汽车的对象 local Car = { color = "红色", model = "SUV", speed = 0 } -- 定义汽车的方法 function Car:start() print("汽车启动了!") self.speed = 10 -- 修改对象自身的状态 (属性) end function Car:accelerate(increment) self.speed = self.speed + increment print("汽车加速,当前速度:" .. self.speed) end function Car:brake() self.speed = 0 print("汽车刹车,速度归零。") end -- 创建一个汽车对象实例 local myCar = Car -- 调用对象的方法 myCar:start() myCar:accelerate(30) myCar:brake() -- 访问对象的属性 print("汽车颜色:" .. myCar.color) print("汽车型号:" .. myCar.model) print("汽车当前速度:" .. myCar.speed)

代码详解:

  • Car = { ... }: 我们创建了一个 table 名为 Car,用它来模拟汽车对象。color, model, speedCar 对象的属性,存储在 table 的字段中。

  • function Car:start() ... end: 我们定义了 Car 对象的方法 start, accelerate, brake。注意这里使用了冒号 : 来定义方法。冒号语法是 Lua 中定义对象方法的语法糖,它会自动将 self 参数传递给函数,self 指向调用该方法的对象自身。

  • self.speed = ...: 在方法内部,我们使用 self 来访问和修改对象自身的属性。例如 self.speed 指的是当前 Car 对象的 speed 属性。

  • local myCar = Car: 这里我们简单地将 Car 表赋值给 myCar。在 Lua 中,表是引用类型,所以 myCarCar 指向同一个表。 这在一定程度上模拟了对象的创建,但更严谨的面向对象实现需要使用类和构造函数,我们稍后会介绍。

  • myCar:start(), myCar:accelerate(30), myCar:brake(): 我们使用冒号 : 调用对象的方法。myCar:start() 等价于 Car.start(myCar),冒号语法会自动将 myCar 作为第一个参数 (即 self) 传递给 start 函数。

  • myCar.color, myCar.model, myCar.speed: 我们使用点号 . 来访问对象的属性。

内容详解:

这段代码展示了如何在 Lua 中使用 table 模拟对象。我们定义了一个 Car 表,将属性和方法都放在这个表中。通过冒号语法和 self 关键字,我们实现了方法对对象自身属性的访问和修改,初步体现了对象的封装性。

需要注意的是,这种简单的模拟方式存在一些问题:

  • 类型和实例的概念模糊: Car 表既充当了“类”的角色(定义了对象的结构和行为),又充当了“实例”的角色(myCar 直接赋值自 Car)。 这会导致所有 "实例" 共享同一个属性表,修改一个 "实例" 的属性会影响到所有 "实例"。

  • 缺乏真正的对象创建机制: 我们只是简单地将表赋值给变量,没有一个明确的对象创建过程,不利于创建多个独立的、拥有各自状态的对象。

为了解决这些问题,我们需要引入 类 (Class)构造函数 (Constructor) 的概念。

1.2 类 (Class)

类是对象的 蓝图模板。它定义了一类对象所共有的属性和方法。类本身并不代表具体的对象,而是一种抽象的描述。对象是类的 实例 (Instance),根据类这个模板创建出来的具体实体。

可以将类比作建筑设计图纸,而对象则是根据图纸建造出来的房子。同一个图纸可以建造出很多房子,这些房子都具有相同的基本结构(类定义的属性和方法),但可能在颜色、装修等方面有所不同(对象自身的属性值)。

代码实践:Lua 中模拟类和构造函数

为了更好地模拟类和对象的概念,我们需要引入 构造函数 (Constructor)。构造函数是一个特殊的函数,用于创建类的实例,并初始化实例的属性。

-- 定义 Car 类 local Car = {} -- Car 类的构造函数 function Car:new(color, model) local obj = { -- 创建一个新的 table 作为对象实例 color = color, model = model, speed = 0 } setmetatable(obj, self) -- 设置元表,实现继承和方法查找 self.__index = self -- 设置 __index 元方法,指向类自身 return obj -- 返回创建的对象实例 end -- Car 类的方法 (与之前相同) function Car:start() print("汽车启动了!") self.speed = 10 end function Car:accelerate(increment) self.speed = self.speed + increment print("汽车加速,当前速度:" .. self.speed) end function Car:brake() self.speed = 0 print("汽车刹车,速度归零。") end -- 创建 Car 类的两个实例 local car1 = Car:new("蓝色", "轿车") local car2 = Car:new("黑色", "跑车") -- 操作 car1 和 car2 对象 car1:start() car1:accelerate(50) car2:start() car2:accelerate(100) -- 访问 car1 和 car2 对象的属性 print("car1 颜色:" .. car1.color .. ", 型号:" .. car1.model .. ", 速度:" .. car1.speed) print("car2 颜色:" .. car2.color .. ", 型号:" .. car2.model .. ", 速度:" .. car2.speed)

代码详解:

  • local Car = {}: 我们仍然使用 table Car 来表示类。

  • function Car:new(color, model) ... end: new 函数被定义为 Car 类的方法,它充当了构造函数的角色。

    • local obj = { ... }: 在 new 函数内部,我们创建一个新的 table obj,作为类的实例。实例的属性 color, model, speed 在这里被初始化。

    • setmetatable(obj, self): 这是 Lua 面向对象实现的关键一步。 我们将 obj 的元表 (metatable) 设置为 self,这里的 self 指的是 Car 类自身。元表用于控制表的行为,包括查找不存在的字段和调用不存在的方法。

    • self.__index = self: 我们设置元表的 __index 元方法为 self (即 Car 类自身)。__index 元方法在访问表中不存在的字段时会被调用。当访问 obj 对象不存在的字段或方法时,Lua 会查找其元表的 __index 元方法。由于 __index 指向 Car 类自身,Lua 就会在 Car 类中查找,从而实现了 方法继承 的效果。

    • return obj: new 函数返回新创建的对象实例 obj

  • local car1 = Car:new("蓝色", "轿车"), local car2 = Car:new("黑色", "跑车"): 我们通过调用 Car:new(...) 来创建 Car 类的实例。每个实例 car1car2 都是独立的 table,拥有各自的属性值。

  • car1:start(), car2:start(): 当我们调用 car1:start() 时,Lua 首先在 car1 对象自身查找 start 方法,没有找到。然后 Lua 会查找 car1 的元表(即 Car 类)的 __index 元方法,找到 Car 类,并在 Car 类中找到了 start 方法,因此成功调用了 Car 类中定义的 start 方法。这体现了 方法查找和继承 的机制。

内容详解:

通过引入构造函数 new 和元表机制,我们更清晰地模拟了类和对象的概念。Car 表扮演了类的角色,Car:new(...) 充当了构造函数,car1car2 则是 Car 类的实例。

关键点:元表和 __index

Lua 的元表机制是实现面向对象编程的基础。__index 元方法在方法查找和继承中起着至关重要的作用。当访问对象不存在的字段或方法时,Lua 会通过 __index 元方法在元表中查找,从而实现了方法和属性的继承。

1.3 封装 (Encapsulation)

封装是面向对象编程的核心原则之一。它指的是将 数据 (属性)操作 (方法) 捆绑在一起,作为一个独立的单元 (对象),并对外部隐藏对象内部的实现细节,只暴露必要的接口与外部进行交互。

封装的主要目的是:

  • 数据隐藏 (Data Hiding): 保护对象内部的数据不被外部随意访问和修改,防止数据被破坏或滥用。

  • 模块化 (Modularity): 将复杂系统分解为独立的模块 (对象),提高代码的可读性、可维护性和可重用性。

  • 接口隔离 (Interface Isolation): 对象只通过定义好的接口与外部交互,降低了模块之间的耦合度,提高了系统的灵活性和可扩展性。

代码实践:Lua 中的封装

Lua 并没有像 Java 或 C++ 那样的访问修饰符 (public, private, protected) 来严格控制成员的访问权限。Lua 的封装更多地依赖于 编程约定闭包 (closure)

1. 编程约定:使用下划线 _ 前缀表示私有成员

一种常见的约定是使用下划线 _ 前缀来命名对象的私有属性和方法。虽然这种方式并不能阻止外部访问,但它是一种清晰的约定,表明这些成员是内部实现细节,不应该被外部直接访问。

local Account = {} function Account:new(balance) local obj = { _balance = balance, -- 使用 _ 前缀表示私有属性 _transactionHistory = {} -- 私有属性 } setmetatable(obj, self) self.__index = self return obj end function Account:deposit(amount) if amount > 0 then self._balance = self._balance + amount table.insert(self._transactionHistory, {type = "deposit", amount = amount, time = os.time()}) print("存款成功,当前余额:" .. self._balance) else print("存款金额必须大于零。") end end function Account:withdraw(amount) if amount > 0 and amount <= self._balance then self._balance = self._balance - amount table.insert(self._transactionHistory, {type = "withdraw", amount = amount, time = os.time()}) print("取款成功,当前余额:" .. self._balance) else print("取款失败,余额不足或金额无效。") end end function Account:getBalance() return self._balance -- 提供公共方法访问私有属性 end function Account:getTransactionHistory() return self._transactionHistory -- 提供公共方法访问私有属性 end local myAccount = Account:new(1000) myAccount:deposit(500) myAccount:withdraw(200) print("账户余额:" .. myAccount:getBalance()) print("交易记录:") for _, transaction in ipairs(myAccount:getTransactionHistory()) do print(transaction.type .. " 金额:" .. transaction.amount .. " 时间:" .. os.date("%Y-%m-%d %H:%M:%S", transaction.time)) end -- 尝试直接访问私有属性 (虽然可以访问,但不推荐) print("直接访问 _balance 属性:" .. myAccount._balance)

代码详解:

  • _balance_transactionHistory 属性使用下划线 _ 前缀,表示它们是 Account 对象的内部私有属性。

  • deposit, withdraw, getBalance, getTransactionHistory 方法是 Account 对象的公共接口,外部代码应该通过这些方法来操作和访问账户的状态。

  • getBalancegetTransactionHistory 方法提供了受控的访问私有属性的方式。外部代码只能通过这些方法读取私有属性的值,而不能直接修改它们。

  • print("直接访问 _balance 属性:" .. myAccount._balance) 这行代码演示了虽然使用了下划线约定,但仍然可以直接访问 _balance 属性。 这说明 Lua 的封装更多依赖于约定,而不是语言本身的强制机制。

2. 闭包 (Closure) 实现更严格的封装

闭包是 Lua 中一种强大的特性,可以用来实现更严格的封装。通过将私有属性定义在构造函数的局部作用域中,并返回闭包函数作为公共方法,可以实现真正的信息隐藏。

local Account = {} function Account:new(balance) local _balance = balance -- 私有属性,定义在局部作用域 local _transactionHistory = {} -- 私有属性 local obj = {} -- 公共方法,以闭包形式返回,可以访问局部作用域的私有属性 obj.deposit = function(self, amount) -- 注意这里 self 是显式参数 if amount > 0 then _balance = _balance + amount -- 访问闭包中的 _balance table.insert(_transactionHistory, {type = "deposit", amount = amount, time = os.time()}) print("存款成功,当前余额:" .. _balance) else print("存款金额必须大于零。") end end obj.withdraw = function(self, amount) -- 注意这里 self 是显式参数 if amount > 0 and amount <= _balance then _balance = _balance - amount -- 访问闭包中的 _balance table.insert(_transactionHistory, {type = "withdraw", amount = amount, time = os.time()}) print("取款成功,当前余额:" .. _balance) else print("取款失败,余额不足或金额无效。") end end obj.getBalance = function(self) -- 注意这里 self 是显式参数 return _balance -- 访问闭包中的 _balance end obj.getTransactionHistory = function(self) -- 注意这里 self 是显式参数 return _transactionHistory -- 访问闭包中的 _transactionHistory end setmetatable(obj, self) self.__index = self return obj end local myAccount = Account:new(1000) myAccount:deposit(500) myAccount:withdraw(200) print("账户余额:" .. myAccount:getBalance()) print("交易记录:") for _, transaction in ipairs(myAccount:getTransactionHistory()) do print(transaction.type .. " 金额:" .. transaction.amount .. " 时间:" .. os.date("%Y-%m-%d %H:%M:%S", transaction.time)) end -- 尝试直接访问私有属性 (会报错,无法访问) -- print("直接访问 _balance 属性:" .. myAccount._balance) -- 会报错,_balance 是局部变量

代码详解:

  • _balance_transactionHistory 属性被定义在 Account:new 构造函数的 局部作用域 中,而不是作为对象的字段。

  • deposit, withdraw, getBalance, getTransactionHistory 方法以 闭包 的形式返回。这些闭包函数可以访问其外部作用域(即 Account:new 函数的作用域)中的局部变量 _balance_transactionHistory

  • 在外部,我们只能通过调用 myAccount.deposit(), myAccount.getBalance() 等方法来操作和访问账户的状态。无法直接访问到 _balance_transactionHistory 局部变量,实现了真正的信息隐藏。

  • 注意闭包方法定义中,self 参数是显式声明的,因为闭包函数不再是对象的方法,而是对象的一个普通字段,需要在调用时显式传递 self 对象。

内容详解:

闭包方式提供了更严格的封装性。私有属性被限制在构造函数的局部作用域内,外部代码无法直接访问,只能通过对象提供的公共方法来间接操作。这种方式更符合面向对象编程中封装的原则,提高了代码的安全性、可维护性和模块化程度。

选择封装方式:

  • 下划线约定: 简单易用,适合小型项目或对封装性要求不高的场景。

  • 闭包: 提供更严格的封装性,适合大型项目或对安全性要求较高的场景。但代码相对复杂一些。

1.4 继承 (Inheritance)

继承是面向对象编程的另一个重要特性。它允许创建一个新的类 (子类/派生类),继承 另一个已存在的类 (父类/基类) 的属性和方法。子类可以复用父类的代码,并在此基础上扩展或修改功能,从而减少代码冗余,提高代码的可重用性和可维护性。

继承体现了 “is-a” 的关系。例如,“狗” 一种 “动物”,“汽车” 一种 “交通工具”。子类是父类的一种特殊类型,拥有父类的所有特性,并可能具有自身独特的特性。

继承的类型:

  • 单继承 (Single Inheritance): 一个子类只能继承一个父类。

  • 多继承 (Multiple Inheritance): 一个子类可以继承多个父类。

Lua 主要通过 元表和 __index 元方法 来实现继承,它更接近于 原型继承 (Prototypal Inheritance) 的概念,而不是传统的类继承。

代码实践:Lua 中的继承

-- 定义 Animal 类 (父类/基类) local Animal = {} function Animal:new(name) local obj = { name = name } setmetatable(obj, self) self.__index = self return obj end function Animal:makeSound() print("动物发出声音...") end function Animal:getName() return self.name end -- 定义 Dog 类 (子类/派生类),继承自 Animal 类 local Dog = {} setmetatable(Dog, {__index = Animal}) -- 设置 Dog 类的元表,__index 指向 Animal 类 function Dog:new(name, breed) local obj = Animal:new(name) -- 调用父类构造函数初始化父类属性 obj.breed = breed -- 子类特有的属性 setmetatable(obj, Dog) -- 设置子类实例的元表为 Dog 类 Dog.__index = Dog -- 重新设置 __index,指向子类自身 (重要) return obj end -- 重写父类的方法 function Dog:makeSound() print("汪汪汪!") -- 子类特有的叫声 end function Dog:getBreed() return self.breed -- 子类特有的方法 end -- 创建 Animal 和 Dog 类的实例 local animal1 = Animal:new("普通动物") local dog1 = Dog:new("旺财", "中华田园犬") -- 调用方法 animal1:makeSound() -- 调用 Animal 类的方法 print(animal1:getName()) dog1:makeSound() -- 调用 Dog 类重写的方法 print(dog1:getName()) print(dog1:getBreed()) -- 调用 Dog 类特有的方法 -- 验证继承关系 print(getmetatable(Dog).__index == Animal) -- true,Dog 类的元表的 __index 指向 Animal 类 print(dog1.__index == Dog) -- 应该为 nil, 因为dog1的元表是Dog,Dog的__index才是Dog本身,dog1自身没有__index print(getmetatable(dog1).__index == Dog) -- true,dog1 实例的元表的 __index 指向 Dog 类

代码详解:

  • local Dog = {}: 定义 Dog 类。

  • setmetatable(Dog, {__index = Animal}): 实现继承的关键步骤。 我们将 Dog 类的元表设置为一个新的 table,并将该 table__index 元方法指向 Animal 类。这意味着当在 Dog 类中找不到方法或属性时,Lua 会去 Animal 类中查找,从而实现了 方法和属性的继承

  • function Dog:new(name, breed) ... end: Dog 类的构造函数。

    • local obj = Animal:new(name): 调用父类 Animal 的构造函数 Animal:new(name) 来初始化从父类继承的属性 name 这是继承中非常重要的步骤,确保子类实例也具有父类的属性。

    • obj.breed = breed: 初始化子类特有的属性 breed

    • setmetatable(obj, Dog), Dog.__index = Dog: 重新设置子类实例 obj 的元表为 Dog 类,并设置 Dog.__index = Dog 这一步是必要的,确保子类自身的方法也能被正确查找和调用。

  • function Dog:makeSound() ... end: 子类 Dog 重写 (Override) 了父类 AnimalmakeSound 方法。 当调用 dog1:makeSound() 时,Lua 会先在 dog1 对象自身 (或其元表 Dog 类) 中找到 Dog 类重写的 makeSound 方法并执行,而不是执行父类 AnimalmakeSound 方法。这体现了 方法重写和多态 的特性 (多态将在后面介绍)。

  • function Dog:getBreed() ... end: 子类 Dog 定义了自身特有的方法 getBreed

内容详解:

通过元表和 __index 元方法,Lua 实现了原型继承。子类 Dog 通过设置元表,继承了父类 Animal 的属性和方法。子类可以重写父类的方法,也可以扩展自身特有的方法和属性。


发布者: 作者: 转发
评论区 (0)
U