Ruby 面向对象编程 II
最后修改于 2023 年 10 月 18 日
在本 Ruby 教程中,我们继续讨论 Ruby 中的面向对象编程。
我们从属性访问器开始。我们涵盖了类常量、类方法和运算符重载。我们定义了多态性,并展示了如何在 Ruby 中使用它。我们还提到了模块和异常。
Ruby 属性访问器
所有 Ruby 变量都是私有的。只能通过方法访问它们。这些方法通常被称为 setter 和 getter。创建 setter 和 getter 方法是一个非常常见的任务。因此 Ruby 具有创建这两种类型方法的便捷方法。它们是 attr_reader、attr_writer 和 attr_accessor。
attr_reader 创建 getter 方法。attr_writer 方法为这些 setter 创建 setter 方法和实例变量。attr_accessor 方法创建 getter、setter 方法及其实例变量。
#!/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_reader 和 attr_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 方法及其实例变量。
#!/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 允许您创建类常量。这些常量不属于具体对象。它们属于类。按照约定,常量用大写字母书写。
#!/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 方法。
#!/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 中,运算符和方法之间只有细微的差别。
#!/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 方法可以分为类方法和实例方法。类方法 在类上调用。它们不能在类的实例上调用。
类方法不能访问实例变量。
#!/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 中有三种创建类方法的方法。
#!/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"
我们看到在 Wood、Brick 和 Rock 类上调用所有三个类方法的输出。
在 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 类创建了三个实例对象。每个对象都有一个实例方法定义。
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)中,多态性的定义存在一些差异。在静态类型语言中,编译器在编译时还是运行时确定方法定义很重要。在动态类型语言中,我们关注的是具有相同名称的方法做不同的事情。
#!/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_noise 和 sleep 方法。
$ ./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 创建 多重继承 的工具。如果一个类从多个类继承功能,我们谈论多重继承。
#!/usr/bin/ruby puts Math::PI puts Math.sin 2
Ruby 有一个内置的 Math 模块。它有多个方法和一个常量。我们使用 :: 运算符访问 PI 常量。方法通过点运算符访问,就像在类中一样。
#!/usr/bin/ruby include Math puts PI puts sin 2
如果我们 在脚本中包含一个模块,我们可以直接引用 Math 对象,省略 Math 名称。使用 include 关键字将模块添加到脚本中。
$ ./modules2.rb 3.141592653589793 0.9092974268256817
在下面的示例中,我们展示了如何使用模块来组织代码。
#!/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。
#!/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,以获取提供有关操作异常的其他信息的自定义异常对象。
#!/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 关键字自己引发异常。
#!/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 子句创建一个始终执行的代码块,无论是否存在异常。
Garnet Topaz Opal Amethyst Ruby Jasper Pyrite Malachite Quartz
#!/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 类。
#!/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 语言面向对象编程的讨论。