ZetCode

Ruby 面向对象编程 II

最后修改于 2023 年 10 月 18 日

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

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

Ruby 属性访问器

所有 Ruby 变量都是私有的。只能通过方法访问它们。这些方法通常被称为 setter 和 getter。创建 setter 和 getter 方法是一个非常常见的任务。因此 Ruby 具有创建这两种类型方法的便捷方法。它们是 attr_readerattr_writerattr_accessor

attr_reader 创建 getter 方法。attr_writer 方法为这些 setter 创建 setter 方法和实例变量。attr_accessor 方法创建 getter、setter 方法及其实例变量。

arw.rb
#!/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}"

puts c1
puts c2

我们有一个 Car 类。在类的定义中,我们使用 attr_readerattr_writer 为 Car 类创建两个 getter 和 setter 方法。

attr_reader :name, :price

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

attr_writer :name, :price

attr_writer 创建了两个名为 name 和 price 的 setter 方法以及两个实例变量 @name@price

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

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

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

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

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

正如我们上面已经陈述的,attr_accessor 方法创建 getter、setter 方法及其实例变量。

accessor.rb
#!/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 创建了两对方法和两个实例变量。

class Book
   attr_accessor :title, :pages
end

attr_accessor 方法设置 title 和 pages 方法以及 @title@pages 实例变量。

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

创建了 Book 类的对象。两个 setter 方法填充了对象的实例变量。

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

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

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

Ruby 类常量

Ruby 允许您创建类常量。这些常量不属于具体对象。它们属于类。按照约定,常量用大写字母书写。

class_constant.rb
#!/usr/bin/ruby

class MMath

    PI = 3.141592
end


puts MMath::PI

我们有一个 MMath 类,其中包含一个 PI 常量。

PI = 3.141592

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

puts MMath::PI

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

$ ./class_constant.rb
3.141592

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

Ruby to_s 方法

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

tostring.rb
#!/usr/bin/ruby

class Being

    def to_s
        "This is Being class"
    end
end

b = Being.new
puts b.to_s
puts b

我们有一个 Being 类,我们在其中覆盖了 to_s 方法的默认实现。

def to_s
    "This is Being class"
end

创建的每个类都继承自基本 Object 类。to_s 方法属于此类。我们覆盖 to_s 方法并创建一个新实现。我们提供我们对象的可读描述。

b = Being.new
puts b.to_s
puts b

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

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

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

Ruby 运算符重载

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

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

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

puts c3

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

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

我们定义了一个名为 + 的方法。该方法添加了两个 Circle 对象的半径。

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

我们创建了两个 Circle 对象。在第三行中,我们将这两个对象相加,创建一个新的对象。

$ ./operator_overloading.rb
Circle with radius: 11

将这两个 Circle 对象相加,会创建一个半径为 11 的第三个对象。

Ruby 类方法

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

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

class_methods.rb
#!/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 类。除了构造函数方法外,它还有一个类方法和一个实例方法。

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

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

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

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

p Circle.info

我们调用一个类方法。请注意,我们在类名上调用该方法。

c = Circle.new 3
p c.area

要调用一个实例方法,我们必须首先创建一个对象。实例方法始终在对象上调用。在我们的例子中,c 变量保存着对象,我们在 Circle 对象上调用 area 方法。我们使用点运算符。

$ ./class_methods.rb
"This is a Circle class"
28.274328

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

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

该示例有三个类。每个类都有一个类方法。

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

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

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

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

def Rock.info
   "This is a Rock class"
end
$ ./classmethods2.rb
"This is a Wood class"
"This is a Brick class"
"This is a Rock class"

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

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

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

three_ways.rb
#!/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、BrickRock 类创建了三个实例对象。每个对象都有一个实例方法定义。

class Wood

    def info
       "This is a wood object"
    end
end

wood = Wood.new
p wood.info

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

class Brick

    attr_accessor :info
end

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

另一种方法是使用属性访问器创建方法。这是一种方便的方式,可以为程序员节省一些打字时间。attr_accessor 创建了两种方法,getter 和 setter 方法。它还创建了一个实例变量,用于存储数据。创建了 brick 对象,并使用 info setter 方法将数据存储在 @info 变量中。最后,由 info getter 方法读取消息。

class Rock

end

rock = Rock.new

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

