# Ruby 中的面向对象编程 II

> 原文： [https://zetcode.com/lang/rubytutorial/oop2/](https://zetcode.com/lang/rubytutorial/oop2/)

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

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

## Ruby 属性访问器

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

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

```ruby
#!/usr/bin/ruby

class Car

    attr_reader :name, :price
    attr_writer :name, :price  

    def to_s
        "#{@name}: #{@price}"
    end

end

c1 = Car.new
c2 = Car.new

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

c2.name = "Volkswagen"
c2.price = 9500

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

p c1
p c2

```

我们有汽车类。 在类的定义中，我们使用`attr_reader`和`attr_writer`为`Car`类创建两个获取器和设置器方法。

```ruby
attr_reader :name, :price

```

在这里，我们创建两个名为`name`和`price`的实例方法。 注意，`attr_reader`将方法的符号作为参数。

```ruby
attr_writer :name, :price  

```

`attr_writer`创建两个名为`name`和`price`的设置方法，以及两个实例变量`@name`和`@price`。

```ruby
c1.name = "Porsche"
c1.price = 23500

```

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

```ruby
puts "The #{c1.name} costs #{c1.price}"

```

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

```ruby
$ ./arw.rb 
The Porsche costs 23500
Porsche: 23500
Volkswagen: 9500

```

这是示例的输出。

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

```ruby
#!/usr/bin/ruby

class Book
   attr_accessor :title, :pages    
end

b1 = Book.new
b1.title = "Hidden motives"
b1.pages = 255

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

```

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

```ruby
class Book
   attr_accessor :title, :pages    
end

```

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

```ruby
b1 = Book.new
b1.title = "Hidden motives"
b1.pages = 255

```

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

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

```

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

```ruby
$ ./accessor.rb
"The book Hidden motives has 255 pages"

```

这是示例输出。

## Ruby 类常量

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

```ruby
#!/usr/bin/ruby

class MMath

    PI = 3.141592
end

puts MMath::PI

```

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

```ruby
PI = 3.141592

```

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

```ruby
puts MMath::PI

```

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

```ruby
$ ./classconstant.rb 
3.141592

```

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

## Ruby `to_s`方法

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

```ruby
#!/usr/bin/ruby

class Being

    def to_s
        "This is Being class"
    end
end

b = Being.new
puts b.to_s
puts b

```

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

```ruby
def to_s
    "This is Being class"
end

```

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

```ruby
b = Being.new
puts b.to_s
puts b

```

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

```ruby
$ ./tostring.rb 
This is Being class
This is Being class

```

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

## 运算符重载

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

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

```ruby
#!/usr/bin/ruby

class Circle

    attr_accessor :radius

    def initialize r
        @radius = r
    end

    def +(other)
        Circle.new @radius + other.radius
    end

    def to_s
        "Circle with radius: #{@radius}"
    end
end

c1 = Circle.new 5
c2 = Circle.new 6
c3 = c1 + c2

p c3

```

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

```ruby
def +(other)
    Circle.new @radius + other.radius
end

```

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

```ruby
c1 = Circle.new 5
c2 = Circle.new 6
c3 = c1 + c2

```

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

```ruby
$ ./operatoroverloading.rb
Circle with radius: 11

```

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

## Ruby 类方法

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

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

```ruby
#!/usr/bin/ruby

class Circle

    def initialize x
        @r = x
    end

    def self.info
       "This is a Circle class" 
    end

    def area
        @r * @r * 3.141592
    end

end

p Circle.info
c = Circle.new 3
p c.area

```

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

```ruby
def self.info
    "This is a Circle class" 
end

```

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

```ruby
def area
    "Circle, radius: #{@r}"
end

```

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

```ruby
p Circle.info

```

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

```ruby
c = Circle.new 3
p c.area

```

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

```ruby
$ ./classmethods.rb
"This is a Circle class"
28.274328

```

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

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

```ruby
#!/usr/bin/ruby

class Wood

    def self.info
       "This is a Wood class" 
    end
end

class Brick

    class << self
        def info
           "This is a Brick class" 
        end
    end
end

class Rock

end

def Rock.info
   "This is a Rock class" 
end

p Wood.info
p Brick.info
p Rock.info

```

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

```ruby
def self.info
    "This is a Wood class" 
end

```

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

```ruby
class << self
    def info
        "This is a Brick class" 
    end
end

```

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

```ruby
def Rock.info
   "This is a Rock class" 
end

```

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

```ruby
$ ./classmethods2.rb
"This is a Wood class"
"This is a Brick class"
"This is a Rock class"

```

我们看到在`Wood`，`Brick`和`Rock`类上调用所有三个类方法的输出。

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

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

```ruby
#!/usr/bin/ruby

class Wood

    def info
       "This is a wood object"        
    end
end

wood = Wood.new
p wood.info

class Brick

    attr_accessor :info
end

brick = Brick.new
brick.info = "This is a brick object"
p brick.info

class Rock

end

rock = Rock.new

def rock.info
    "This is a rock object"
end

p rock.info

```

在示例中，我们从`Wood`，`Brick`和`Rock`类创建三个实例对象。 每个对象都有一个定义的实例方法。

```ruby
class Wood

    def info
       "This is a wood object"        
    end
end

wood = Wood.new
p wood.info

```

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

```ruby
class Brick

    attr_accessor :info
end

brick = Brick.new
brick.info = "This is a brick object"
p brick.info

```

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

```ruby
class Rock

end

rock = Rock.new

def rock.info
    "This is a rock object"
end

p rock.info

```

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

```ruby
$ ./threeways.rb
"This is a wood object"
"This is a brick object"
"This is a rock object"

```

示例输出。

## Ruby 多态

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

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

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

```ruby
#!/usr/bin/ruby

class Animal

    def make_noise 
        "Some noise"
    end

    def sleep 
        puts "#{self.class.name} is sleeping." 
    end

end

class Dog < Animal

    def make_noise 
        'Woof!'         
    end 

end

class Cat < Animal 

    def make_noise 
        'Meow!' 
    end 
end

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

```

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

```ruby
class Dog < Animal

    def make_noise 
        'Woof!'         
    end 

end

```

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

```ruby
[Animal.new, Dog.new, Cat.new].each do |animal|
  puts animal.make_noise
  animal.sleep
end

```

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

```ruby
$ ./polymorhism.rb
Some noise
Animal is sleeping.
Woof!
Dog is sleeping.
Meow!
Cat is sleeping.

```

这是`polymorhism.rb`脚本的输出。

## Ruby 模块

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

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

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

```ruby
#!/usr/bin/ruby

puts Math::PI
puts Math.sin 2

```

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

```ruby
#!/usr/bin/ruby

include Math

puts PI
puts sin 2

```

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

```ruby
$ ./modules.rb
3.141592653589793
0.9092974268256817

```

程序的输出。

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

```ruby
#!/usr/bin/ruby

module Forest

    class Rock ; end
    class Tree ; end
    class Animal ; end    

end

module Town

   class Pool ; end
   class Cinema ; end
   class Square ; end
   class Animal ; end

end

p Forest::Tree.new
p Forest::Rock.new
p Town::Cinema.new

p Forest::Animal.new
p Town::Animal.new

```

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

```ruby
p Forest::Tree.new
p Forest::Rock.new
p Town::Cinema.new

```

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

```ruby
p Forest::Animal.new
p Town::Animal.new

```

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

```ruby
$ ./modules3.rb
#<Forest::Tree:0x97f35ec>
#<Forest::Rock:0x97f35b0>
#<Town::Cinema:0x97f3588>
#<Forest::Animal:0x97f3560>
#<Town::Animal:0x97f3538>

```

这是`modules3.rb`程序的输出。

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

```ruby
#!/usr/bin/ruby

module Device
    def switch_on ; puts "on" end    
    def switch_off ; puts "off" end
end

module Volume
    def volume_up ; puts "volume up" end    
    def vodule_down ; puts "volume down" end
end

module Pluggable
    def plug_in ; puts "plug in" end    
    def plug_out ; puts "plug out" end
end

class CellPhone
    include Device, Volume, Pluggable

    def ring
        puts "ringing"
    end    
end

cph = CellPhone.new
cph.switch_on
cph.volume_up
cph.ring

```

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

```ruby
module Volume
    def volume_up ; puts "volume up" end    
    def vodule_down ; puts "volume down" end
end

```

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

```ruby
class CellPhone
    include Device, Volume, Pluggable

    def ring
        puts "ringing"
    end    
end

```

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

```ruby
cph = CellPhone.new
cph.switch_on
cph.volume_up
cph.ring

```

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

```ruby
$ ./mixins.rb
on
volume up
ringing

```

运行示例将给出此输出。

## Ruby 异常

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

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

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

```ruby
#!/usr/bin/ruby

x = 35
y = 0

begin
    z = x / y
    puts z
rescue => e
    puts e
    p e
end

```

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

```ruby
begin
    z = x / y
    puts z

```

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

```ruby
rescue => e
    puts e
    p e
end

```

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

```ruby
$ ./zerodivision.rb
divided by 0
#<ZeroDivisionError: divided by 0>

```

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

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

```ruby
#!/usr/bin/ruby

age = 17

begin
    if age < 18
        raise "Person is a minor"
    end

    puts "Entry allowed"
rescue => e
    puts e
    p e
    exit 1
end

```

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

```ruby
begin
    if age < 18
        raise "Person is a minor"
    end

    puts "Entry allowed"

```

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

```ruby
rescue => e
    puts e
    p e
    exit 1
end

```

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

```ruby
$ ./raise_exception.rb 
Person is a minor
#<RuntimeError: Person is a minor>
$ echo $?
1

```

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

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

```ruby
#!/usr/bin/ruby

begin
    f = File.open("stones", "r")

    while line = f.gets do
        puts line
    end    

rescue => e
    puts e
    p e
ensure
    f.close if f
end

```

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

```ruby
ensure
    f.close if f
end

```

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

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

```ruby
#!/usr/bin/ruby

class BigValueError < StandardError ; end

LIMIT = 333
x = 3_432_453

begin

    if x > LIMIT
        raise BigValueError, "Exceeded the maximum value"
    end

    puts "Script continues"

rescue => e
    puts e
    p e
    exit 1
end

```

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

```ruby
class BigValueError < StandardError ; end

```

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

```ruby
LIMIT = 333

```

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

```ruby
if x > LIMIT
    raise BigValueError, "Exceeded the maximum value"
end

```

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

```ruby
$ ./custom_exception.rb
Exceeded the maximum value
#<BigValueError: Exceeded the maximum value>

```

运行程序。

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