Lua


9.3 类和对象的创建


文档摘要

9.3 类和对象的创建 Lua面向对象编程:深入理解类与对象的创建 (3种方法详解) 引言 面向对象编程 (Object-Oriented Programming, OOP) 是一种强大的编程范式,它强调将数据和操作数据的方法组合成称为“对象”的实体。这种范式通过封装、继承和多态等核心概念,提高了代码的可重用性、可维护性和可扩展性。 Lua 语言虽然以其轻量级和脚本特性而闻名,但它也提供了灵活而强大的机制来实现面向对象编程。Lua 的 OOP 实现方式与传统的类-对象模型有所不同,它主要基于原型 (prototype) 的概念,并巧妙地利用了 表 (table) 和 元表 (metatable) 这两种核心数据结构。

9.3 类和对象的创建

Lua面向对象编程:深入理解类与对象的创建 (3种方法详解)

引言

面向对象编程 (Object-Oriented Programming, OOP) 是一种强大的编程范式,它强调将数据和操作数据的方法组合成称为“对象”的实体。这种范式通过封装、继承和多态等核心概念,提高了代码的可重用性、可维护性和可扩展性。

Lua 语言虽然以其轻量级和脚本特性而闻名,但它也提供了灵活而强大的机制来实现面向对象编程。Lua 的 OOP 实现方式与传统的类-对象模型有所不同,它主要基于原型 (prototype) 的概念,并巧妙地利用了 表 (table)元表 (metatable) 这两种核心数据结构。

第一种方法:使用表作为对象 (基于原型,隐式类)

在 Lua 中,最基本也是最直接的对象创建方式是直接使用表。表本身可以充当对象,其键值对可以表示对象的属性和方法。这种方法体现了 Lua 原型 OOP 的本质:每个对象都可以作为其他对象的原型,而无需显式的类定义。

1. 创建对象 (实例)

我们创建一个表,并为其添加属性和方法,即可将其视为一个对象。

-- 创建一个简单的对象,表示一个 "Dog" local dog = { name = "Buddy", breed = "Golden Retriever", age = 3 } -- 定义方法(函数)作为对象的属性 dog.bark = function(self) -- 'self' 指向对象自身 print(self.name .. " says Woof!") end dog.wagTail = function(self) print(self.name .. " wags its tail happily.") end -- 访问对象的属性 print("Dog's name: " .. dog.name) -- 输出: Dog's name: Buddy print("Dog's breed: " .. dog.breed) -- 输出: Dog's breed: Golden Retriever -- 调用对象的方法 dog:bark() -- 输出: Buddy says Woof! (冒号语法糖,等价于 dog.bark(dog)) dog:wagTail() -- 输出: Buddy wags its tail happily. (冒号语法糖,等价于 dog.wagTail(dog))

代码详解:

  • local dog = { ... }: 我们使用表字面量 {} 创建了一个名为 dog 的表。这个表将作为我们的对象实例。

  • name = "Buddy", breed = "Golden Retriever", age = 3: 在表中,我们使用键值对定义了对象的属性,例如 namebreedage。这些键就相当于对象的属性名,值则是属性的具体数据。

  • dog.bark = function(self) ... end: 我们为 dog 对象添加了一个名为 bark 的方法。在 Lua 中,方法本质上就是存储在表中的函数。

    • function(self): 方法定义为一个匿名函数,并接受一个名为 self 的参数。在 Lua 的 OOP 惯例中,self 参数用于指代调用该方法的对象实例本身。

    • print(self.name .. " says Woof!"): 在方法内部,我们使用 self.name 来访问当前对象实例的 name 属性。

  • dog:bark(): 我们使用冒号 : 语法来调用对象的方法。dog:bark() 实际上是 Lua 语法糖,它等价于 dog.bark(dog)。冒号语法会自动将调用方法的对象 (dog 在这里) 作为第一个参数 (self) 传递给方法函数。

优点:

  • 简单直接: 这是 Lua 中最简单、最直接的对象创建方式,易于理解和使用。

  • 灵活性: 由于 Lua 表的动态性,可以随时为对象添加或删除属性和方法。

  • 原型继承的基础: 这种方法是 Lua 原型继承的基础,可以作为构建更复杂 OOP 结构的基础。

