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

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

我们从属性访问器开始。 我们将介绍类常量,类方法和运算符重载。 我们将定义多态,并展示如何在 Ruby 中使用它。 我们还将提到模块和异常。

Ruby 属性访问器

所有 Ruby 变量都是私有的。 只能通过方法访问它们。 这些方法通常称为设置器和获取器。 创建一个设置器和一个获取器方法是非常常见的任务。 因此,Ruby 具有方便的方法来创建两种类型的方法。 它们是attr_readerattr_writerattr_accessor

attr_reader创建获取器方法。 attr_writer方法为该设置器创建设置器方法和实例变量。 attr_accessor方法同时创建获取器方法,设置器方法及其实例变量。

  1. #!/usr/bin/ruby
  2. class Car
  3. attr_reader :name, :price
  4. attr_writer :name, :price
  5. def to_s
  6. "#{@name}: #{@price}"
  7. end
  8. end
  9. c1 = Car.new
  10. c2 = Car.new
  11. c1.name = "Porsche"
  12. c1.price = 23500
  13. c2.name = "Volkswagen"
  14. c2.price = 9500
  15. puts "The #{c1.name} costs #{c1.price}"
  16. p c1
  17. p c2

我们有汽车类。 在类的定义中,我们使用attr_readerattr_writerCar类创建两个获取器和设置器方法。

  1. attr_reader :name, :price

在这里,我们创建两个名为nameprice的实例方法。 注意,attr_reader将方法的符号作为参数。

  1. attr_writer :name, :price

attr_writer创建两个名为nameprice的设置方法,以及两个实例变量@name@price

  1. c1.name = "Porsche"
  2. c1.price = 23500

在这种情况下,调用了两个设置器方法以用一些数据填充实例变量。

  1. puts "The #{c1.name} costs #{c1.price}"

在这里,调用了两个获取器方法以从c1对象的实例变量获取数据。

  1. $ ./arw.rb
  2. The Porsche costs 23500
  3. Porsche: 23500
  4. Volkswagen: 9500

这是示例的输出。

如前所述,attr_accessor方法创建获取器,设置器方法及其实例变量。

  1. #!/usr/bin/ruby
  2. class Book
  3. attr_accessor :title, :pages
  4. end
  5. b1 = Book.new
  6. b1.title = "Hidden motives"
  7. b1.pages = 255
  8. p "The book #{b1.title} has #{b1.pages} pages"

我们有一个Book类,其中attr_accessor创建两对方法和两个实例变量。

  1. class Book
  2. attr_accessor :title, :pages
  3. end

设置标题和页面方法以及@title@pages实例变量的attr_accessor方法。

  1. b1 = Book.new
  2. b1.title = "Hidden motives"
  3. b1.pages = 255

创建一个Book类的对象。 两种设置方法填充对象的实例变量。

  1. p "The book #{b1.title} has #{b1.pages} pages"

在此代码行中,我们使用两种获取器方法来读取实例变量的值。

  1. $ ./accessor.rb
  2. "The book Hidden motives has 255 pages"

这是示例输出。

Ruby 类常量

Ruby 使您可以创建类常量。 这些常量不属于具体对象。 他们属于阶级。 按照约定,常量用大写字母表示。

  1. #!/usr/bin/ruby
  2. class MMath
  3. PI = 3.141592
  4. end
  5. puts MMath::PI

我们有一个带有PI常量的MMath类。

  1. PI = 3.141592

我们创建一个PI常量。 请记住,Ruby 中的常量不是强制性的。

  1. puts MMath::PI

我们使用::运算符访问PI常量。

  1. $ ./classconstant.rb
  2. 3.141592

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

Ruby to_s方法

每个对象都有一个to_s方法。 它返回对象的字符串表示形式。 请注意,当puts方法将对象作为参数时,将调用该对象的to_s

  1. #!/usr/bin/ruby
  2. class Being
  3. def to_s
  4. "This is Being class"
  5. end
  6. end
  7. b = Being.new
  8. puts b.to_s
  9. puts b

我们有一个Beinging类,其中我们重写了to_s方法的默认实现。

  1. def to_s
  2. "This is Being class"
  3. end

创建的每个类都从基Object继承。 to_s方法属于此类。 我们覆盖to_s方法并创建一个新的实现。 我们提供了一个易于理解的对象描述。

  1. b = Being.new
  2. puts b.to_s
  3. puts b

