根据维基百科,面向对象编程(OOP)是一种基于 "对象 "概念的编程范式,它可以包含数据和代码:数据以字段的形式存在(通常称为属性或属性),代码以程序的形式存在(通常称为方法)。
Ruby是一种纯粹的面向对象的语言,这意味着在Ruby语言中,所有东西都是一个对象。这些对象,无论它们是字符串、数字、类、模块等,都在一个叫做 "对象模型 " 的系统中运行。
Ruby提供了一个叫做object_id 的方法,这个方法对所有的对象都可用。这个标识符返回一个整数,对任何两个对象来说都是不一样的。
在这篇文章中,我们将详细介绍以下概念:
- 类和实例
- 继承性
- 公有、私有和受保护的方法
- 混合器
- 模块
- 对象的层次结构
类和实例
在Ruby中,类是定义一个对象的属性和态度(动作)的地方。如果一个对象是圆的(一个属性),并且应该能够说话(一个动作),我们可以从它所属的类中看出,因为这些属性和动作被定义为该类中的方法。 属于一个类的对象被称为该类的实例,并且是通过.new 来创建(实例化)。让我们从创建一个名为Human 的类开始。我想我们都是人类,所以这应该很有趣。
class Human
def initialize(name)
@name = name
end
end
initialize方法确定了创建一个新的类的实例的要求。在上面的例子中,我们可以看到,要创建一个新的人类,需要一个名字。因此,一个新的人类实例可以用命令Human.new(name) ,其中的名字是你选择的任何东西。在我们的例子中,让我们使用 "Henry"。为了在我们的irb环境中测试这一点,我们需要做的就是使用命令load './path_to_filename' 加载文件,并在文件发生变化时重新使用该命令进行重新执行。在我的例子中,它是load './Human.rb' ,因为irb是在包含上述文件的文件夹中打开的。为了在没有irb的情况下运行代码,我们需要在每个命令前添加一个puts 语句,这样就可以看到结果了。
当我们试图创建一个没有名字的新人类时,我们会得到一个参数错误,因为需要一个名字。然而,当我们正确操作时,我们看到名为Henry的人类被创建,并且属于Human 。因此,亨利是类Human 的一个实例。
@name 变量被称为实例变量,因为它以@ 符号开头,这意味着只要相关的类实例存在,这个变量就可以被类中的任何其他方法所引用。在这里,我们已经创建了它,并将其设置为等于对象初始化时的名称。
让我们继续定义属于这个类的任何对象的特征和行为。由于创建的对象被称为该类的实例,定义其行为的方法被称为实例方法。人类有一个名字和某些身体部位,可以执行某些动作,所以我们来定义它们。
def name
@name
end
def no_of_legs
2
end
def no_of_hands
2
end
def speak
'blablabla'
end
我们添加了一些方法来检索创建的人类的名字,定义人类的腿和手的数量,并赋予人类说话的能力。我们可以使用instance.method_name ,在类实例上调用这些方法,如下图所示。

如果我们改变主意,决定要改变我们的类实例的名字Henry,怎么办?Ruby有一个内置的方法,我们可以用它来做这件事,但也可以手动完成。我们可以手动改变我们的名字方法,从一个获取名字的getter方法变成一个setter方法,如果提供了一个值,也可以将其设置为一个值。
def name=(new_name)
@name = new_name
end
使用Ruby内置的attr_accessor 方法,我们可以丢弃我们的name方法,用attr_accessor :name 行来代替。
class Human
attr_accessor :name
def initialize(name)
@name = name
end
# rest of the code
end
不管选择哪种方法,到最后,这就是可以得到的。

到目前为止创建的所有方法都被称为instance methods ,因为它们可以在类的任何实例上调用,但不是在类本身上。还有一种叫做class method ,它可以在类本身上调用,而不是在其实例上。类方法的命名方式是在方法名前加上self. 。 这里有一个例子。
# within the Human class
def self.introduction
"I am a human, not an alien!"
end

