原文: https://zetcode.com/lang/rubytutorial/oop/

在 Ruby 教程的这一部分中,我们将讨论 Ruby 中的面向对象编程。

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

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

OOP 中的基本编程概念是:

  • 抽象
  • 多态
  • 封装
  • 继承

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

Ruby 对象

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

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

  1. #!/usr/bin/ruby
  2. class Being
  3. end
  4. b = Being.new
  5. puts b

在第一个示例中,我们创建一个简单的对象。

  1. class Being
  2. end

这是一个简单的类定义。 模板的主体为空。 它没有任何数据或方法。

  1. b = Being.new

我们创建Being类的新实例。 为此,我们有new方法。 b变量存储新创建的对象。

  1. puts b

我们将对象打印到控制台以获取该对象的一些基本描述。 实际上,当我们打印对象时,我们将其称为to_s方法。 但是我们还没有定义任何方法。 这是因为创建的每个对象都继承自基本Object。 它具有一些基本功能,这些功能在所有创建的对象之间共享。 其中之一是to_s方法。

  1. $ ./simple.rb
  2. #<Being:0x9f3c290>

我们得到对象类名。

Ruby 构造器

构造器是一种特殊的方法。 创建对象时会自动调用它。 构造器不返回值。 构造器的目的是初始化对象的状态。 Ruby 中的构造器称为initialize。 构造器不返回任何值。

使用super方法调用父对象的构造器。 它们按继承顺序被调用。

  1. #!/usr/bin/ruby
  2. class Being
  3. def initialize
  4. puts "Being is created"
  5. end
  6. end
  7. Being.new

我们有一个存在类。

  1. class Being
  2. def initialize
  3. puts "Being is created"
  4. end
  5. end

Betweening类具有一个名为initialize的构造方法。 它将消息打印到控制台。 Ruby 方法的定义位于defend关键字之间。

  1. Being.new

创建Being类的实例。 创建对象时,将调用构造方法。

  1. $ ./constructor.rb
  2. Being is created

这是程序的输出。

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

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

  1. #!/usr/bin/ruby
  2. class Person
  3. def initialize name
  4. @name = name
  5. end
  6. def get_name
  7. @name
  8. end
  9. end
  10. p1 = Person.new "Jane"
  11. p2 = Person.new "Beky"
  12. puts p1.get_name
  13. puts p2.get_name

在上面的 Ruby 代码中,我们有一个带有一个成员字段的Person类。

  1. class Person
  2. def initialize name
  3. @name = name
  4. end

Person类的构造器中,我们将一个member字段设置为一个值名称。 名称参数在创建时传递给构造器。 构造器是称为initialize的方法,该方法在创建实例对象时被调用。 @name是一个实例变量。 实例变量在 Ruby 中以@字符开头。

  1. def get_name
  2. @name
  3. end

get_name方法返回成员字段。 在 Ruby 中,成员字段只能通过方法访问。

  1. p1 = Person.new "Jane"
  2. p2 = Person.new "Beky"

我们创建Person类的两个对象。 字符串参数传递给每个对象构造器。 名称存储在每个对象唯一的实例变量中。

  1. puts p1.get_name
  2. puts p2.get_name

我们通过在每个对象上调用get_name来打印成员字段。

  1. $ ./person.rb
  2. Jane
  3. Beky

我们看到了程序的输出。 Person类的每个实例都有其自己的名称成员字段。

我们可以创建对象而无需调用构造器。 Ruby 为此有一种特殊的allocate方法。 allocate方法为类的新对象分配空间,并且不会在新实例上调用initialize

  1. #!/usr/bin/ruby
  2. class Being
  3. def initialize
  4. puts "Being created"
  5. end
  6. end
  7. b1 = Being.new
  8. b2 = Being.allocate
  9. puts b2

在示例中,我们创建两个对象。 第一个对象使用new方法,第二个对象使用allocate方法。

  1. b1 = Being.new

在这里,我们使用new关键字创建对象的实例。 调用构造器方法initialize,并将消息打印到控制台。

  1. b2 = Being.allocate
  2. puts b2

allocate方法的情况下,不调用构造器。 我们使用puts关键字调用to_s方法以显示该对象已创建。

  1. $ ./allocate.rb
  2. Being created
  3. #<Being:0x8ea0044>

在这里,我们看到了程序的输出。

Ruby 构造器重载

构造器重载是在一个类中具有多种类型的构造器的能力。 这样,我们可以创建具有不同数量或不同类型参数的对象。

