← 返回目录

第三章:方法与面向对象

方法、闭包、类与模块

1. 方法定义

Ruby 用 def...end 定义方法,方法名使用蛇形命名法(snake_case)。

def greet(name)
  "Hello, #{name}!"
end

puts greet("Ruby")  # => "Hello, Ruby!"

默认参数与关键字参数

def connect(host, port: 3306, ssl: false)
  puts "连接 #{host}:#{port},SSL: #{ssl}"
end

connect("db.example.com")                    # 使用默认值
connect("db.example.com", port: 5432, ssl: true)

def log(message, level: :info, timestamp: Time.now)
  puts "[#{timestamp}] #{level.upcase}: #{message}"
end

可变参数(Splat)

def sum(*numbers)
  numbers.reduce(0, :+)
end

puts sum(1, 2, 3, 4, 5)  # => 15

def create_user(name, **options)
  puts "用户:#{name}"
  options.each { |k, v| puts "  #{k}: #{v}" }
end

create_user("Alice", role: "admin", active: true)

方法命名约定

def empty?        # 以 ? 结尾 —— 返回布尔值(谓词方法)
  @items.length == 0
end

def save!         # 以 ! 结尾 —— 危险操作(修改自身或可能抛异常)
  raise "验证失败" unless valid?
  perform_save
end

def name=(value)  # 以 = 结尾 —— setter 方法
  @name = value.strip
end

隐式返回:Ruby 方法自动返回最后一个表达式的值,无需写 return。只有需要提前退出方法时才使用 return。这让代码更简洁,也是 Ruby 风格的标志。

🔄 函数/方法定义对比

语言 语法
Ruby def greet(name) ... end
Python def greet(name): ...
PHP function greet(string $name): string { ... }
Java public String greet(String name) { ... }

Ruby 和 Python 用 def,但 Ruby 用 end 替代缩进来界定方法体。Ruby 的 ?/! 命名约定是其他语言没有的特色。

2. Block、Proc 与 Lambda

Block 是 Ruby 最独特的特性之一,它是一段可以传递给方法的代码块。

Block 基础

# 两种写法:do...end 适合多行,{} 适合单行
[1, 2, 3].each do |n|
  puts n * 10
end

[1, 2, 3].each { |n| puts n * 10 }

yield 调用 Block

def with_logging
  puts "[开始] #{Time.now}"
  result = yield
  puts "[结束] #{Time.now}"
  result
end

with_logging do
  sleep(1)
  "操作完成"
end

def repeat(n)
  n.times { |i| yield(i) }
end

repeat(3) { |i| puts "第 #{i + 1} 次" }

&block 参数

def transform(data, &block)
  data.map(&block)
end

result = transform([1, 2, 3]) { |n| n ** 2 }
puts result.inspect  # => [1, 4, 9]

def execute(&block)
  block.call if block    # 显式调用
  puts "没有 Block" unless block
end

execute { puts "有 Block" }
execute

Proc vs Lambda

square = Proc.new { |n| n ** 2 }
square = proc { |n| n ** 2 }       # 简写
puts square.call(5)                 # => 25
puts square.(5)                     # => 25(简写调用)
puts square[5]                      # => 25(另一种简写)

double = lambda { |n| n * 2 }
double = ->(n) { n * 2 }           # 箭头 Lambda(推荐写法)
puts double.call(5)                 # => 10

# Proc 与 Lambda 的两个关键区别:
# 1. 参数检查:Lambda 严格检查参数数量,Proc 宽松(多余忽略,不足补 nil)
# 2. return 行为:Lambda 的 return 返回到 Lambda 自身,Proc 的 return 返回到外层方法

validator = ->(age) { age >= 18 }
[15, 20, 12, 25].select(&validator)  # => [20, 25]

Method 对象

def triple(n)
  n * 3
end

m = method(:triple)
puts m.call(5)            # => 15
[1, 2, 3].map(&m)         # => [3, 6, 9]

puts method(:puts).arity  # => -1(可变参数)

Block 是 Ruby 的灵魂:几乎所有 Ruby 的迭代、资源管理、DSL 都建立在 Block 之上。File.open(path) { |f| ... } 会自动关闭文件,ActiveRecord 的作用域和回调也是 Block 的应用。理解 Block/Proc/Lambda 是掌握 Ruby 的关键。

3. 类 (Class)