缺点:

  • 缺乏结构化: 当需要创建多个相似对象时,需要重复编写类似的代码,代码结构较为分散,不利于维护和复用。

  • 没有显式类定义: 这种方法没有明确的类定义,对象的结构和行为分散在各个对象实例中,不易于理解对象的类型和关系。

  • 继承机制需要手动实现: 如果需要实现继承,需要手动编写代码来模拟原型链,较为繁琐。

适用场景:

  • 简单对象: 适用于创建结构简单的对象,或者只需要少量对象实例的场景。

  • 快速原型开发: 在快速原型开发阶段,可以使用这种方法快速创建对象进行测试和验证。

  • 学习 Lua OOP 的基础: 理解这种方法是理解 Lua 更高级 OOP 机制的基础。

第二种方法:使用元表和 __index 元方法 (基于原型,显式原型)

为了解决第一种方法中缺乏结构化和代码复用的问题,Lua 引入了元表 (metatable) 和元方法 (metamethod) 的机制。我们可以使用元表来定义对象的 "类" (原型),并使用 __index 元方法来实现属性和方法的继承。

1. 创建原型 (Prototype/Class)

我们创建一个表作为原型,用于存储共享的属性和方法。

-- 创建 Dog 的原型 (Prototype) local DogPrototype = { breed = "Unknown Breed" -- 默认品种 } -- 定义原型的方法 DogPrototype.bark = function(self) print(self.name .. " (a " .. self.breed .. ") says Woof!") end DogPrototype.wagTail = function(self) print(self.name .. " (a " .. self.breed .. ") wags its tail.") end

2. 创建对象 (实例) 并关联原型

我们创建一个新的空表作为对象实例,并使用 setmetatable 函数将其元表设置为原型表。

-- 创建一个 Dog 对象,并关联 DogPrototype 作为其元表 local function createDog(name, breed, age) local dog = { name = name, age = age } setmetatable(dog, { __index = DogPrototype }) -- 设置元表和 __index 元方法 return dog end -- 创建两个 Dog 对象实例 local buddy = createDog("Buddy", "Golden Retriever", 3) local max = createDog("Max", "Labrador", 5) local unknownDog = createDog("UnknownDog", nil, 1) -- 使用默认品种 -- 访问对象的属性 (会从原型中查找) print(buddy.name .. "'s breed: " .. buddy.breed) -- 输出: Buddy's breed: Golden Retriever print(max.name .. "'s breed: " .. max.breed) -- 输出: Max's breed: Labrador print(unknownDog.name .. "'s breed: " .. unknownDog.breed) -- 输出: UnknownDog's breed: Unknown Breed (从原型继承) -- 调用对象的方法 (也会从原型中查找) buddy:bark() -- 输出: Buddy (a Golden Retriever) says Woof! max:wagTail() -- 输出: Max (a Labrador) wags its tail. unknownDog:bark() -- 输出: UnknownDog (a Unknown Breed) says Woof!

代码详解:

  • local DogPrototype = { ... }: 我们创建了一个名为 DogPrototype 的表作为 Dog 对象的原型。它包含了共享的属性 breed 和方法 barkwagTail

  • setmetatable(dog, { __index = DogPrototype }): 这是关键的一步。

    • setmetatable(table, metatable): setmetatable 函数用于设置表的元表。

    • { __index = DogPrototype }: 我们创建了一个新的表作为 dog 对象的元表,并将 __index 元方法设置为 DogPrototype

    • __index 元方法: 当 Lua 试图访问一个表中不存在的键时,如果该表有元表并且元表中定义了 __index 元方法,Lua 就会调用 __index 元方法。

      • 在这里,__index 被设置为 DogPrototype 表。这意味着,当访问 dog 对象中不存在的属性或方法时,Lua 会到 DogPrototype 表中去查找。如果找到了,就返回原型表中的值;如果原型表中也没有找到,则返回 nil
  • createDog 函数: 我们创建了一个 createDog 函数,用于封装创建 Dog 对象的逻辑。这个函数接受 namebreedage 作为参数,创建对象实例并设置元表,最后返回对象实例。

优点:

  • 原型继承: 通过 __index 元方法实现了原型继承,对象可以共享原型表中的属性和方法,提高了代码复用性。

  • 结构化: 原型表起到了 "类" 的作用,定义了对象的共享结构和行为,代码结构更加清晰。

  • 易于扩展: 可以通过修改原型表来修改所有基于该原型创建的对象实例的行为。