Ruby 没有我们从某些编程语言中知道的构造器重载。 可以使用 Ruby 中的默认参数值在某种程度上模拟此行为。

  1. #!/usr/bin/ruby
  2. class Person
  3. def initialize name="unknown", age=0
  4. @name = name
  5. @age = age
  6. end
  7. def to_s
  8. "Name: #{@name}, Age: #{@age}"
  9. end
  10. end
  11. p1 = Person.new
  12. p2 = Person.new "unknown", 17
  13. p3 = Person.new "Becky", 19
  14. p4 = Person.new "Robert"
  15. p p1, p2, p3, p4

本示例说明了如何在具有两个成员字段的Person类上模拟构造器重载。 如果未指定name参数,则使用字符串"unknown"。 对于未指定的年龄,我们为 0。

  1. def initialize name="unknown", age=0
  2. @name = name
  3. @age = age
  4. end

构造器有两个参数。 它们具有默认值。 如果在创建对象时未指定自己的值,则使用默认值。 请注意,必须保留参数的顺序。 首先是名字,然后是年龄。

  1. p1 = Person.new
  2. p2 = Person.new "unknown", 17
  3. p3 = Person.new "Becky", 19
  4. p4 = Person.new "Robert"
  5. p p1, p2, p3, p4

我们创建四个对象。 构造器采用不同数量的参数。

  1. $ ./consover.rb
  2. Name: unknown, Age: 0
  3. Name: unknown, Age: 17
  4. Name: Becky, Age: 19
  5. Name: Robert, Age: 0

这是示例的输出。

Ruby 方法

方法是在类主体内定义的函数。 它们用于通过对象的属性执行操作。 在 OOP 范式的封装概念中,方法至关重要。 例如,我们的AccessDatabase类中可能有一个connect方法。 我们无需告知该方法如何准确地连接到数据库。 我们只需要知道它用于连接数据库。 这对于划分编程中的职责至关重要,尤其是在大型应用中。

在 Ruby 中,只能通过方法访问数据。

  1. #!/usr/bin/ruby
  2. class Person
  3. def initialize name
  4. @name = name
  5. end
  6. def get_name
  7. @name
  8. end
  9. end
  10. per = Person.new "Jane"
  11. puts per.get_name
  12. puts per.send :get_name

该示例显示了两种调用方法的基本方法。

  1. puts per.get_name

常见的方法是在对象上使用点运算符,后跟方法名称。

  1. puts per.send :get_name

替代方法是使用内置的send方法。 它以方法的符号作为参数。

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

  1. #!/usr/bin/ruby
  2. class Circle
  3. @@PI = 3.141592
  4. def initialize
  5. @radius = 0
  6. end
  7. def set_radius radius
  8. @radius = radius
  9. end
  10. def area
  11. @radius * @radius * @@PI
  12. end
  13. end
  14. c = Circle.new
  15. c.set_radius 5
  16. puts c.area

在代码示例中,我们有一个Circle类。 我们定义了两种方法。

  1. @@PI = 3.141592

我们在Circle类中定义了@@PI变量。 它是一个类变量。 类变量以 Ruby 中的@@信号开头。 类变量属于类,而不属于对象。 每个对象都可以访问其类变量。 我们使用@@PI计算圆的面积。

  1. def initialize
  2. @radius = 0
  3. end

我们只有一个成员字段。 它是圆的半径。 如果要从外部修改此变量,则必须使用公共可用的set_radius方法。 数据受到保护。

  1. def set_radius radius
  2. @radius = radius
  3. end

这是set_radius方法。 它为@radius实例变量提供了一个新值。

  1. def area
  2. @radius * @radius * @@PI
  3. end

area方法返回圆的面积。 这是方法的典型任务。 它可以处理数据并为我们带来一些价值。

  1. c = Circle.new
  2. c.set_radius 5
  3. puts c.area

我们创建Circle类的实例,并通过在圆对象上调用set_radius方法来设置其半径。 我们使用点运算符来调用该方法。

  1. $ ./circle.rb
  2. 78.5398

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

Ruby 访问修饰符

访问修饰符设置方法和成员字段的可见性。 Ruby 具有三个访问修饰符:publicprotectedprivate。 在 Ruby 中,所有数据成员都是私有的。 访问修饰符只能在方法上使用。 除非我们另有说明,否则 Ruby 方法是公共的。

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

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

访问修饰符可防止意外修改数据。 它们使程序更强大。 某些方法的实现可能会发生变化。 这些方法是很好的私有方法。 公开给用户的接口仅应在确实必要时更改。 多年来,用户习惯于使用特定的方法,并且通常不赞成向后兼容。

  1. #!/usr/bin/ruby
  2. class Some
  3. def method1
  4. puts "public method1 called"
  5. end
  6. public
  7. def method2
  8. puts "public method2 called"
  9. end
  10. def method3
  11. puts "public method3 called"
  12. method1
  13. self.method1
  14. end
  15. end
  16. s = Some.new
  17. s.method1
  18. s.method2
  19. s.method3