正如我们在上面看到的,类方法只对类本身可用,而不是对其实例。此外,就像实例变量的存在一样,我们也有类变量,其前缀为两个@ 符号。下面是一个例子。
class Human
attr_accessor :name
@@no_cars_bought = 0 #class variable
def initialize(name)
@name = name
end
def self.no_cars_bought
@@no_cars_bought
end
def buy_car
@@no_of_cars_bought += 1
"#{@name} just purchased a car"
end
end
在这个例子中,我们添加了一个buy_car 方法,使每个人都能购买一辆汽车。我们还创建了一个叫做@@no_of_cars_bought 的类变量,每次买车都会增加1。最后,我们创建了一个名为no_cars_bought 的类方法,用来获取已经购买的汽车的数量。让我们看看这是如何工作的。

到目前为止定义的所有方法都被称为公共方法,因为它们可以在类外被访问。我们也可以定义称为私有方法的方法,这些方法只能在类内访问。让我们来试着举个例子。
# at the bottom of the Human class
def say_account_number
"My account number is #{account_number}"
end
private
def account_number
"1234567890"
end
这得到的结果如下:

当我们调用henry.account_number ,我们得到一个 "NoMethodError",因为account_number 是一个私有方法,只能在类内访问。当像我们这样通过say_account_number 方法访问时,没有错误,因为这个方法与私有方法存在于同一个类中。需要注意的是,private 关键字之后的每一个实例方法都会变成一个私有方法;因此,私有方法应该定义在类的底部,在所有的公共方法之后。
你听说过受保护的方法吗?是的,没错!这些方法也是存在的,但我们会在了解了继承的概念后再来讨论它们。
继承
现在我们已经知道了类和实例,让我们继续讨论继承问题。为了正确理解继承的概念,让我们创建一个新的类,叫做Mammal ,因为人类是哺乳动物。我想起科学课上的某句话:"所有人类都是哺乳动物,但不是所有哺乳动物都是人类"。我还记得,哺乳动物的一些特征包括有毛发或皮毛和复杂的大脑。让我们把这些信息放在Mammal 类中。
class Mammal
def has_hair?
"Most certainly, Yes"
end
def has_complex_brain?
"Well, as a mammal, what do you expect?"
end
end
你还记得我的科学课用语吗?如果是的话,重要的是我们要建立一种关系来验证这一说法。那么,我们该怎么做呢?我们通过在其类定义中添加< Mammal ,允许人类类继承Mammal 类的属性。
class Human < Mammal
# all other code
end
一个继承另一个类的属性的类被称为子类,而它所继承的类被称为超类。在我们的例子中,Human 是子类,Mammal 是超类。在这一点上需要注意的是,如果你像我们现在这样在一个文件中定义所有的类,那么在你的文件中,Mammal 类的定义应该在Human 类之前,因为我们不希望在一个变量被定义之前引用它。让我们看看人类现在有哪些额外的功能。

如上所示,人类现在可以访问Mammal 类中定义的所有属性。如果我们去掉继承(即从那行代码中去掉< Mammal )并运行命令henry.class.superclass ,我们会得到 "Object "作为响应。这告诉我们,每一个类在不直接继承于另一个类时,其超类都是 "Object",这进一步加强了在Ruby中,即使是类也是对象的事实。

现在我们知道了什么是超类,现在是谈论关键字super的最佳时机。Ruby提供这个关键字是为了能够重复使用和修改超类中已经存在的方法。在我们的Mammal 超类中,记得我们有一个叫做has_hair? 的方法;如果我们想在这个方法被调用时添加一些专门针对人类的额外信息,该怎么办?这就是使用超级关键字的地方。在我们的Human 类中,我们定义了一个同名的方法,has_hair? 。
def has_hair?
super + ", but humans can be bald at times"
end
当超级关键字被调用时,Ruby会在超类中寻找该方法的名称并返回其结果。在上面的方法中,我们为超类的has_hair? 方法产生的结果添加了一些额外的信息。

