Skip to content

Ruby 面向对象

Ruby 面向对象编程 (Object-Oriented Programming, OOP)

Section titled “Ruby 面向对象编程 (Object-Oriented Programming, OOP)”

Ruby 是一种纯面向对象编程 (OOP) 语言。在 Ruby 中,一切都是对象 (Object),包括像数字 (Numbers)、字符串 (Strings) 这样的原始数据类型,甚至 true、false 和 nil。类 (Classes) 本身也是对象(Class 类的实例)。本章将探讨 Ruby 中的核心 OOP 概念。

类是创建对象的蓝图 (Blueprint)。它封装了数据(属性或实例变量 Instance Variables)和操作这些数据的方法 (Methods)(行为 Behaviors)。

定义一个类会创建一个新的数据类型。它规定了该类的对象由什么组成(其状态 State)以及可以对它执行哪些操作(其行为 Behavior)。

类定义以 class 关键字开头,后跟类名(必须以大写字母开头),并以 end 关键字结束。按照惯例,多单词的类名使用驼峰命名法 (CamelCase)(例如,CustomerAccount)。

class Box
# code for the class goes here
end

定义 Ruby 对象 (Objects) / 实例 (Instances)

Section titled “定义 Ruby 对象 (Objects) / 实例 (Instances)”

对象是类的实例 (Instances)。您可以使用 new 方法创建一个对象(实例化一个类),new 方法是所有类都可用的类方法 (Class Method)。

box1 = Box.new
box2 = Box.new

initialize 方法是 Ruby 中一个特殊的实例方法 (Instance Method)。它充当构造方法 (Constructor),当您使用 ClassName.new 创建新对象时会自动调用。它用于设置对象的初始状态,通常通过为实例变量赋值。它可以接受参数 (Arguments)。

class Box
def initialize(width, height)
@width = width # @width 是一个实例变量
@height = height # @height 是一个实例变量
end
end
# 当调用 Box.new(10, 20) 时,initialize 方法被调用,w=10, h=20。

实例变量以 @ 符号开头(例如,@width)。它们保存特定类实例(对象)的数据。每个对象都拥有自己的实例变量副本。在类内部,可以直接使用 @variable_name 访问它们。要从类外部访问或修改它们,通常使用存取器 (Accessor) 和设置器 (Setter) 方法。

class Box
def initialize(w, h)
# 赋值给实例变量
@width = w
@height = h
end
end

存取器 (Accessor) 和设置器 (Setter) 方法 (Getter 和 Setter)

Section titled “存取器 (Accessor) 和设置器 (Setter) 方法 (Getter 和 Setter)”

为了允许从类外部对对象的实例变量进行受控访问,您可以定义存取器(getter)方法来读取它们的值,以及设置器(setter 或 mutator)方法来更改它们。Ruby 提供了创建这些常用方法的便捷快捷方式:

  • attr_reader :variable_name_symbol: 创建 getter 方法。
  • attr_writer :variable_name_symbol: 创建 setter 方法。
  • attr_accessor :variable_name_symbol: 同时创建 getter 和 setter 方法。

使用 attr_accessor 的示例:

#!/usr/bin/env ruby
class Box
# 创建 getter (width, height) 和 setter (width=, height=) 方法
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
def display_dimensions
puts "Box dimensions - Width: #{@width}, Height: #{@height}"
end
end
# 创建一个对象
box = Box.new(10, 20)
box.display_dimensions
# 使用存取器 (getter) 方法
puts "Initial width: #{box.width}"
# 使用设置器方法
box.width = 30
box.height = 50
puts "Updated width: #{box.width}"
box.display_dimensions

运行上述代码,将产生:

Box dimensions - Width: 10, Height: 20
Initial width: 10
Updated width: 30
Box dimensions - Width: 30, Height: 50

手动定义 getter 和 setter(由于 attr_* 辅助方法,现在不太常用):

#!/usr/bin/env ruby
class Box
def initialize(w, h)
@width = w
@height = h
end
# width 的 Getter
def width
@width
end
# width 的 Setter
def width=(value)
@width = value
end
# height 的 Getter
def height
@height
end
# height 的 Setter
def height=(value)
@height = value
end
end
box = Box.new(10, 20)
box.width = 30 # 调用 width=(30)
puts box.height # 调用 height()