我们创建一个Beinging类,并调用to_s方法两次。 第一次是显式的,第二次是隐式的。

  1. $ ./tostring.rb
  2. This is Being class
  3. This is Being class

这是我们运行示例时得到的。

运算符重载

运算符重载是指不同的运算符根据其参数具有不同的实现的情况。

在 Ruby 中,运算符和方法之间只有微小的区别。

  1. #!/usr/bin/ruby
  2. class Circle
  3. attr_accessor :radius
  4. def initialize r
  5. @radius = r
  6. end
  7. def +(other)
  8. Circle.new @radius + other.radius
  9. end
  10. def to_s
  11. "Circle with radius: #{@radius}"
  12. end
  13. end
  14. c1 = Circle.new 5
  15. c2 = Circle.new 6
  16. c3 = c1 + c2
  17. p c3

在示例中,我们有一个Circle类。 我们在类中重载了+运算符。 我们使用它来添加两个圆形对象。

  1. def +(other)
  2. Circle.new @radius + other.radius
  3. end

我们定义一个带有+名称的方法。 该方法将两个圆形对象的半径相加。

  1. c1 = Circle.new 5
  2. c2 = Circle.new 6
  3. c3 = c1 + c2

我们创建两个圆形对象。 在第三行中,我们添加这两个对象以创建一个新对象。

  1. $ ./operatoroverloading.rb
  2. Circle with radius: 11

将这两个圆形对象相加会创建第三个半径为 11 的对象。

Ruby 类方法

Ruby 方法可以分为类方法和实例方法。 类方法在类上调用。 不能在类的实例上调用它们。

类方法不能访问实例变量。

  1. #!/usr/bin/ruby
  2. class Circle
  3. def initialize x
  4. @r = x
  5. end
  6. def self.info
  7. "This is a Circle class"
  8. end
  9. def area
  10. @r * @r * 3.141592
  11. end
  12. end
  13. p Circle.info
  14. c = Circle.new 3
  15. p c.area

上面的代码示例展示了Circle类。 除了构造器方法外,它还具有一个类和一个实例方法。

  1. def self.info
  2. "This is a Circle class"
  3. end

self关键字开头的方法是类方法。

  1. def area
  2. "Circle, radius: #{@r}"
  3. end

实例方法不能以self关键字开头。

  1. p Circle.info

我们称为类方法。 注意,我们在类名上调用该方法。

  1. c = Circle.new 3
  2. p c.area

要调用实例方法,我们必须首先创建一个对象。 实例方法总是在对象上调用。 在我们的例子中,c变量保存对象,然后在圆对象上调用area方法。 我们利用点运算符。

  1. $ ./classmethods.rb
  2. "This is a Circle class"
  3. 28.274328

描述 Ruby 中类方法的代码示例的输出。

有三种方法可以在 Ruby 中创建类方法。

  1. #!/usr/bin/ruby
  2. class Wood
  3. def self.info
  4. "This is a Wood class"
  5. end
  6. end
  7. class Brick
  8. class << self
  9. def info
  10. "This is a Brick class"
  11. end
  12. end
  13. end
  14. class Rock
  15. end
  16. def Rock.info
  17. "This is a Rock class"
  18. end
  19. p Wood.info
  20. p Brick.info
  21. p Rock.info

该示例包含三个类。 它们每个都有一个类方法。

  1. def self.info
  2. "This is a Wood class"
  3. end

类方法可以以self关键字开头。

  1. class << self
  2. def info
  3. "This is a Brick class"
  4. end
  5. end

另一种方法是将方法定义放在class << self构造之后。

  1. def Rock.info
  2. "This is a Rock class"
  3. end

这是在 Ruby 中定义类方法的第三种方法。

  1. $ ./classmethods2.rb
  2. "This is a Wood class"
  3. "This is a Brick class"
  4. "This is a Rock class"

我们看到在WoodBrickRock类上调用所有三个类方法的输出。

在 Ruby 中创建实例方法的三种方法

Ruby 有三种创建实例方法的基本方法。 实例方法属于对象的实例。 使用点运算符在对象上调用它们。

  1. #!/usr/bin/ruby
  2. class Wood
  3. def info
  4. "This is a wood object"
  5. end
  6. end
  7. wood = Wood.new
  8. p wood.info
  9. class Brick
  10. attr_accessor :info
  11. end
  12. brick = Brick.new
  13. brick.info = "This is a brick object"
  14. p brick.info
  15. class Rock
  16. end
  17. rock = Rock.new
  18. def rock.info
  19. "This is a rock object"
  20. end
  21. p rock.info