p rock.info

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

$ ./three_ways.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)中,多态性的定义存在一些差异。在静态类型语言中,编译器在编译时还是运行时确定方法定义很重要。在动态类型语言中,我们关注的是具有相同名称的方法做不同的事情。

polymorhism.rb
#!/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 类中方法的定义。

class Dog < Animal

    def make_noise
        'Woof!'
    end

end

Dog 类中 make_noise 方法 的实现取代了 Animal 类中 make_noise 的实现。

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

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

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

Ruby 模块

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

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

模块还支持在 Ruby 中使用 mixin。一个 mixin 是 Ruby 创建 多重继承 的工具。如果一个类从多个类继承功能,我们谈论多重继承。

modules.rb
#!/usr/bin/ruby

puts Math::PI
puts Math.sin 2

Ruby 有一个内置的 Math 模块。它有多个方法和一个常量。我们使用 :: 运算符访问 PI 常量。方法通过点运算符访问,就像在类中一样。

modules2.rb
#!/usr/bin/ruby

include Math

puts PI
puts sin 2

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

$ ./modules2.rb
3.141592653589793
0.9092974268256817

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

modules3.rb
#!/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 代码可以按语义进行分组。岩石和树属于森林。游泳池、电影院、广场属于城镇。通过使用模块,我们的代码具有一定的顺序。动物也可以在森林中,也可以在城镇中。在一个脚本中,我们不能定义两个 animal 类。它们会发生冲突。将它们放在不同的模块中,我们解决了这个问题。

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

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

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

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

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

本节的最后一个代码示例将演示使用 Ruby 模块的多重继承。在这种情况下,模块被称为 mixin。

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

我们有三个模块和一个类。模块表示某些功能。可以打开和关闭设备。许多对象可以共享此功能,包括电视、手机、计算机或冰箱。我们不是为每个对象类创建这种开/关的能力,而是将其分离到一个模块中,如果需要,可以将其包含在每个对象中。这样代码的组织性更好,更紧凑。

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

Volume 模块组织了负责控制音量级别的方法。如果设备需要这些方法,它只需将其模块包含到其类中即可。

class CellPhone
    include Device, Volume, Pluggable

    def ring
        puts "ringing"
    end
end

手机添加了所有三个模块,使用 include 方法。模块的方法混合在 CellPhone 类中。并可用于该类的实例。CellPhone 类也有其自己的 ring 方法,该方法是其特有的。

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

创建了一个 CellPhone 对象,我们在该对象上调用了三种方法。

$ ./mixins.rb
on
volume up
ringing

运行该示例会产生以下输出。

Ruby 异常

异常是表明程序执行的正常流程发生偏差的对象。异常被引发、抛出或启动。

在我们的应用程序执行期间,可能会出现很多问题。磁盘可能会被填满,我们无法保存文件。互联网连接可能会中断,我们的应用程序试图连接到一个站点。所有这些都可能导致我们的应用程序崩溃。为了防止这种情况发生,我们应该预测并响应预期的程序操作中的错误。为此,我们可以使用异常处理。

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

zero_division.rb
#!/usr/bin/ruby

x = 35
y = 0

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

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

begin
    z = x / y
    puts z

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

rescue => e
    puts e
    p e
end

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

$ ./zero_division.rb
divided by 0
#<ZeroDivisionError: divided by 0>

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

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

raise_exception.rb
#!/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 脚本中模拟这种情况。

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

    puts "Entry allowed"

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

rescue => e
    puts e
    p e
    exit 1
end

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

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

该人(未成年人)不允许进入俱乐部。bash $? 变量设置为脚本的退出错误。

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

stones.txt
Garnet
Topaz
Opal
Amethyst
Ruby
Jasper
Pyrite
Malachite
Quartz
ensure_clause.rb
#!/usr/bin/ruby

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

    while line = f.gets do
        puts line
    end

rescue => e
    puts e
    p e

ensure
    f.close if f
end

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

ensure
    f.close if f
end

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

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

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

假设我们遇到了一种无法处理大数的情况。

class BigValueError < StandardError ; end

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

LIMIT = 333

超过此常量的数字被我们的程序视为“大”数字。

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

如果该值大于限制,我们将抛出我们的自定义异常。我们给异常一个消息“超过最大值”。

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

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