实例方法定义对象的行为。它们使用 def 关键字在类内定义,并操作被调用对象的数据(实例变量)。它们通过类的实例来调用。

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
# 计算面积的实例方法
def area
@width * @height
end
end
# 创建一个对象
box = Box.new(10, 20)
# 调用实例方法
a = box.area
puts "Area of the box is: #{a}"

运行结果:

Area of the box is: 200

类方法 (Class Methods) 和类变量 (Class Variables)

Section titled “类方法 (Class Methods) 和类变量 (Class Variables)”

类变量(例如,@@count)在类的所有实例、类本身及其子类之间共享。它们以 @@ 为前缀,必须在类定义内初始化。

类方法通过在类定义内给方法名加上 self. 前缀(例如,def self.method_name)或 ClassName. 来定义。它们在类本身上调用,而不是在实例上调用,通常操作类变量或作为实例的工厂 (Factories)。

#!/usr/bin/env ruby
class Box
# 类变量
@@count = 0
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
@@count += 1
end
# 类方法
def self.print_count
puts "Box count is: #{@@count}"
end
end
# 创建两个对象
box1 = Box.new(10, 20)
box2 = Box.new(30, 100)
# 调用类方法
Box.print_count

运行结果:

Box count is: 2

to_s 实例方法用于返回对象的字符串表示。它会被 puts 等方法或在字符串插值中使用对象时自动调用。为您的类定义自定义的 to_s 方法是一个好习惯,可以提供有意义的字符串表示。

#!/usr/bin/env ruby
class Box
attr_reader :width, :height # 本例只需要 getter
def initialize(w, h)
@width = w
@height = h
end
# 自定义 to_s 方法
def to_s
"(w:#{@width}, h:#{@height})" # 对象的字符串格式化
end
end
# 创建一个对象
box = Box.new(10, 20)
# puts 自动调用 to_s
puts "String representation of box is: #{box}"

运行结果:

String representation of box is: (w:10, h:20)

访问控制 (Access Control)(public, private, protected)

Section titled “访问控制 (Access Control)(public, private, protected)”

Ruby 为实例方法提供了三个级别的访问控制。访问控制不直接应用于实例变量或类变量;对它们的访问通常通过方法进行管理。

  • Public (公开): Public 方法可以被任何人从任何地方调用。方法默认是 public 的(除了 initialize,它是隐式 private 的)。
  • Private (私有): Private 方法只能由同一类定义内的其他实例方法调用,并且只能通过隐式接收者 (Implicit Receiver)(self)。它们不能通过显式接收者 (Explicit Receiver) 调用(例如,object.private_method 会失败,即使在类内部也是如此)。initialize 始终是 private 的。
  • Protected (受保护): Protected 方法可以由定义该类的实例方法及其子类的实例方法调用。与 private 方法不同,它们可以使用显式接收者调用,前提是该接收者是 self 或同一类或子类的实例。

访问控制关键字 (public、private、protected) 应用于它们在类定义中声明之后的方法,直到遇到另一个访问控制关键字或类定义结束。它们也可以接受方法名(作为符号 Symbol)作为参数,以设置特定方法的可见性。

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
# 默认是 public
def calculate_area
internal_width * internal_height # 调用 private 方法
end
# 演示 protected 的实例方法
def compare_area(other_box)
# 如果 other_box 是 Box 或其子类,则可以在其上调用 protected 方法
other_box.display_protected_area # 这对于 protected 是可行的
# 对于本例,我们直接使用它的 public calculate_area
self.calculate_area > other_box.calculate_area
end
protected
def display_protected_area
puts "Protected: Area is #{internal_width * internal_height}"
end
private
# private getter 方法(示例,通常 attr_reader 会是 public 或 protected)
def internal_width
@width
end
def internal_height
@height
end
end
box = Box.new(10, 20)
# 调用 public 方法
area = box.calculate_area
puts "Area of the box is: #{area}"
# 尝试直接从外部调用 private 或 protected 方法(将导致错误)
begin
# box.internal_width # 这将引发 NoMethodError(private 方法被调用)
# box.display_protected_area # 这将引发 NoMethodError(protected 方法被调用)
rescue NoMethodError => e
puts "Error: #{e.message}"
end
box2 = Box.new(5,10)
puts "Box 1 is larger than Box 2: #{box.compare_area(box2)}"
# box.display_protected_area # 仍然不能直接从外部调用
# box2.display_protected_area # 这个也不行

运行上述代码时,尝试直接从外部调用 private/protected 方法将导致 NoMethodError。公共方法 calculate_area 可以正常工作。