在示例中,我们从WoodBrickRock类创建三个实例对象。 每个对象都有一个定义的实例方法。

  1. class Wood
  2. def info
  3. "This is a wood object"
  4. end
  5. end
  6. wood = Wood.new
  7. p wood.info

这可能是定义和调用实例方法的最常用方法。 info方法在Wood类中定义。 之后,创建对象,然后在对象实例上调用info方法。

  1. class Brick
  2. attr_accessor :info
  3. end
  4. brick = Brick.new
  5. brick.info = "This is a brick object"
  6. p brick.info

另一种方法是使用属性访问器创建方法。 这是一种方便的方法,可以节省程序员的键入时间。 attr_accessor创建两个方法,即获取器和设置器方法。它还创建一个实例变量来存储数据。 使用信息设置器方法,将创建砖对象,并将数据存储在@info变量中。 最后,该消息由info获取器方法读取。

  1. class Rock
  2. end
  3. rock = Rock.new
  4. def rock.info
  5. "This is a rock object"
  6. end
  7. p rock.info

在第三种方法中,我们创建一个空的Rock类。 该对象被实例化。 以后,将动态创建一个方法并将其放置到对象中。

  1. $ ./threeways.rb
  2. "This is a wood object"
  3. "This is a brick object"
  4. "This is a rock object"

示例输出。

Ruby 多态

多态是对不同的数据输入以不同方式使用运算符或函数的过程。 实际上,多态意味着如果类 B 从类 A 继承,则不必继承关于类 A 的所有内容; 它可以完成 A 类所做的某些事情。 (维基百科)

通常,多态是以不同形式出现的能力。 从技术上讲,它是重新定义派生类的方法的能力。 多态与将特定实现应用于接口或更通用的基类有关。

