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 语言面向对象编程的讨论。