Area of the box is: 200
Box 1 is larger than Box 2: true

继承是 OOP 的基本概念,一个类(子类 Subclass 或派生类 Derived Class)可以继承另一个类(超类 Superclass 或基类 Base Class)的属性 (Attributes) 和方法。这促进了代码重用,并建立了“is-a”(是)关系(例如,BigBox 是一种 Box)。

Ruby 支持单继承(一个类只能继承自一个超类)。然而,Ruby 通过使用模块 (Modules) 进行 Mixin 来实现类似于多重继承 (Multiple Inheritance) 的特性。Mixin 允许一个类包含一个或多个模块的方法。

要定义子类,使用 < 符号,后跟超类名。

#!/usr/bin/env ruby
class Box # 超类
attr_accessor :width, :height
def initialize(w, h)
@width = w
@height = h
end
def area
@width * @height
end
end
# BigBox 是 Box 的子类
class BigBox < Box
# BigBox 继承了 Box 的 initialize, width, height, 和 area
# 添加 BigBox 特有的新实例方法
def print_area
# @width 和 @height 可访问,因为它们是实例变量,并且 BigBox 是 Box。
# 如果它们在 Box 中是 private 方法,直接访问可能会有所不同。
# 'area' 方法是继承来的,并且是 public。
puts "Big box area is: #{area}" # 使用继承的 area 方法
end
end
# 创建子类对象
big_box = BigBox.new(100, 200)
# 调用继承方法(通过 print_area)
big_box.print_area
# 直接调用继承方法
puts "Directly calling area: #{big_box.area}"

运行结果:

Big box area is: 20000
Directly calling area: 20000

子类可以提供其超类中已定义的实例方法的特定实现。这称为方法重写 (Method Overriding)。子类中重写的方法必须与超类中的方法具有相同的名称。

您可以使用 super 关键字在重写方法内部调用超类版本的方法。

#!/usr/bin/env ruby
class Box
def initialize(w, h)
@width = w
@height = h
end
def display_info
"Box with width #{@width} and height #{@height}"
end
end
class ColoredBox < Box
attr_accessor :color
def initialize(w, h, color)
super(w, h) # 调用 Box 的 initialize 方法
@color = color
end
# 重写 display_info 方法
def display_info
# 调用超类的 display_info 方法并添加更多信息
"#{super}, and color #{@color}"
end
end
colored_box = ColoredBox.new(10, 20, "Red")
puts colored_box.display_info

运行结果:

Box with width 10 and height 20, and color Red

Ruby 允许您重新定义标准运算符(如 +、-、*、[])如何对您的类对象进行操作。这通过定义与运算符名称对应的方法来实现。例如,要重载 + 运算符,您需要定义一个名为 + 的方法。

class Vector2D
attr_accessor :x, :y
def initialize(x, y)
@x, @y = x, y
end
# 重载 + 运算符进行向量加法
def +(other_vector)
# 确保 other_vector 也是 Vector2D 对象以保证类型安全(可选,但推荐)
raise ArgumentError, "Can only add Vector2D objects" unless other_vector.is_a?(Vector2D)
Vector2D.new(@x + other_vector.x, @y + other_vector.y)
end
# 重载一元 - 运算符用于向量分量取反
def -@
Vector2D.new(-@x, -@y)
end
# 重载 * 运算符进行标量乘法
def *(scalar)
raise ArgumentError, "Can only multiply by a Numeric" unless scalar.is_a?(Numeric)
Vector2D.new(@x * scalar, @y * scalar)
end
def to_s
"(#{x}, #{y})"
end
end
v1 = Vector2D.new(2, 3)
v2 = Vector2D.new(5, 1)
sum_vector = v1 + v2 # 调用 v1.+(v2)
puts "v1 + v2 = #{sum_vector}" # 输出: (7, 4)
negated_v1 = -v1 # 调用 v1.-@()
puts "-v1 = #{negated_v1}" # 输出: (-2, -3)
scaled_v1 = v1 * 3 # 调用 v1.*(3)
puts "v1 * 3 = #{scaled_v1}" # 输出: (6, 9)

freeze 方法(所有对象都可用,因为它来自 Object)可以阻止对象被修改。一旦对象被冻结,尝试更改其实例变量或内部状态将引发 FrozenError(在旧版 Ruby 中为 RuntimeError)。您可以使用 frozen? 方法检查对象是否已冻结。