请注意,在静态类型的语言(例如 C++ ,Java 或 C# )和动态类型的语言(例如 Python 或 Ruby)中,多态的定义有所不同。 在静态类型的语言中,当编译器在编译时或运行时确定方法定义时,这一点很重要。 在动态类型的语言中,我们专注于具有相同名称的方法执行不同操作的事实。

  1. #!/usr/bin/ruby
  2. class Animal
  3. def make_noise
  4. "Some noise"
  5. end
  6. def sleep
  7. puts "#{self.class.name} is sleeping."
  8. end
  9. end
  10. class Dog < Animal
  11. def make_noise
  12. 'Woof!'
  13. end
  14. end
  15. class Cat < Animal
  16. def make_noise
  17. 'Meow!'
  18. end
  19. end
  20. [Animal.new, Dog.new, Cat.new].each do |animal|
  21. puts animal.make_noise
  22. animal.sleep
  23. end

我们有一个简单的继承层次结构。 有一个Animal基类和两个后代,即CatDog。 这三个类中的每一个都有其自己的make_noise方法实现。 后代方法的实现替换了Animal类中方法的定义。

  1. class Dog < Animal
  2. def make_noise
  3. 'Woof!'
  4. end
  5. end

Dog类中的make_noise method的实现替换了Animal类中的make_noise的实现。

  1. [Animal.new, Dog.new, Cat.new].each do |animal|
  2. puts animal.make_noise
  3. animal.sleep
  4. end

我们为每个类创建一个实例。 我们在对象上调用make_noisesleep方法。

  1. $ ./polymorhism.rb
  2. Some noise
  3. Animal is sleeping.
  4. Woof!
  5. Dog is sleeping.
  6. Meow!
  7. Cat is sleeping.

这是polymorhism.rb脚本的输出。

Ruby 模块

Ruby Module是方法,类和常量的集合。 模块与类相似,但有一些区别。 模块不能有实例,也不能是子类。

模块用于对相关的类进行分组,方法和常量可以放入单独的模块中。 这也防止了名称冲突,因为模块封装了它们所包含的对象。 在这方面,Ruby 模块类似于 C# 名称空间和 Java 包。

模块还支持在 Ruby 中使用 mixins。 mixin 是 Ruby 工具,用于创建多重继承。 如果一个类从一个以上的类继承功能,那么我们说的是多重继承。

  1. #!/usr/bin/ruby
  2. puts Math::PI
  3. puts Math.sin 2

Ruby 具有内置的Math模块。 它具有多种方法和一个常数。 我们使用::运算符访问PI常量。 和类中一样,方法由点运算符访问。

  1. #!/usr/bin/ruby
  2. include Math
  3. puts PI
  4. puts sin 2

如果我们在脚本中包含模块,则可以直接引用Math对象,而无需使用Math名称。 使用include关键字将模块添加到脚本中。

  1. $ ./modules.rb
  2. 3.141592653589793
  3. 0.9092974268256817

程序的输出。

在下面的示例中,我们展示了如何使用模块来组织代码。

  1. #!/usr/bin/ruby
  2. module Forest
  3. class Rock ; end
  4. class Tree ; end
  5. class Animal ; end
  6. end
  7. module Town
  8. class Pool ; end
  9. class Cinema ; end
  10. class Square ; end
  11. class Animal ; end
  12. end
  13. p Forest::Tree.new
  14. p Forest::Rock.new
  15. p Town::Cinema.new
  16. p Forest::Animal.new
  17. p Town::Animal.new

Ruby 代码可以在语义上进行分组。 岩石和树木属于森林。 游泳池,电影院,广场属于一个城镇。 通过使用模块,我们的代码具有一定的顺序。 动物也可以在森林和城镇中。 在一个脚本中,我们不能定义两个动物类。 他们会发生冲突。 将它们放在不同的模块中,我们可以解决问题。

  1. p Forest::Tree.new
  2. p Forest::Rock.new
  3. p Town::Cinema.new

我们正在创建属于森林和城镇的对象。 要访问模块中的对象,我们使用::运算符。

  1. p Forest::Animal.new
  2. p Town::Animal.new

将创建两个不同的动物对象。 Ruby 解释器可以在它们之间进行区分。 它通过模块名称来标识它们。

  1. $ ./modules3.rb
  2. #<Forest::Tree:0x97f35ec>
  3. #<Forest::Rock:0x97f35b0>
  4. #<Town::Cinema:0x97f3588>
  5. #<Forest::Animal:0x97f3560>
  6. #<Town::Animal:0x97f3538>

这是modules3.rb程序的输出。

本节的最终代码示例将演示使用 Ruby 模块的多重继承。 在这种情况下,这些模块称为混合模块。

  1. #!/usr/bin/ruby
  2. module Device
  3. def switch_on ; puts "on" end
  4. def switch_off ; puts "off" end
  5. end
  6. module Volume
  7. def volume_up ; puts "volume up" end
  8. def vodule_down ; puts "volume down" end
  9. end
  10. module Pluggable
  11. def plug_in ; puts "plug in" end
  12. def plug_out ; puts "plug out" end
  13. end
  14. class CellPhone
  15. include Device, Volume, Pluggable
  16. def ring
  17. puts "ringing"
  18. end
  19. end
  20. cph = CellPhone.new
  21. cph.switch_on
  22. cph.volume_up
  23. cph.ring

我们有三个模块和一个类。 模块代表一些功能。 可以打开和关闭设备。 许多对象可以共享此功能,包括电视,移动电话,计算机或冰箱。 我们没有为每个对象类创建这种被切换为开/关的功能,而是将其分离到一个模块中,如有必要,该模块可以包含在每个对象中。 这样,代码可以更好地组织和紧凑。

  1. module Volume
  2. def volume_up ; puts "volume up" end
  3. def vodule_down ; puts "volume down" end
  4. end

Volume模块组织负责控制音量级别的方法。 如果设备需要这些方法,则仅将模块包含在其类中。

  1. class CellPhone
  2. include Device, Volume, Pluggable
  3. def ring
  4. puts "ringing"
  5. end
  6. end

手机使用include方法添加所有三个模块。 模块的方法在CellPhone类中混合。 并且可用于该类的实例。 CellPhone类还具有特定于它的自己的ring方法。

  1. cph = CellPhone.new
  2. cph.switch_on
  3. cph.volume_up
  4. cph.ring

创建一个CellPhone对象,然后在该对象上调用三个方法。

  1. $ ./mixins.rb
  2. on
  3. volume up
  4. ringing

运行示例将给出此输出。

Ruby 异常

异常是表示偏离正常程序执行流程的对象。 引发,引发或引发异常。

在执行应用期间,许多事情可能出错。 磁盘可能已满,我们无法保存文件。 互联网连接可能断开,我们的应用尝试连接到站点。 所有这些都可能导致我们的应用崩溃。 为了防止这种情况的发生,我们应该预期并应对预期程序操作中的错误。 为此,我们可以使用异常处理。

异常是对象。 它们是内置Exception类的后代。 异常对象携带有关异常的信息。 它的类型(异常的类名),可选的描述性字符串和可选的回溯信息。 程序可以是Exception的子类,或更常见的是StandardError的子类,以获取提供有关操作异常的其他信息的自定义Exception对象。

  1. #!/usr/bin/ruby
  2. x = 35
  3. y = 0
  4. begin
  5. z = x / y
  6. puts z
  7. rescue => e
  8. puts e
  9. p e
  10. end

在上面的程序中,我们有意将数字除以零。 这会导致错误。

  1. begin
  2. z = x / y
  3. puts z

可能失败的语句放置在begin关键字之后。

  1. rescue => e
  2. puts e
  3. p e
  4. end

rescue关键字后面的代码中,我们处理了一个异常。 在这种情况下,我们将错误消息打印到控制台。 e是发生错误时创建的异常对象。

  1. $ ./zerodivision.rb
  2. divided by 0
  3. #<ZeroDivisionError: divided by 0>

在示例的输出中,我们看到了异常消息。 最后一行显示名为ZeroDivisionError的异常对象。

程序员可以使用raise关键字自己引发异常。

  1. #!/usr/bin/ruby
  2. age = 17
  3. begin
  4. if age < 18
  5. raise "Person is a minor"
  6. end
  7. puts "Entry allowed"
  8. rescue => e
  9. puts e
  10. p e
  11. exit 1
  12. end

18 岁以下的年轻人不允许进入俱乐部。 我们在 Ruby 脚本中模拟这种情况。

  1. begin
  2. if age < 18
  3. raise "Person is a minor"
  4. end
  5. puts "Entry allowed"

如果此人是未成年人,则会引发异常情况。 如果raise关键字没有特定的异常作为参数,则会引发RuntimeError异常,将其消息设置为给定的字符串。 该代码未到达puts "Entry allowed"行。 代码的执行被中断,并在救援块处继续执行。

  1. rescue => e
  2. puts e
  3. p e
  4. exit 1
  5. end

在救援块中,我们输出错误消息和RuntimeError对象的字符串表示形式。 我们还调用exit方法来通知环境脚本执行错误结束。

  1. $ ./raise_exception.rb
  2. Person is a minor
  3. #<RuntimeError: Person is a minor>
  4. $ echo $?
  5. 1

未成年人未获准进入俱乐部。 bash $?变量设置为脚本的退出错误。

Ruby 的ensure子句创建一个始终执行的代码块,无论是否存在异常。

  1. #!/usr/bin/ruby
  2. begin
  3. f = File.open("stones", "r")
  4. while line = f.gets do
  5. puts line
  6. end
  7. rescue => e
  8. puts e
  9. p e
  10. ensure
  11. f.close if f
  12. end

在代码示例中,我们尝试打开并读取Stones文件。 I/O 操作容易出错。 我们很容易会有异常。

  1. ensure
  2. f.close if f
  3. end

在确保块中,我们关闭文件处理器。 我们检查处理器是否存在,因为它可能尚未创建。 分配的资源通常放置在确保块中。

如果需要,我们可以创建自己的自定义异常。 Ruby 中的自定义异常应继承自StandardError类。

  1. #!/usr/bin/ruby
  2. class BigValueError < StandardError ; end
  3. LIMIT = 333
  4. x = 3_432_453
  5. begin
  6. if x > LIMIT
  7. raise BigValueError, "Exceeded the maximum value"
  8. end
  9. puts "Script continues"
  10. rescue => e
  11. puts e
  12. p e
  13. exit 1
  14. end

假设我们处于无法处理大量数字的情况。

  1. class BigValueError < StandardError ; end

我们有一个BigValueError类。 该类派生自内置的StandardError类。

  1. LIMIT = 333

超出此常数的数字在我们的程序中被视为big

  1. if x > LIMIT
  2. raise BigValueError, "Exceeded the maximum value"
  3. end

如果该值大于限制,则抛出自定义异常。 我们给异常消息"Exceeded the maximum value"

  1. $ ./custom_exception.rb
  2. Exceeded the maximum value
  3. #<BigValueError: Exceeded the maximum value>

运行程序。

在本章中,我们结束了关于 Ruby 语言的面向对象编程的讨论。