Ruby只支持单类继承,这意味着你只能从一个类中继承类属性。我相信你一定想知道,如果你想在你的类中添加多个非特定类的属性,会发生什么。RUBY以混合器的形式对此做了规定,但在我们讨论混合器之前,让我们先谈谈受保护方法。
受保护的方法
受保护方法的功能类似于私有方法,它们可以在一个类和它的子类中被调用。让我们在Mammal 类中创建一个受保护的方法。
#at the bottom of the Mammal class
def body_status
body
end
protected
def body
"This body is protected"
end

如上所示,我们不能访问body 方法,因为它是受保护的。然而,我们可以从body_status 方法中访问它,它是Mammal 类中的另一个方法。我们也可以从定义该方法的类的子类中访问受保护的方法。让我们在Human 类中尝试一下。
# within the Human class
def check_body_status
"Just a human checking that " + body.downcase
end

如上所示,无论主体方法是否定义在Human 类中并被保护,Human 类都可以访问它,因为它是Mammal 类的一个子类。这种子类的访问对于私有方法来说也是可能的;然而,这导致了这些方法之间存在什么区别的问题。
受保护的方法可以使用显式接收器来调用,如receiver.protected_method ,只要有关的接收器是关键字self ,或者与self 在同一个类中。然而,只有当显式接收器是self ,私有方法才能使用显式接收器来调用。让我们编辑我们的代码,在check_body_status 方法中用self.body.downcase 替换body.downcase ,并改变我们的私有方法调用,也包括self 。
def check_body_status
"Just a human checking that " + self.body.downcase
# body method is protected
end
def say_account_number
"My account number is #{self.account_number}"
# account_number method is private
end

附注:在Ruby 2.7之前,当调用一个私有方法来获取一些信息时,使用self 是不可能的。
让我们继续用与self 同类的对象来代替self 。在我们的例子中,self 就是Henry ,他正在调用这些方法,而Henry 是Human 类的一个实例,它继承自Mammal 类。
def check_body_status
non_self = Human.new('NotSelf') #same class as self
puts "Just #{non_self.name} checking that " + non_self.body.downcase
# Mammal.new is also in the same class as self
"Just a human checking that " + Mammal.new.body.downcase
end
def say_account_number
non_self = Human.new('NotSelf')
"My account number is #{non_self.account_number}"
end

如上所示,在受保护的方法中可以用与self 同类的对象替换self ,但在私有方法中这是不可能的。
混合器
混合器是一组在模块中定义的代码,当它被包含在任何类中或扩展到任何类中时,就为该类提供了额外的能力。一个类可以添加多个模块,因为这没有任何限制,与类的继承不同。根据这些信息,让我们创建一个名为Movement 的模块,为人类类添加运动能力。
module Movement
def hop
"I can hop"
end
def swim
"Ermmmm, I most likely can if I choose"
end
end
下一步将是在我们的Human 类中包含这个模块。这可以通过在相关的类中添加短语include <module_name> 来实现。在我们的例子中,这需要将include Movement 加入到人类类中,如下图所示。
class Human < Mammal
include Movement
# rest of the code
end
让我们测试一下这段代码。

如上所示,通过模块混入类的方法只对类的实例可用,而不是对类本身。如果我们想让模块中的方法对类而不是实例可用呢?要做到这一点,我们用 "扩展 "代替 "包括",如extend Movement 。

.extend 术语也可以用在类的实例上,但方式非常不同。让我们创建另一个名为Parent 的模块。
module Parent
def has_kids?
"most definitely"
end
end

如上所示,在 dad 变量上使用.extend(Parent) 使得has_kids? 方法对其可用。当我们不希望模块的方法混入每个类实例时,这一点特别有用。因此,extend 可以只在我们感兴趣的特定实例上使用。
模块
模块是类、方法、常量,甚至是其他模块的外壳。除了作为混合函数使用外,如前所述,模块还有其他用途。在Ruby中,每个类都是一个模块,但没有一个模块是类,因为模块既不能被实例化也不能被继承。 为了理解命名间距,让我们创建一个包含常量、类、方法和另一个模块的模块。
module Male
AGE = "Above 0"
class Adult
def initialize
puts "I am an adult male"
end
end
def self.hungry
puts "There's no such thing as male food, I just eat."
end
module Grown
def self.age
puts "18 and above"
end
end
end
你可能已经注意到,在上面的模块中定义和调用的方法前缀是self. 。这是因为模块不能被实例化,在模块中定义的任何方法如果没有self. 前缀,就只能作为一个混合体使用,正如我们在前面讨论mixins 时看到的那样。要调用一个模块本身的方法,我们必须通过使用self.method_name ,在方法名称上确定这一点。回顾一下,我们在类方法中也使用了self 关键字;同样的原则也适用于此。