class User
  attr_accessor :name, :email    # 同时生成 getter 和 setter
  attr_reader :id                # 只生成 getter
  attr_writer :password          # 只生成 setter

  @@user_count = 0

  def initialize(name, email)
    @id = SecureRandom.uuid rescue rand(10000)
    @name = name
    @email = email
    @@user_count += 1
  end

  def to_s
    "#{@name} <#{@email}>"
  end

  def self.count
    @@user_count
  end

  def admin?
    @email.end_with?("@admin.com")
  end

  private

  def encrypt_password(raw)
    Digest::SHA256.hexdigest(raw) rescue raw.reverse
  end
end

user = User.new("Alice", "alice@example.com")
puts user.name       # => "Alice"(attr_accessor 生成的 getter)
user.name = "Bob"    # attr_accessor 生成的 setter
puts user            # => "Bob "(调用 to_s)
puts user.admin?     # => false
puts User.count      # => 1(类方法)

继承

class Admin < User
  attr_reader :permissions

  def initialize(name, email, permissions = [])
    super(name, email)
    @permissions = permissions
  end

  def admin?
    true
  end

  def can?(action)
    @permissions.include?(action)
  end
end

admin = Admin.new("Charlie", "charlie@admin.com", [:manage_users, :edit_posts])
puts admin.admin?                # => true
puts admin.can?(:manage_users)   # => true
puts admin.is_a?(User)           # => true
puts admin.is_a?(Admin)          # => true
puts Admin.superclass            # => User

访问控制

class Account
  def deposit(amount)      # public(默认)
    @balance += amount
  end

  protected

  def balance              # protected:同类或子类的实例可以调用
    @balance
  end

  def >(other)
    balance > other.balance
  end

  private

  def audit_log(action)    # private:只能在实例内部调用,不能有显式接收者
    puts "[审计] #{action}"
  end
end

4. 模块与 Mixin

Ruby 是单继承语言,但通过模块(Module)的 Mixin 机制实现了多重继承的效果。

模块作为 Mixin

module Loggable
  def log(message)
    puts "[#{self.class}] #{message}"
  end
end

module Serializable
  def to_json
    instance_variables.each_with_object({}) do |var, hash|
      hash[var.to_s.delete("@")] = instance_variable_get(var)
    end.to_s
  end
end

class Order
  include Loggable        # 作为实例方法混入
  include Serializable

  def initialize(id, total)
    @id = id
    @total = total
  end

  def process
    log("处理订单 ##{@id}")
  end
end

order = Order.new(1, 99.9)
order.process           # => "[Order] 处理订单 #1"
puts order.to_json      # => {"id"=>1, "total"=>99.9}

include vs extend vs prepend

module Greetable
  def hello
    "Hello from #{self.class}!"
  end
end

class MyClass
  include Greetable    # hello 成为实例方法
end
MyClass.new.hello      # => "Hello from MyClass!"

class MyClass2
  extend Greetable     # hello 成为类方法
end
MyClass2.hello         # => "Hello from MyClass2!"

module Logging
  def save
    puts "保存前记录日志..."
    super                # 调用原始的 save
    puts "保存后记录日志..."
  end
end

class Record
  def save
    puts "保存数据"
  end
end

class LoggedRecord < Record
  prepend Logging      # prepend 插入到方法查找链的前面
end

LoggedRecord.new.save
# 输出:
# 保存前记录日志...
# 保存数据
# 保存后记录日志...

模块作为命名空间

module Payment
  class Gateway
    def charge(amount)
      puts "收费 #{amount}"
    end
  end

  class Receipt
    def generate
      puts "生成收据"
    end
  end
end

gateway = Payment::Gateway.new
gateway.charge(100)

🔄 接口/Trait/Mixin 对比

语言 机制 特点
Ruby Module (Mixin) 可包含具体实现,通过 include/extend/prepend 混入
Java Interface Java 8+ 支持 default 方法,但主要是契约定义
PHP Trait 类似 Ruby Mixin,用 use 引入,支持冲突解决
Python 多重继承 直接支持多继承,通过 MRO 解决方法查找顺序

5. 常用内置模块

Enumerable

只要类实现了 each 方法并 include Enumerable,就能获得数十个集合操作方法:

class NumberSet
  include Enumerable

  def initialize(*numbers)
    @data = numbers
  end

  def each(&block)
    @data.each(&block)
  end
end

set = NumberSet.new(3, 1, 4, 1, 5, 9)
puts set.sort.inspect        # => [1, 1, 3, 4, 5, 9]
puts set.min                 # => 1
puts set.max                 # => 9
puts set.select(&:odd?).inspect  # => [3, 1, 1, 5, 9]
puts set.any? { |n| n > 8 }     # => true
puts set.all? { |n| n > 0 }     # => true
puts set.find { |n| n.even? }   # => 4
puts set.flat_map { |n| [n, -n] }.inspect  # => [3, -3, 1, -1, ...]
puts set.group_by(&:even?).inspect  # => { false: [3,1,1,5,9], true: [4] }
puts set.tally.inspect       # => {3=>1, 1=>2, 4=>1, 5=>1, 9=>1}