该示例说明了公共 Ruby 方法的用法。

  1. def method1
  2. puts "public method1 called"
  3. end

即使我们未指定public访问修饰符,method1也是公共的。 这是因为如果没有另外指定,默认情况下方法是公共的。

  1. public
  2. def method2
  3. puts "public method2 called"
  4. end
  5. ...

public关键字后面的方法是公共的。

  1. def method3
  2. puts "public method3 called"
  3. method1
  4. self.method1
  5. end

从公共method3内部,我们调用带有和不带有self关键字的其他公共方法。

  1. s = Some.new
  2. s.method1
  3. s.method2
  4. s.method3

公共方法是唯一可以在类定义之外调用的方法,如下所示。

  1. $ ./public_methods.rb
  2. public method1 called
  3. public method2 called
  4. public method3 called
  5. public method1 called
  6. public method1 called

运行示例,我们将获得以下输出。

下一个示例着眼于私有方法。

  1. #!/usr/bin/ruby
  2. class Some
  3. def initialize
  4. method1
  5. # self.method1
  6. end
  7. private
  8. def method1
  9. puts "private method1 called"
  10. end
  11. end
  12. s = Some.new
  13. # s.method1

私有方法是 Ruby 中最严格的方法。 只能在类定义内调用它们,而不能使用self关键字。

  1. def initialize
  2. method1
  3. # self.method1
  4. end

在方法的构造器中,我们称为私有method1。 带有self的方法被注释。 接收者无法指定私有方法。

  1. private
  2. def method1
  3. puts "private method1 called"
  4. end

关键字private之后的方法在 Ruby 中是私有的。

  1. s = Some.new
  2. # s.method1

我们创建Some类的实例。 禁止在类定义之外调用该方法。 如果我们取消注释该行,则 Ruby 解释器将给出错误。

  1. $ ./private_methods.rb
  2. private method called

示例代码的输出。

最后,我们将使用受保护的方法。 Ruby 中的保护方法和私有方法之间的区别非常微妙。 受保护的方法就像私有的。 只有一个小差异。 可以使用指定的self关键字来调用它们。

  1. #!/usr/bin/ruby
  2. class Some
  3. def initialize
  4. method1
  5. self.method1
  6. end
  7. protected
  8. def method1
  9. puts "protected method1 called"
  10. end
  11. end
  12. s = Some.new
  13. # s.method1

上面的示例显示了使用中的受保护方法。

  1. def initialize
  2. method1
  3. self.method1
  4. end

可以使用self关键字来调用受保护的方法。

  1. protected
  2. def method1
  3. puts "protected method1 called"
  4. end

受保护的方法前面带有protected关键字。

  1. s = Some.new
  2. # s.method1

不能在类定义之外调用受保护的方法。 取消注释该行将导致错误。

Ruby 继承

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

  1. #!/usr/bin/ruby
  2. class Being
  3. def initialize
  4. puts "Being class created"
  5. end
  6. end
  7. class Human < Being
  8. def initialize
  9. super
  10. puts "Human class created"
  11. end
  12. end
  13. Being.new
  14. Human.new

在此程序中,我们有两个类:基础Being类和派生的Human类。 派生类继承自基类。

  1. class Human < Being

在 Ruby 中,我们使用<运算符创建继承关系。 Human类继承自Being类。

  1. def initialize
  2. super
  3. puts "Human class created"
  4. end

super方法调用父类的构造器。

  1. Being.new
  2. Human.new

我们实例化BeingHuman类。

  1. $ ./inheritance.rb
  2. Being class created
  3. Being class created
  4. Human class created

首先创建Being类。 派生的Human类还调用其父级的构造器。

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

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

  1. #!/usr/bin/ruby
  2. class Being
  3. end
  4. class Living < Being
  5. end
  6. class Mammal < Living
  7. end
  8. class Human < Mammal
  9. end
  10. p Human.ancestors

在此示例中,我们有四个类:HumanMammalLivingBeing

  1. p Human.ancestors

我们打印人类类的祖先。

  1. $ ./ancestors.rb
  2. [Human, Mammal, Living, Being, Object, Kernel, BasicObject]