#!/usr/bin/env ruby
class Config
attr_accessor :setting
def initialize(val)
@setting = val
end
end
app_config = Config.new("initial_value")
puts "Initially frozen? #{app_config.frozen?}" # => false(初始是否冻结?)
app_config.setting = "new_value" # 可修改
puts "Setting after change: #{app_config.setting}"
app_config.freeze # 冻结对象
puts "Now frozen? #{app_config.frozen?}" # => true(现在是否冻结?)
begin
app_config.setting = "another_value" # 这将引发错误
rescue FrozenError => e
puts "Error: #{e.message}"
end
# 注意:冻结对象通常是浅冻结(shallow freeze)。
# 如果实例变量引用了另一个可变对象(例如,一个数组),那么那个对象本身并不会被冻结,除非你也明确地冻结它。
# 例如:
# mutable_array = [1, 2]
# app_config.some_array = mutable_array
# app_config.freeze
# app_config.some_array << 3 # 如果 mutable_array 没有被冻结,这条语句仍然可以修改它。

此代码演示了冻结对象:

Initially frozen? false
Setting after change: new_value
Now frozen? true
Error: can't modify frozen Config

类内部的常量通过为以大写字母开头的名称赋值来定义。它们通常用于不应更改的值。按照惯例,常量名称使用 ALL_CAPS_WITH_UNDERSCORES 格式。

常量可以在类内部直接使用名称访问。从类外部,它们使用类名后跟作用域解析运算符 :: 访问(例如,ClassName::CONSTANT_NAME)。尽管 Ruby 在您尝试重新赋值常量时会发出警告,但它技术上允许这样做(这通常是混淆的来源)。

#!/usr/bin/env ruby
class Box
BOX_COMPANY = "ACME Inc."
DEFAULT_MATERIAL = "Cardboard"
attr_reader :material
def initialize(material = DEFAULT_MATERIAL)
@material = material
end
def display_company
puts "This box is from #{BOX_COMPANY} made of #{@material}."
end
end
box = Box.new
box.display_company
custom_box = Box.new("Plastic")
custom_box.display_company
puts "Default material for all boxes: #{Box::DEFAULT_MATERIAL}"

运行结果:

This box is from ACME Inc. made of Cardboard.
This box is from ACME Inc. made of Plastic.
Default material for all boxes: Cardboard

类常量会被子类继承,并且可以被重写(尽管这也会生成警告,通常不鼓励这样做)。

allocate 类方法创建一个类的实例,但不调用其 initialize 方法。生成的对象未初始化;其实例变量将为 nil。在典型的应用代码中很少需要这样做,通常用于更高级的场景,例如对象反序列化或某些元编程任务。

#!/usr/bin/env ruby
class Box
attr_accessor :width, :height
def initialize(w, h)
puts "Initialize called!"
@width, @height = w, h
end
def area
# 如果 width 或 height 为 nil,这将导致错误
@width * @height
end
end
puts "Creating box1 with new:"
box1 = Box.new(10, 20)
puts "Area of box1: #{box1.area}"
puts "\nCreating box2 with allocate:"
box2 = Box.allocate # initialize 没有被调用
puts "box2 width: #{box2.width.inspect}" # => nil(box2 的 width)
puts "box2 height: #{box2.height.inspect}" # => nil(box2 的 height)
begin
puts "Area of box2: #{box2.area}"
rescue NoMethodError => e
# 尝试在 nil 上调用 * (例如 nil * nil)
puts "Error calculating area for box2: #{e.message}"
end

运行结果类似于:

Creating box1 with new:
Initialize called!
Area of box1: 200
Creating box2 with allocate:
box2 width: nil
box2 height: nil
Error calculating area for box2: undefined method `*' for nil:NilClass

在类定义内部,self 指代类本身。您可以使用标准对象方法在 self 上获取关于类的信息。

#!/usr/bin/env ruby
class MyCoolClass
puts "Inside class definition:"
puts "Type of self: #{self.class}" # self 是类 MyCoolClass,它的类是 Class
puts "Name of self: #{self.name}" # self.name 提供类名字符串
puts "Self inspect: #{self.inspect}" # 对于类来说,与 self.name 相同
end

加载此文件时,它将产生:

Inside class definition:
Type of self: Class
Name of self: MyCoolClass
Self inspect: MyCoolClass

这表明类定义是可执行代码,并且在其执行期间,当前对象 (self) 是正在定义的类。这使得 Ruby 具备强大的元编程 (Metaprogramming) 能力。