Comparable

class Temperature
  include Comparable

  attr_reader :degrees

  def initialize(degrees)
    @degrees = degrees
  end

  def <=>(other)
    @degrees <=> other.degrees
  end
end

temps = [Temperature.new(30), Temperature.new(15), Temperature.new(25)]
puts temps.sort.map(&:degrees).inspect  # => [15, 25, 30]
puts temps.min.degrees                  # => 15
puts Temperature.new(20).between?(Temperature.new(10), Temperature.new(30))  # => true

Kernel 常用方法

puts "标准输出(自动换行)"
print "不换行"
p [1, 2, 3]       # 输出 inspect 结果:[1, 2, 3](调试用)
pp({ a: 1, b: { c: [2, 3] } })  # 格式化输出复杂对象

name = gets.chomp              # 读取用户输入
system("echo hello")           # 执行 shell 命令
result = `ls -la`              # 反引号捕获命令输出
sleep(2)                       # 暂停 2 秒
rand(100)                      # 0-99 随机整数
Array(nil)                     # => [](安全转换)
Array("hello")                 # => ["hello"]
Array([1, 2])                  # => [1, 2]

p vs puts vs pp:puts 调用对象的 to_s 方法输出人类可读的文本。p 调用 inspect 方法,输出带引号的字符串和结构信息,适合调试。pp(pretty print)对复杂嵌套对象做格式化缩进输出。

6. Struct 与 Data

Struct —— 轻量数据容器

Point = Struct.new(:x, :y)

p1 = Point.new(3, 4)
puts p1.x          # => 3
puts p1.y          # => 4
puts p1.to_a       # => [3, 4]
p1.x = 10          # Struct 默认可修改

Point = Struct.new(:x, :y) do
  def distance_to(other)
    Math.sqrt((x - other.x) ** 2 + (y - other.y) ** 2)
  end
end

p1 = Point.new(0, 0)
p2 = Point.new(3, 4)
puts p1.distance_to(p2)  # => 5.0

Data —— 不可变值对象(Ruby 3.2+)

Color = Data.define(:r, :g, :b)

red = Color.new(r: 255, g: 0, b: 0)
puts red.r          # => 255
# red.r = 100       # NoMethodError! Data 对象不可变

blue = Color.new(r: 0, g: 0, b: 255)
puts red == Color.new(r: 255, g: 0, b: 0)  # => true(值比较)

Config = Data.define(:host, :port) do
  def url
    "http://#{host}:#{port}"
  end
end

cfg = Config.new(host: "localhost", port: 3000)
puts cfg.url  # => "http://localhost:3000"

OpenStruct —— 动态属性

require "ostruct"

person = OpenStruct.new(name: "Alice", age: 30)
person.email = "alice@example.com"    # 随时添加新属性
puts person.name    # => "Alice"
puts person.email   # => "alice@example.com"
puts person.phone   # => nil(不存在的属性返回 nil)

选择建议:需要简单的数据载体用 Struct;需要不可变的值对象用 Data(Ruby 3.2+);需要动态灵活的属性用 OpenStruct(性能较差,避免在热路径使用)。需要完整业务逻辑用 Class

7. 本章要点

🔧 方法定义

def...end 定义方法,支持默认参数、关键字参数、*args/**kwargs?/!/= 命名约定表达语义。隐式返回。

📦 Block/Proc/Lambda

Block 是 Ruby 的核心闭包机制,用 yield&block 接收。Lambda 严格检查参数,Proc 宽松。箭头 Lambda ->(x) { } 是推荐写法。

🏗️ 类与继承

class...end 定义类,initialize 是构造方法。attr_accessor 生成 getter/setter。< 实现继承,super 调用父类。

🧩 模块与 Mixin

include 混入实例方法,extend 混入类方法,prepend 插入方法链前端。模块也用作命名空间。

🔢 Enumerable/Comparable

实现 each + include Enumerable 获得全套集合操作。实现 <=> + include Comparable 获得排序能力。

📋 Struct/Data

Struct 快速创建数据容器,Data(3.2+)创建不可变值对象,OpenStruct 支持动态属性。按需选用。

方法和面向对象是 Ruby 的核心。掌握 Block/Proc/Lambda 和 Mixin 机制后,你就拥有了阅读和编写地道 Ruby 代码的能力。接下来可以深入 Ruby 标准库和 Rails 框架。