Ruby 面向对象编程
最后修改于 2023 年 10 月 18 日
在本 Ruby 教程中,我们将讨论 Ruby 中的面向对象编程。
编程语言有过程式编程、函数式编程和面向对象编程范式。Ruby 是一种面向对象语言,具有一些函数式和过程式特性。
面向对象编程 (OOP) 是一种使用对象及其交互来设计应用程序和计算机程序的编程范例。
OOP 中的基本编程概念是
- 抽象
- 多态
- 封装
- 继承
抽象是通过对适合该问题的类进行建模来简化复杂的现实。多态性是以不同的方式对不同的数据输入使用运算符或函数的过程。封装隐藏了类的实现细节,使其不被其他对象看到。继承是使用已经定义的类形成新类的一种方式。
Ruby 对象
对象是 Ruby OOP 程序的基本构建块。一个对象是数据和方法的组合。在 OOP 程序中,我们创建对象。这些对象通过方法相互通信。每个对象都可以接收消息、发送消息和处理数据。
创建对象有两个步骤。首先,我们定义一个类。一个类是对象的模板。它是一个描述该类对象的共享状态和行为的蓝图。一个类可以用来创建许多对象。在运行时从类创建的对象称为该特定类的实例。
#!/usr/bin/ruby class Being end b = Being.new puts b
在我们的第一个例子中,我们创建一个简单的对象。
class Being end
这是一个简单的类定义。模板的主体是空的。它没有任何数据或方法。
b = Being.new
我们创建 Being 类的新实例。为此,我们使用 new
方法。变量 b 存储新创建的对象。
puts b
我们将对象打印到控制台,以获取对象的某些基本描述。当我们打印一个对象时,实际上调用了它的 to_s
方法。但我们还没有定义任何方法。这是因为每个创建的对象都继承自基类 Object
。它有一些基本的功能,这些功能在所有创建的对象之间共享。其中之一是 to_s
方法。
$ ./simple.rb #<Being:0x9f3c290>
我们获取对象类名。
Ruby 构造函数
构造函数是一种特殊的方法。它在创建对象时自动调用。构造函数不返回值。构造函数的目的是初始化对象的状态。Ruby 中的构造函数称为 initialize
。构造函数不返回任何值。
父对象的构造函数使用 super
方法调用。它们按照继承顺序调用。
#!/usr/bin/ruby class Being def initialize puts "Being is created" end end Being.new
我们有一个 Being 类。
class Being def initialize puts "Being is created" end end
Being 类有一个名为 initialize
的构造函数方法。它将消息打印到控制台。Ruby 方法的定义放在 def
和 end
关键字之间。
Being.new
创建 Being
类的实例。在创建对象的那一刻,构造函数方法被调用。
$ ./constructor.rb Being is created
对象的属性是捆绑在该对象内部的数据项。这些项也被称为实例变量或成员字段。实例变量是在类中定义的变量,该类中的每个对象都有一个单独的副本。
在下一个例子中,我们初始化该类的数据成员。初始化变量是构造函数的典型工作。
#!/usr/bin/ruby class Person def initialize name @name = name end def get_name @name end end p1 = Person.new "Jane" p2 = Person.new "Beky" puts p1.get_name puts p2.get_name
在上面的 Ruby 代码中,我们有一个 Person
类,其中包含一个成员字段。
class Person def initialize name @name = name end ...
在 Person 类的构造函数中,我们将成员字段设置为一个值名称。name 参数在创建时传递给构造函数。构造函数是一个名为 initialize
的方法,该方法在创建实例对象时被调用。@name
是一个实例变量。实例变量在 Ruby 中以 @
字符开头。
def get_name @name end
get_name
方法返回成员字段。在 Ruby 中,成员字段只能通过方法访问。
p1 = Person.new "Jane" p2 = Person.new "Beky"
我们创建 Person
类的两个对象。一个字符串参数传递给每个对象构造函数。名称存储在每个对象独有的实例变量中。
puts p1.get_name puts p2.get_name
我们通过在每个对象上调用 get_name
来打印成员字段。
$ ./person.rb Jane Beky
我们看到程序的输出。Person
类的每个实例都有其自己的 name 成员字段。
我们可以在不调用构造函数的情况下创建对象。Ruby 有一个用于此目的的特殊 allocate
方法。allocate
方法为类的新对象分配空间,并且不调用新实例上的 initialize。
#!/usr/bin/ruby class Being def initialize puts "Being created" end end b1 = Being.new b2 = Being.allocate puts b2
在这个例子中,我们创建了两个对象。第一个对象使用 new
方法,第二个对象使用 allocate
方法。
b1 = Being.new
在这里,我们使用 new
关键字创建对象的实例。构造函数方法 initialize
被调用,消息被打印到控制台。
b2 = Being.allocate puts b2
在 allocate
方法的情况下,构造函数不会被调用。我们使用 puts
关键字调用 to_s
方法,以表明对象已创建。
$ ./allocate.rb Being created #<Being:0x8ea0044>
在这里,我们看到了程序的输出。
Ruby 构造函数重载
构造函数重载是指在一个类中拥有多种类型的构造函数的能力。通过这种方式,我们可以使用不同数量或不同类型的参数来创建对象。
Ruby 没有构造函数重载,这是我们从某些编程语言中知道的。这种行为可以在某种程度上通过 Ruby 中的默认参数值来模拟。
#!/usr/bin/ruby class Person def initialize name="unknown", age=0 @name = name @age = age end def to_s "Name: #{@name}, Age: #{@age}" end end p1 = Person.new p2 = Person.new "unknown", 17 p3 = Person.new "Becky", 19 p4 = Person.new "Robert" puts p1, p2, p3, p4
此示例显示了我们如何在具有两个成员字段的 Person
类上模拟构造函数重载。当未指定 name 参数时,将使用字符串“unknown”。对于未指定的 age,我们使用 0。
def initialize name="unknown", age=0 @name = name @age = age end
构造函数接受两个参数。它们有一个默认值。如果我们没有在创建对象时指定自己的值,则使用默认值。请注意,必须保留参数的顺序。首先是名称,然后是年龄。
p1 = Person.new p2 = Person.new "unknown", 17 p3 = Person.new "Becky", 19 p4 = Person.new "Robert" puts p1, p2, p3, p4
我们创建了四个对象。构造函数采用不同数量的参数。
$ ./consover.rb Name: unknown, Age: 0 Name: unknown, Age: 17 Name: Becky, Age: 19 Name: Robert, Age: 0
Ruby 方法
方法是在类的正文中定义的函数。它们用于对我们对象的属性执行操作。方法对于 OOP 范例的封装概念至关重要。例如,我们可能在 AccessDatabase
类中有一个连接方法。我们不需要知道该方法是如何连接到数据库的。我们只需要知道它用于连接到数据库。这对于划分编程中的职责至关重要,尤其是在大型应用程序中。
在 Ruby 中,数据只能通过方法访问。
#!/usr/bin/ruby class Person def initialize name @name = name end def get_name @name end end per = Person.new "Jane" puts per.get_name puts per.send :get_name
此示例显示了调用方法的两种基本方法。
puts per.get_name
常见的方法是在对象上使用点运算符,后跟方法名称。
puts per.send :get_name
另一种方法是使用内置的 send
方法。它将要调用的方法的符号作为参数。
方法通常对对象的数据执行某些操作。
#!/usr/bin/ruby class Circle @@PI = 3.141592 def initialize @radius = 0 end def set_radius radius @radius = radius end def area @radius * @radius * @@PI end end c = Circle.new c.set_radius 5 puts c.area
在代码示例中,我们有一个 Circle 类。我们定义了两个方法。
@@PI = 3.141592
我们在 Circle
类中定义了一个 @@PI
变量。这是一个类变量。类变量在 Ruby 中以 @@
符号开头。类变量属于一个类,而不是一个对象。每个对象都可以访问其类变量。我们使用 @@PI
计算圆的面积。
def initialize @radius = 0 end
我们有一个成员字段。它是圆的半径。如果我们要从外部修改此变量,则必须使用公开可用的 set_radius
方法。数据受到保护。
def set_radius radius @radius = radius end
这是 set_radius
方法。它为 @radius
实例变量提供一个新值。
def area @radius * @radius * @@PI end
area 方法返回圆的面积。这是方法的一个典型任务。它使用数据并为我们生成一些值。
c = Circle.new c.set_radius 5 puts c.area
我们创建一个 Circle 类的实例,并通过在圆的对象上调用 set_radius
方法来设置其半径。我们使用点运算符来调用该方法。
$ ./circle.rb 78.5398
运行该示例,我们得到此输出。
Ruby 访问修饰符
访问修饰符设置方法和成员字段的可见性。Ruby 有三个访问修饰符:public
、protected
和 private
。在 Ruby 中,所有数据成员都是私有的。访问修饰符只能用于方法。除非另有说明,否则 Ruby 方法是公开的。
public
方法可以从类定义内部以及从类的外部访问。protected
和 private
方法之间的区别很微妙。两者都不能在类定义之外访问。它们只能在类本身及其继承或父类中访问。
请注意,与其他面向对象的编程语言不同,继承在 Ruby 访问修饰符中不起作用。只有两件事很重要。首先,我们是在类定义内部还是外部调用该方法。其次,我们是否使用或未使用指向当前接收者的 self
关键字。
访问修饰符保护数据免受意外修改。它们使程序更强大。某些方法的实现可能会更改。这些方法是成为私有方法的良好候选者。应该仅在绝对必要时更改向用户公开的接口。多年来,用户习惯于使用特定的方法,并且通常不赞成破坏向后兼容性。
#!/usr/bin/ruby class Some def method1 puts "public method1 called" end public def method2 puts "public method2 called" end def method3 puts "public method3 called" method1 self.method1 end end s = Some.new s.method1 s.method2 s.method3
该示例解释了公共 Ruby 方法的用法。
def method1 puts "public method1 called" end
即使我们没有指定 public
访问修饰符,方法 1 也是公共的。这是因为方法默认是公共的,除非另有说明。
public def method2 puts "public method2 called" end ...
public
关键字之后的方法是公共的。
def method3 puts "public method3 called" method1 self.method1 end
从公共 method3
内部,我们调用其他公共方法,无论是否使用 self
关键字。
s = Some.new s.method1 s.method2 s.method3
公共方法是唯一可以在类定义外部调用的方法,如此处所示。
$ ./public_methods.rb public method1 called public method2 called public method3 called public method1 called public method1 called
运行该示例,我们得到此输出。
下一个示例查看私有方法。
#!/usr/bin/ruby class Some def initialize method1 # self.method1 end private def method1 puts "private method1 called" end end s = Some.new # s.method1
私有方法是 Ruby 中最严格的方法。它们只能在类定义内部调用,并且不能使用 self
关键字。
def initialize method1 # self.method1 end
在方法构造函数中,我们调用私有的 method1
。使用 self 调用该方法已被注释掉。私有方法不能使用接收者指定。
private def method1 puts "private method1 called" end
private
关键字之后的方法在 Ruby 中是私有的。
s = Some.new # s.method1
我们创建 Some 类的实例。在类定义之外调用该方法是被禁止的。如果取消注释该行,Ruby 解释器会给出一个错误。
$ ./private_methods.rb private method called
最后,我们使用受保护的方法。Ruby 中受保护方法和私有方法之间的区别很微妙。受保护的方法就像私有方法。只有一个小的区别。它们可以使用指定的 self
关键字调用。
#!/usr/bin/ruby class Some def initialize method1 self.method1 end protected def method1 puts "protected method1 called" end end s = Some.new # s.method1
上面的示例显示了受保护方法的使用。
def initialize method1 self.method1 end
受保护的方法可以带或不带 self
关键字调用。
protected def method1 puts "protected method1 called" end
受保护的方法前面是 protected
关键字。
s = Some.new # s.method1
受保护的方法不能在类定义外部调用。取消注释该行将导致错误。
Ruby 继承
继承是使用已经定义的类形成新类的一种方式。新形成的类称为派生类,我们从中派生的类称为基类。继承的重要好处是代码重用和降低程序的复杂性。派生类(后代)重写或扩展基类(祖先)的功能。
#!/usr/bin/ruby class Being def initialize puts "Being class created" end end class Human < Being def initialize super puts "Human class created" end end Being.new Human.new
在这个程序中,我们有两个类:一个基类 Being
类和一个派生类 Human
类。派生类继承自基类。
class Human < Being
在 Ruby 中,我们使用 <
运算符创建继承关系。Human
类继承自 Being
类。
def initialize super puts "Human class created" end
super
方法调用父类的构造函数。
Being.new Human.new
我们实例化 Being
和 Human
类。
$ ./inheritance.rb Being class created Being class created Human class created
首先创建 Being
类。派生的 Human
类也调用其父类的构造函数。
一个对象可能涉及复杂的联系。一个对象可以有多个祖先。Ruby 有一个 ancestors
方法,它给出了特定类的祖先列表。
每个 Ruby 对象都是 Object
和 BasicObject
类以及 Kernel
模块的后代。它们内置于 Ruby 语言的核心中。
#!/usr/bin/ruby class Being end class Living < Being end class Mammal < Living end class Human < Mammal end p Human.ancestors
在这个例子中,我们有四个类:一个 Human
是一个 Mammal
、一个 Living
和一个 Being
。
p Human.ancestors
我们打印 Human 类的祖先。
$ ./ancestors.rb [Human, Mammal, Living, Being, Object, Kernel, BasicObject]
Human 类有三个自定义祖先和三个内置祖先。
接下来是一个更复杂的示例。
#!/usr/bin/ruby class Being @@count = 0 def initialize @@count += 1 puts "Being class created" end def show_count "There are #{@@count} beings" end end class Human < Being def initialize super puts "Human is created" end end class Animal < Being def initialize super puts "Animal is created" end end class Dog < Animal def initialize super puts "Dog is created" end end Human.new d = Dog.new puts d.show_count
我们有四个类。继承层次结构更复杂。Human
和 Animal
类继承自 Being
类。而 Dog
类直接继承自 Animal
类,并进一步继承自 Being
类。我们还使用一个类变量来计算创建的生物数量。
@@count = 0
我们定义一个类变量。类变量以 @@
符号开头,它属于类,而不是类的实例。我们使用它来计算创建的生物数量。
def initialize @@count += 1 puts "Being class created" end
每次实例化 Being
类时,我们都会将 @@count
变量增加 1。通过这种方式,我们跟踪创建的实例的数量。
class Animal < Being ... class Dog < Animal ...
Animal
继承自 Being
,而 Dog
继承自 Animal
。此外,Dog
也继承自 Being
。
Human.new d = Dog.new puts d.show_count
我们从 Human
类和 Dog
类创建实例。我们对 Dog
对象调用 show_count
方法。Dog
类没有这样的方法;然后调用祖父(Being)的方法。
$ ./inheritance2.rb Being class created Human is created Being class created Animal is created Dog is created There are 2 beings
Human 对象调用两个构造函数。Dog
对象调用三个构造函数。实例化了两个生物。
继承在方法和数据成员的可见性方面不起作用。这是与许多常见的面向对象编程语言的一个显著区别。
在 C# 或 Java 中,public 和 protected 的数据成员和方法会被继承,而 private 的数据成员和方法则不会。与此相反,在 Ruby 中,private 的数据成员和方法也会被继承。在 Ruby 中,数据成员和方法的可见性不受继承的影响。
#!/usr/bin/ruby class Base def initialize @name = "Base" end private def private_method puts "private method called" end protected def protected_method puts "protected_method called" end public def get_name return @name end end class Derived < Base def public_method private_method protected_method end end d = Derived.new d.public_method puts d.get_name
在示例中,我们有两个类。Derived
类继承自 Base
类。它继承了所有三个方法和一个数据字段。
def public_method private_method protected_method end
在 Derived
类的 public_method 中,我们调用了一个 private 方法和一个 protected 方法。它们是在父类中定义的。
d = Derived.new d.public_method puts d.get_name
我们创建了 Derived
类的一个实例。我们调用 public_method
,以及 get_name
,它返回 private 的 @name
变量。请记住,在 Ruby 中,所有实例变量都是 private 的。get_name 方法返回该变量,无论 @name 是否是 private 且在父类中定义。
$ ./inheritance3.rb private method called protected_method called Base
该示例的输出结果证实,在 Ruby 语言中,public、protected、private 方法和 private 成员字段都会被子对象从其父类继承。
Ruby 的 super 方法
super
方法调用父类中同名的方法。如果该方法没有参数,它会自动传递其所有参数。如果我们只写 super
,则不会向父类方法传递任何参数。
#!/usr/bin/ruby class Base def show x=0, y=0 p "Base class, x: #{x}, y: #{y}" end end class Derived < Base def show x, y super super x super x, y super() end end d = Derived.new d.show 3, 3
在示例中,我们在一个层次结构中定义了两个类。它们都有一个 show 方法。Derived
类中的 show 方法使用 super
方法调用了 Base
类中的 show 方法。
def show x, y super super x super x, y super() end
super
方法在没有任何参数的情况下,调用了父类的 show 方法,并使用了传递给 Derived
类的 show 方法的参数:这里,x=3 和 y=3。super
方法不会向父类的 show 方法传递任何参数。
$ ./super_method.rb "Base class, x: 3, y: 3" "Base class, x: 3, y: 0" "Base class, x: 3, y: 3" "Base class, x: 0, y: 0"
这是 Ruby 面向对象编程描述的第一部分。