ZetCode

Ruby 面向对象编程

最后修改于 2023 年 10 月 18 日

在本 Ruby 教程中,我们将讨论 Ruby 中的面向对象编程。

编程语言有过程式编程、函数式编程和面向对象编程范式。Ruby 是一种面向对象语言,具有一些函数式和过程式特性。

面向对象编程 (OOP) 是一种使用对象及其交互来设计应用程序和计算机程序的编程范例。

OOP 中的基本编程概念是

抽象是通过对适合该问题的类进行建模来简化复杂的现实。多态性是以不同的方式对不同的数据输入使用运算符或函数的过程。封装隐藏了类的实现细节,使其不被其他对象看到。继承是使用已经定义的类形成新类的一种方式。

Ruby 对象

对象是 Ruby OOP 程序的基本构建块。一个对象是数据和方法的组合。在 OOP 程序中,我们创建对象。这些对象通过方法相互通信。每个对象都可以接收消息、发送消息和处理数据。

创建对象有两个步骤。首先,我们定义一个类。一个是对象的模板。它是一个描述该类对象的共享状态和行为的蓝图。一个类可以用来创建许多对象。在运行时从类创建的对象称为该特定类的实例

simple.rb
#!/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 方法调用。它们按照继承顺序调用。

constructor.rb
#!/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 方法的定义放在 defend 关键字之间。

Being.new

创建 Being 类的实例。在创建对象的那一刻,构造函数方法被调用。

$ ./constructor.rb
Being is created

对象的属性是捆绑在该对象内部的数据项。这些项也被称为实例变量成员字段。实例变量是在类中定义的变量,该类中的每个对象都有一个单独的副本。

在下一个例子中,我们初始化该类的数据成员。初始化变量是构造函数的典型工作。

person.rb
#!/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。

allocate.rb
#!/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 中的默认参数值来模拟。

consover.rb
#!/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 中,数据只能通过方法访问。

call_method.rb
#!/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 方法。它将要调用的方法的符号作为参数。

方法通常对对象的数据执行某些操作。

circle.rb
#!/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 有三个访问修饰符:publicprotectedprivate。在 Ruby 中,所有数据成员都是私有的。访问修饰符只能用于方法。除非另有说明,否则 Ruby 方法是公开的。

public 方法可以从类定义内部以及从类的外部访问。protectedprivate 方法之间的区别很微妙。两者都不能在类定义之外访问。它们只能在类本身及其继承或父类中访问。

请注意,与其他面向对象的编程语言不同,继承在 Ruby 访问修饰符中不起作用。只有两件事很重要。首先,我们是在类定义内部还是外部调用该方法。其次,我们是否使用或未使用指向当前接收者的 self 关键字。

访问修饰符保护数据免受意外修改。它们使程序更强大。某些方法的实现可能会更改。这些方法是成为私有方法的良好候选者。应该仅在绝对必要时更改向用户公开的接口。多年来,用户习惯于使用特定的方法,并且通常不赞成破坏向后兼容性。

public_methods.rb
#!/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

运行该示例,我们得到此输出。

下一个示例查看私有方法。

private_methods.rb
#!/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 关键字调用。

protected_methods.rb
#!/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 继承

继承是使用已经定义的类形成新类的一种方式。新形成的类称为派生类,我们从中派生的类称为类。继承的重要好处是代码重用和降低程序的复杂性。派生类(后代)重写或扩展基类(祖先)的功能。

inheritance.rb
#!/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

我们实例化 BeingHuman 类。

$ ./inheritance.rb
Being class created
Being class created
Human class created

首先创建 Being 类。派生的 Human 类也调用其父类的构造函数。

一个对象可能涉及复杂的联系。一个对象可以有多个祖先。Ruby 有一个 ancestors 方法,它给出了特定类的祖先列表。

每个 Ruby 对象都是 ObjectBasicObject 类以及 Kernel 模块的后代。它们内置于 Ruby 语言的核心中。

ancestors.rb
#!/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 类有三个自定义祖先和三个内置祖先。

接下来是一个更复杂的示例。

inheritance2.rb
#!/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

我们有四个类。继承层次结构更复杂。HumanAnimal 类继承自 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 中,数据成员和方法的可见性不受继承的影响。

inheritance3.rb
#!/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,则不会向父类方法传递任何参数。

super_method.rb
#!/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 面向对象编程描述的第一部分。