如上图所示,要到达一个模块内定义的类、模块或常量,我们使用module_name::target_name 。为了正确理解命名的概念,我们将创建另一个模块,其中包含一个名为Adult 的类,然后我们将看到这两个类是如何区分的。
module Female
class Adult
def initialize
puts "I am an adult female"
end
end
def self.hungry
puts "Maybe there's such a thing as female food, I'm not sure. I just need to eat"
end
end

如上所示,我们有两个名字为 "Adult "的类。通过将它们包裹在自己的模块中,我们能够使用这些名字,而不会对每次调用的确切的成人类产生任何混淆。此外,我们的代码更有可读性和组织性,我们实现了关注点分离的设计原则,因为我们将不同的代码块根据其功能分成了几个类别。
对象层次结构
理解类、实例、模块和继承的概念是很惊人的,但是如果不理解Ruby中的对象层次结构,这方面的知识就不完整。这指的是在Ruby中搜索一个方法的顺序。有一些方法可以帮助我们理解这种层次结构;其中之一是ancestors 方法。这个方法对类的实例是不可用的,但可以在类本身和它们的祖先上调用。下面是一个例子。

从Human.ancestors 的结果中,我们可以看到这个方法返回相关的类,它直接包含的模块,它的父类,以及父类的直接包含的模块;这个循环一直持续到Basic Object ,它是所有对象的根。
另一个可用来获取一个类的更多信息的方法是included_modules 。

如上所示,人类类中包含了两个模块:一个是我们用include Movement 直接包含的模块,另一个是包含在Object类中的模块。这意味着每个类都从它的祖先类中继承了类的属性,每个包含在其中的模块都会被包含在其中。 基于这些信息,我们将进行一个简单的练习来确认Ruby中的方法查找路径,以及在这个路径中哪些类比其他类有优先权。我们将定义名称相同但输出字符串不同的方法,并将它们放在Human 类、Movement 模块和Mammal 类中。
# in the mammal class
def find_path
"Found me in the Mammal class path"
end
# in the Movement module
def find_path
"Found me in the Movement module path"
end
# in the Human class
def find_path
"Found me in the Human class path"
end
现在让我们来进行这个练习:

如上所示,当我们在一个类上调用.ancestors 时,祖先的排列顺序是在寻找该类实例上调用的方法时所遵循的路径。对于我们创建的Human 类实例,Ruby首先在该类本身中搜索该方法;如果没有找到,则继续搜索任何直接包含的模块;如果没有找到,则继续搜索超类,循环往复,直到搜索到BasicObject 。如果在那里仍然没有找到该方法,则会返回一个 "NoMethodError"。通过使用.ancestors 方法,我们可以确定一个对象的查找路径,以及它的方法在任何时间点上的来源。
Ruby还提供了一个叫做methods 的方法,通过它我们可以识别任何对象的所有方法。我们甚至可以进行减法,以显示哪些方法来自它的父对象,哪些是它特有的。下面是一个例子。

如上所示,变量Henry有很多可用的方法。然而,当我们从对象类可用的方法中减去这些方法时,我们发现我们只剩下那些我们在文件中特别定义的方法,而其他的方法都是继承的。这对每个对象、它的类和它的任何祖先都是一样的。你可以自由地尝试一些组合,包括Human.methods、Mammal.methods、Module.methods和其他所有先前定义的类或模块;你会发现这让你对Ruby对象模型有了更强的掌握。