缺点:

  • 概念相对复杂: 元表和元方法的概念对于初学者来说可能比较抽象,需要理解原型继承的机制。

  • 需要手动管理原型: 需要手动创建和管理原型表,以及使用 setmetatable 函数关联对象和原型。

适用场景:

  • 需要原型继承的场景: 当需要实现对象之间的继承关系,并共享属性和方法时,可以使用这种方法。

  • 需要创建多个相似对象实例的场景: 当需要创建大量结构相似的对象实例时,使用原型可以提高代码复用性和可维护性。

  • 构建更复杂 OOP 框架的基础: 这种方法是 Lua 中构建更复杂 OOP 框架的基础,例如基于类的 OOP 框架。

第三种方法:使用 "类" 函数和构造器 (基于元表,模拟类)

为了进一步简化 OOP 的使用,并使其更接近传统的类-对象模型,我们可以创建一个 "类" 函数,来封装原型创建、对象实例化和构造器定义等操作。这种方法在 Lua 社区中比较流行,可以提供更友好的 OOP 语法。

1. 创建 "类" 函数

我们创建一个名为 Class 的函数,它接受一个可选的父类作为参数,用于创建新的 "类"。

-- 通用的 Class 函数 local function Class(base) local newClass = {} -- 新的类表 (原型) newClass.__index = newClass -- 设置 __index 指向自身,实现方法查找 if base then -- 如果有父类,则实现继承 setmetatable(newClass, { __index = base }) -- 设置 newClass 的元表为父类 newClass.super = base -- 保存父类引用,方便调用父类方法 end -- 定义构造器函数 newClass.new = function(...) local instance = setmetatable({}, newClass) -- 创建实例,并设置元表为类表 if instance:ctor then -- 如果类定义了构造器 (ctor) 方法,则调用 instance:ctor(...) -- 调用构造器,并传递参数 end return instance end return newClass end

代码详解:

  • local function Class(base): Class 函数接受一个可选的 base 参数,用于指定父类。

  • local newClass = {}: 创建一个新的空表 newClass,它将作为新的 "类" (原型)。

  • newClass.__index = newClass: 设置 newClass 自身的 __index 元方法指向自身。这使得在查找对象方法时,首先会在类自身的方法列表中查找。

  • if base then ... end: 如果提供了 base 参数 (父类),则实现继承。

    • setmetatable(newClass, { __index = base }): 将 newClass 的元表设置为 base (父类)。这样,newClass 就继承了 base 的属性和方法。

    • newClass.super = base: 保存父类的引用到 newClass.super 属性,方便在子类中调用父类的方法。

  • newClass.new = function(...) ... end: 定义类的构造器函数 new

    • local instance = setmetatable({}, newClass): 创建一个新的空表 instance 作为对象实例,并使用 setmetatable 将其元表设置为 newClass (类表)。

    • if instance:ctor then instance:ctor(...) end: 检查类是否定义了构造器方法 ctor (constructor 的缩写)。如果定义了,则调用实例的 ctor 方法,并传递构造器参数。

    • return instance: 返回创建的对象实例。

2. 定义类和对象

使用 Class 函数来定义 "类",并使用 类名.new() 来创建对象实例。

-- 定义 Animal 类 local Animal = Class() -- 定义 Animal 类的构造器 function Animal:ctor(name) self.name = name end -- 定义 Animal 类的方法 function Animal:speak() print("Generic animal sound.") end -- 定义 Dog 类,继承自 Animal 类 local Dog = Class(Animal) -- 定义 Dog 类的构造器 (重写父类构造器) function Dog:ctor(name, breed) Animal.ctor(self, name) -- 调用父类构造器 self.breed = breed end -- 定义 Dog 类的方法 (重写父类方法) function Dog:speak() print("Woof!") end -- 定义 Dog 类的新方法 function Dog:wagTail() print(self.name .. " wags its tail.") end -- 创建 Animal 对象 local animal = Animal.new("Generic Animal") animal:speak() -- 输出: Generic animal sound. -- 创建 Dog 对象 local buddy = Dog.new("Buddy", "Golden Retriever") buddy:speak() -- 输出: Woof! (调用 Dog 类的方法,重写了 Animal 类的方法) buddy:wagTail() -- 输出: Buddy wags its tail. (调用 Dog 类自身的方法) print(buddy.name .. "'s breed: " .. buddy.breed) -- 输出: Buddy's breed: Golden Retriever