人类类具有三个习俗和三个内置祖先。

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

  1. #!/usr/bin/ruby
  2. class Being
  3. @@count = 0
  4. def initialize
  5. @@count += 1
  6. puts "Being class created"
  7. end
  8. def show_count
  9. "There are #{@@count} beings"
  10. end
  11. end
  12. class Human < Being
  13. def initialize
  14. super
  15. puts "Human is created"
  16. end
  17. end
  18. class Animal < Being
  19. def initialize
  20. super
  21. puts "Animal is created"
  22. end
  23. end
  24. class Dog < Animal
  25. def initialize
  26. super
  27. puts "Dog is created"
  28. end
  29. end
  30. Human.new
  31. d = Dog.new
  32. puts d.show_count

我们有四个类。 继承层次更加复杂。 HumanAnimal类继承自Being类。 Dog类直接继承自Animal类,并且进一步继承自Being类。 我们还使用一个类变量来计算创建的生物的数量。

  1. @@count = 0

我们定义一个类变量。 类变量以@@信号开头,并且它属于该类,而不是该类的实例。 我们用它来计算创造的生物数量。

  1. def initialize
  2. @@count += 1
  3. puts "Being class created"
  4. end

每次实例化Being类时,我们都会将@@count变量加 1。 这样,我们就可以跟踪创建的实例数。

  1. class Animal < Being
  2. ...
  3. class Dog < Animal
  4. ...

Animal继承自BeingDog继承自Animal。 另外,Dog也继承自Being

  1. Human.new
  2. d = Dog.new
  3. puts d.show_count

我们从HumanDog类创建实例。 我们在Dog对象上调用show_count方法。 Dog类没有这种方法。 然后调用祖父级Being的方法。

  1. $ ./inheritance2.rb
  2. Being class created
  3. Human is created
  4. Being class created
  5. Animal is created
  6. Dog is created
  7. There are 2 beings

Human对象调用两个构造器。 Dog对象调用三个构造器。 有两个实例化的存在。

继承在方法和数据成员的可见性中不起作用。 与许多常见的面向对象的编程语言相比,这是一个明显的区别。

在 C# 或 Java 中,公共和受保护的数据成员和方法是继承的。 私有数据成员和方法不是。 与此相反,私有数据成员和方法也在 Ruby 中继承。 数据成员和方法的可见性不受 Ruby 中继承的影响。

  1. #!/usr/bin/ruby
  2. class Base
  3. def initialize
  4. @name = "Base"
  5. end
  6. private
  7. def private_method
  8. puts "private method called"
  9. end
  10. protected
  11. def protected_method
  12. puts "protected_method called"
  13. end
  14. public
  15. def get_name
  16. return @name
  17. end
  18. end
  19. class Derived < Base
  20. def public_method
  21. private_method
  22. protected_method
  23. end
  24. end
  25. d = Derived.new
  26. d.public_method
  27. puts d.get_name

在示例中,我们有两个类。 Derived类继承自Base类。 它继承了所有三种方法和一个数据字段。

  1. def public_method
  2. private_method
  3. protected_method
  4. end

Derived类的public_method中,我们调用一种私有方法和一种受保护方法。 它们在父类中定义。

  1. d = Derived.new
  2. d.public_method
  3. puts d.get_name

我们创建Derived类的实例。 我们称为public_method,也称为get_name,它返回私有@name变量。 请记住,所有实例变量在 Ruby 中都是私有的。 无论@name是私有的还是在父类中定义的,get_name方法都会返回该变量。

  1. $ ./inheritance3.rb
  2. private method called
  3. protected_method called
  4. Base

该示例的输出确认,在 Ruby 语言中,子对象从其父级继承了publicprotectedprivate方法和private成员字段。

Ruby super方法

super方法在父类的类中调用相同名称的方法。 如果该方法没有参数,它将自动传递其所有参数。 如果我们写super(),则不会将任何参数传递给父方法。

  1. #!/usr/bin/ruby
  2. class Base
  3. def show x=0, y=0
  4. p "Base class, x: #{x}, y: #{y}"
  5. end
  6. end
  7. class Derived < Base
  8. def show x, y
  9. super
  10. super x
  11. super x, y
  12. super()
  13. end
  14. end
  15. d = Derived.new
  16. d.show 3, 3

在示例中,我们在层次结构中有两个类。 它们都有显示方法。 派生类中的show方法使用super方法调用基类中的show方法。

  1. def show x, y
  2. super
  3. super x
  4. super x, y
  5. super()
  6. end

不带任何参数的super方法使用传递给Derived类的show方法的参数调用父级的show方法:此处,x = 3y = 3super()方法不将任何参数传递给父级的show方法。

  1. $ ./super.rb
  2. "Base class, x: 3, y: 3"
  3. "Base class, x: 3, y: 0"
  4. "Base class, x: 3, y: 3"
  5. "Base class, x: 0, y: 0"

这是示例的输出。

这是 Ruby 中 OOP 描述的第一部分。