代码详解:

  • local Animal = Class(): 使用 Class() 函数创建 Animal 类,没有指定父类,表示 Animal 是基类。

  • function Animal:ctor(name) ... end: 定义 Animal 类的构造器 ctor 方法。构造器方法在对象创建时被调用,用于初始化对象的状态。

  • function Animal:speak() ... end: 定义 Animal 类的 speak 方法。

  • local Dog = Class(Animal): 使用 Class(Animal) 函数创建 Dog 类,并指定 Animal 作为父类,实现继承。

  • function Dog:ctor(name, breed) ... end: 定义 Dog 类的构造器,重写了父类的构造器。

    • Animal.ctor(self, name): 在子类构造器中,通常需要先调用父类的构造器,以初始化父类的属性。使用 Animal.ctor(self, name) 调用父类的 ctor 方法,并传递 self 和必要的参数。
  • function Dog:speak() ... end: 定义 Dog 类的 speak 方法,重写了父类的方法。当调用 Dog 对象的 speak 方法时,会执行 Dog 类中定义的版本,而不是 Animal 类中的版本。

  • function Dog:wagTail() ... end: 定义 Dog 类的新方法 wagTail,这是 Dog 类特有的方法。

  • Animal.new("Generic Animal")Dog.new("Buddy", "Golden Retriever"): 使用 类名.new() 语法创建对象实例。Animal.new(...) 会调用 Animal 类的构造器,Dog.new(...) 会调用 Dog 类的构造器。

优点:

  • 更接近传统类-对象模型: 使用 Class 函数和 类名.new() 语法,使得 Lua OOP 的语法更接近传统的类-对象模型,更易于理解和使用。

  • 封装性更好: 通过类来组织属性和方法,代码结构更加清晰,封装性更好。

  • 继承机制更易用: Class 函数封装了继承的实现细节,使得继承更加易用,只需要在创建类时指定父类即可。

  • 构造器和方法定义更规范: 使用 ctor 作为构造器方法名,方法定义也更符合 OOP 的习惯。

缺点:

  • 增加了一层抽象: Class 函数本身是一个抽象层,需要理解 Class 函数的实现原理才能更好地使用。

  • 本质仍然是原型继承: 虽然语法上更像类,但底层仍然是基于原型继承的,需要理解 Lua 原型 OOP 的本质。

适用场景:

  • 需要更结构化 OOP 的场景: 当需要构建更大型、更复杂的面向对象应用程序时,使用 "类" 函数可以提高代码的可读性、可维护性和可扩展性。

  • 希望语法更接近传统类-对象模型的场景: 对于熟悉传统类-对象模型的开发者来说,使用 "类" 函数可以降低学习成本。

  • 构建 Lua OOP 框架或库: "类" 函数是构建更高级 Lua OOP 框架或库的基础。

总结与选择建议

本文介绍了 Lua 中创建类和对象的三种主要方法,每种方法都有其优缺点和适用场景。

  • 第一种方法 (表作为对象):简单直接,适用于小型项目或快速原型开发,但缺乏结构化和代码复用。

  • 第二种方法 (元表和 __index): 实现了原型继承,提高了代码复用性,结构更清晰,是 Lua OOP 的核心机制,但概念相对复杂。

  • 第三种方法 ("类" 函数):语法更接近传统类-对象模型,封装性更好,继承机制更易用,适用于构建更大型的 OOP 应用,但增加了一层抽象。

选择建议:

  • 小型项目或简单对象: 可以使用第一种方法,简单快速。

  • 需要原型继承,但对语法要求不高: 可以使用第二种方法,直接使用元表和 __index 实现原型继承。

  • 大型项目,需要更结构化 OOP,或希望语法更接近传统类-对象模型: 可以使用第三种方法,使用 "类" 函数来构建类和对象。

无论选择哪种方法,理解 Lua 原型 OOP 的本质,以及表和元表在 OOP 中的作用,都是至关重要的。掌握这三种方法,可以根据不同的项目需求和场景,灵活选择最合适的 OOP 方式,构建高效、可维护的 Lua 应用程序。

希望本文能够帮助读者深入理解 Lua 面向对象编程中类和对象的创建,并在实践中灵活运用。Lua 的 OOP 机制虽然与传统 OOP 语言有所不同,但其灵活性和强大性使其成为构建各种类型应用的有力工具。


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