Skip to content

Ruby 速查表

动态、优雅的语言,为开发者幸福感而优化。

01

基础

变量与类型

Ruby 是动态类型的——变量不需要类型声明。使用 .class 检查类型,使用 is_a? 进行类型检查。一切都是对象,包括数字和布尔值。

ruby
name = "Alice"    # string
age = 30           # integer
pi = 3.14          # float
is_dev = true      # boolean
nums = [1, 2, 3]   # array
puts name.class    # String
puts age.is_a?(Integer)  # true

符号

符号(:name)是不可变的、驻留的字符串——内存中只存在一个副本。将它们用于哈希键、方法名和标识比内容更重要的标识符。对于重复使用,比字符串更节省内存。

ruby
status = :active
puts status.class        # Symbol
puts status.to_s         # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status]       # active

Nil 与真值

在 Ruby 中,只有 nil 和 false 是假值——其他所有值(包括 0、'' 和 [])都是真值。这与许多 0 为假值的语言不同。使用 || 提供默认值,使用 nil? 专门检查 nil。

ruby
x = nil
puts x.nil?        # true
puts x || "default"  # default
# Only nil and false are falsy; 0 and "" are truthy!
puts 0 ? "truthy" : "falsy"  # truthy
puts "" ? "truthy" : "falsy" # truthy
puts nil ? "truthy" : "falsy" # falsy

类型转换

to_i/to_s/to_f 是宽松转换(失败时返回 0)。Integer()/Float() 是严格的(引发 ArgumentError)。需要验证时使用严格转换,想要优雅降级时使用宽松转换。

ruby
puts "42".to_i      # 42
puts 42.to_s        # "42"
puts "3.14".to_f    # 3.14
puts 3.14.to_i      # 3
puts "abc".to_i     # 0 (no error)
puts Integer("42")  # 42 (raises if invalid)

字符串插值

双引号字符串支持 #{expr} 插值——内部可以是任何 Ruby 表达式。单引号字符串是字面量(无插值或转义,除了 \\ 和 \')。需要插值或转义序列时使用双引号。

ruby
name = "Alice"
age = 30
puts "Name: #{name}, Age: #{age}"
puts "5 + 3 = #{5 + 3}"
puts "Upper: #{name.upcase}"
# Single quotes don't interpolate
puts 'No #{name} here'  # No #{name} here
02

字符串

常用字符串方法

Ruby 字符串有丰富的方法。大多数返回新字符串(Ruby 中字符串是可变的)。使用 ! 变体(upcase!、gsub!)进行原地修改——它们修改接收者,如果无更改则返回 nil。

ruby
s = "Hello, World"
puts s.length         # 12
puts s.upcase         # HELLO, WORLD
puts s.downcase       # hello, world
puts s.reverse        # dlroW ,olleH
puts s.split(", ")    # ["Hello", "World"]
puts s.gsub("o", "0") # Hell0, W0rld

字符串修改(Bang 方法)

以 ! 结尾的方法原地修改对象,通常更高效。如果未进行更改,它们可能返回 nil。当你想避免创建副本时使用,但要注意副作用。

ruby
s = "hello"
s.upcase!    # s is now "HELLO"
s.gsub!(/L/, "1")  # s is now "HE11O"
puts s       # HE11O
# Bang methods modify in-place; use with caution
arr = [3, 1, 2]
arr.sort!    # arr is now [1, 2, 3]

Heredoc 与多行

Heredoc(<<TEXT ... TEXT)创建多行字符串。<<~(波浪号 heredoc)去除公共前导空白以保持代码整洁。适用于 SQL 查询、HTML 模板或长消息。

ruby
text = <<~HEREDOC
  Hello,
  World!
  Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentation

格式化(sprintf)

使用 % 运算符或 format/sprintf 进行 C 风格字符串格式化。%-10s 在 10 个字符中左对齐,%08.2f 零填充到 8 个字符并保留 2 位小数。适用于表格输出和固定宽度格式化。

ruby
puts sprintf("%s is %d", "Alice", 30)
puts "%-10s|%5d" % ["Name", 42]
puts format("%.2f", 3.14159)  # 3.14
puts "%08.2f" % 3.14          # 00003.14
# %s string, %d integer, %f float

字符串连接

+ 创建新字符串,<< 原地追加(构建字符串更高效)。* 重复字符串。join 用分隔符组合数组元素。性能上优先使用 << 或 join 而非重复的 +。

ruby
s1 = "Hello" + ", " + "World"
s2 = ["a", "b", "c"].join("-")  # a-b-c
s3 = "Hello"
s3 << " " << "World"  # in-place append
s4 = "x" * 3          # xxx (repetition)
puts s1, s2, s3, s4
03

数据结构

数组

数组是有序的、从零索引的,可以保存混合类型。<< 和 push 添加到末尾。使用 include? 检查成员,使用 sum 求和。数组是可变的——使用 freeze 使其不可变。

ruby
nums = [1, 2, 3, 4, 5]
nums.push(6)          # [1,2,3,4,5,6]
nums << 7             # same as push
nums[0] = 0
puts nums.first       # 0
puts nums.length      # 7
puts nums.sum         # 28
puts nums.include?(3) # true

哈希

哈希是键值字典。符号键(name:)是惯用的且高效。使用 key? 检查存在,使用 fetch 安全访问(缺失时引发异常),使用 transform_values 批量更新。

ruby
user = { name: "Alice", age: 30 }
user[:email] = "[email protected]"
puts user[:name]          # Alice
puts user.key?(:name)     # true
user.each { |k, v| puts "#{k}: #{v}" }
puts user.values          # ["Alice", 30, "[email protected]"]
puts user.transform_values(&:to_s)

范围

'..' 包含结尾,'...' 不包含。范围是惰性的,对于大序列节省内存。适用于迭代、切片和生成序列。可用于任何 Comparable 类型。

ruby
(1..5).each { |n| puts n }      # 1 2 3 4 5
puts (1...5).to_a                # [1, 2, 3, 4]
puts (1..10).select(&:even?)     # [2, 4, 6, 8, 10]
puts ('a'..'e').include?('c')    # true
puts (1..5).map { |n| n ** 2 }   # [1, 4, 9, 16, 25]

Enumerable 方法

Enumerable 是 Ruby 最强大的混入——map、select、reject、reduce、find、group_by、sort_by 等。使用 &:method 作为 { |x| x.method } 的简写。这些实现了富有表现力的数据转换管道。

ruby
nums = [1, 2, 3, 4, 5]
puts nums.map { |n| n * 2 }.inspect    # [2,4,6,8,10]
puts nums.select(&:even?).inspect      # [2, 4]
puts nums.reject(&:odd?).inspect       # [2, 4]
puts nums.reduce(0) { |sum, n| sum + n } # 15
puts nums.find { |n| n > 3 }           # 4
puts nums.group_by(&:even?).inspect    # {false=>[1,3,5], true=>[2,4]}

集合

Set(来自 'set' 库)存储唯一元素,O(1) 查找。用于去重和集合操作(并集、交集、差集)。需要数组时用 to_a 转换。需要 require 'set'。

ruby
require 'set'
a = Set.new([1, 2, 3])
b = Set.new([3, 4, 5])
puts a.union(b).to_a.inspect        # [1,2,3,4,5]
puts a.intersection(b).to_a.inspect # [3]
puts a.subtract([1]).to_a.inspect   # [2, 3]
puts a.subset?(Set.new([1,2,3,4]))  # true
a.add(6)
04

控制流

If / Elsif / Unless

if/elsif/else 是标准分支。unless 是 if 的相反(条件为假时执行)。修饰符形式(statement if condition)是单行守卫的惯用写法——提高简单情况的可读性。

ruby
score = 85
if score >= 90
  puts "A"
elsif score >= 80
  puts "B"
else
  puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60

Case(When)

case/when 使用 === 进行匹配,支持范围、类和正则表达式。多个值用逗号分隔。then 允许单行体。没有目标时,case 充当更清晰的 if/elsif 链。

ruby
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
end

While / Until / Loop

while 在为真时运行,until 运行直到为真(即为假时)。loop 是无限的——使用 break 退出。集合优先使用迭代器(each、map)而非 while——它们更惯用且不易出错。

ruby
count = 0
while count < 3
  puts count
  count += 1
end
# until = opposite of while
n = 3
until n == 0
  puts n
  n -= 1
end
# Infinite loop with break
loop do
  puts "forever"
  break if rand > 0.8
end

迭代器(Each/Times/Upto)

Ruby 的迭代器比 for 循环更惯用。times 用于计数,upto/downto 用于范围,each_with_index 用于索引+值。step 控制增量。这些是 Ruby 迭代的骨干。

ruby
3.times { |i| puts i }          # 0 1 2
1.upto(3) { |n| puts n }        # 1 2 3
3.downto(1) { |n| puts n }      # 3 2 1
[1,2,3].each_with_index do |n, i|
  puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n }   # 1 2 3

Break / Next / Redo

next 跳到下一次迭代(类似 continue),break 提前退出循环。break 可以从块返回值。redo 重新开始当前迭代而不重新检查条件——很少使用。

ruby
[1, 2, 3, 4, 5].each do |n|
  next if n.even?    # skip even
  break if n > 4     # stop at 5
  puts n             # prints 1, 3
end
# break value returns from the block
result = [1,2,3].each { |n| break n * 10 if n == 2 }
puts result  # 20
05

方法与块

方法定义

方法使用 def/end。关键字参数(key:)提高多参数的可读性。*args 将额外位置参数收集到数组中,**kwargs 将关键字参数收集到哈希中。默认值使用 =。

ruby
def greet(name, greeting: "Hello")
  "#{greeting}, #{name}!"
end
puts greet("Alice")               # Hello, Alice!
puts greet("Bob", greeting: "Hi") # Hi, Bob!
def add(*nums)  # splat (variadic)
  nums.sum
end
puts add(1, 2, 3, 4)  # 10

块与 Yield

块是传递给方法的匿名代码块。yield 调用块。块在 Ruby 中无处不在(each、map 等)。使用 yield 使方法灵活——调用者提供行为。

ruby
def repeat(n)
  n.times { yield }
end
repeat(3) { puts "hi" }  # prints hi 3 times
def with_result
  yield(5)
end
puts with_result { |x| x * 2 }  # 10

Proc 与 Lambda

Proc 和 lambda 是存储在变量中的可复用块。关键区别:lambda 检查参数数量(不匹配时引发异常)且仅从自身返回;proc 是宽松的并从封闭方法返回。优先使用 lambda 获得严格行为。

ruby
square = proc { |x| x * x }
puts square.call(5)   # 25
puts square.(5)       # 25 (shortcut)
double = lambda { |x| x * 2 }
puts double.call(5)   # 10
# Lambda checks arity; Proc doesn't
# Lambda returns from itself; Proc returns from enclosing method

方法对象(&)

&:method 将方法名转换为 Proc。它是简单单方法块的惯用简写:map(&:to_i) 而非 map { |s| s.to_i }。对于简单转换更清晰、更可读。

ruby
nums = ["1", "2", "3"]
ints = nums.map(&:to_i)  # [1, 2, 3]
puts ints.inspect
# &:to_i is shorthand for { |s| s.to_i }
names = ["alice", "bob"]
up = names.map(&:upcase)  # ["ALICE", "BOB"]
puts up.inspect

返回值

Ruby 方法隐式返回最后求值的表达式——无需显式 return。使用 return 提前退出。多个值作为数组返回并可解构。这使代码简洁。

ruby
def status(ok)
  return "error" unless ok
  "ok"  # implicit return of last expression
end
puts status(true)   # ok
puts status(false)  # error
# Multiple assignment
def coords
  return 1, 2  # returns [1, 2]
end
x, y = coords
06

类与面向对象

类与实例变量

@var = 实例变量(每个对象),@@var = 类变量(共享)。attr_accessor 生成 getter+setter,attr_reader 仅 getter,attr_writer 仅 setter。self.method 定义类方法。

ruby
class Person
  attr_accessor :name, :age
  attr_reader :id
  @@count = 0  # class variable
  def initialize(name, age)
    @name = name  # instance variable
    @age = age
    @@count += 1
  end
  def self.count; @@count; end
end
p = Person.new("Alice", 30)
puts p.name, Person.count

继承与 Super

< 表示继承(仅单继承)。super 调用父类的当前方法版本。使用 super(带括号传递参数,不带传递相同参数)扩展父行为。Ruby 使用单继承 + 混入。

ruby
class Animal
  def initialize(name); @name = name; end
  def speak; "..."; end
end
class Dog < Animal
  def speak; "#{@name}: Woof!"; end
end
class Puppy < Dog
  def speak; "#{super} (small)"; end
end
puts Puppy.new("Rex").speak  # Rex: Woof! (small)

模块(混入)

模块分组可复用方法。include 添加实例方法,extend 添加类方法。这是 Ruby 对多重继承的解决方案。Enumerable 是著名的混入——包含它并定义 each 即可获得 map、select 等。

ruby
module Walkable
  def walk; "#{@name} is walking"; end
end
module Swimmable
  def swim; "#{@name} is swimming"; end
end
class Duck
  include Walkable    # instance methods
  extend Swimmable    # class methods
  def initialize(n); @name = n; end
end
puts Duck.new("Donald").walk  # Donald is walking

访问控制

public(默认)、private(仅可无显式接收者调用)、protected(可在类层次结构内调用)。内部辅助方法使用 private,同一类实例之间共享的方法使用 protected。

ruby
class BankAccount
  def initialize(bal); @balance = bal; end
  def deposit(amt); @balance += amt; end
  def balance; @balance; end
  private
  def audit; "auditing..."; end
  protected
  def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance  # 100
# a.audit  # Error: private method

Self 与类方法

self 指当前对象。在类体内,self 是类——def self.method 定义类方法。类方法在类上调用(Counter.total),实例方法在实例上调用。alias_method 创建方法别名。

ruby
class Counter
  @@total = 0
  def initialize; @@total += 1; end
  def self.total; @@total; end
  def self.reset!; @@total = 0; end
  def instance_method; "I'm an instance"; end
  alias_method :count, :instance_method
end
Counter.new; Counter.new
puts Counter.total  # 2
07

错误处理

Begin / Rescue / Ensure

begin/rescue 是 Ruby 的 try/catch。针对特定异常类进行救援以进行针对性处理。=> e 捕获异常对象。ensure 总是运行——用于清理(关闭文件、释放锁)。

ruby
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "Caught: #{e.message}"
rescue => e
  puts "Other error: #{e.class}"
ensure
  puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failure

Raise 与自定义异常

raise 抛出异常(不带参数的 raise 重新抛出)。自定义异常继承自 StandardError(或更具体的类)。用 Error 后缀命名。按类救援以处理特定失败模式。

ruby
class InvalidAgeError < StandardError; end
def set_age(age)
  raise InvalidAgeError, "Age cannot be negative" if age < 0
  raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
  @age = age
end
begin
  set_age(-5)
rescue InvalidAgeError => e
  puts "Custom: #{e.message}"
end

Retry

retry 从头开始重启 begin 块。用于瞬时失败(网络、速率限制),配合计数器以避免无限循环。没有限制,retry 可能挂起程序——始终保护它。

ruby
attempts = 0
begin
  attempts += 1
  fetch_data  # might fail
rescue NetworkError
  retry if attempts < 3
  puts "Failed after 3 attempts"
end
# retry re-runs the begin block from the top

Rescue 修饰符

rescue 作为修饰符是提供回退值的简洁方式。它捕获 StandardError 并返回右侧。用于简单情况——避免用于复杂逻辑,因为它隐藏错误。非常适合解析或可选操作。

ruby
result = risky_operation rescue "default"
puts result
# Same as:
# result = begin; risky_operation; rescue; "default"; end
# Catches StandardError only
json = JSON.parse(str) rescue nil
puts "Invalid JSON" if json.nil?

Throw / Catch(控制流)

throw/catch 不是异常处理——它是一种用于从深层嵌套提前退出的控制流机制(与其他语言不同)。throw :symbol 跳转到匹配的 catch。用于跳出嵌套循环;实际错误使用 begin/rescue。

ruby
catch(:done) do
  [1, 2, 3, 4, 5].each do |n|
    throw :done, n if n > 3
    puts n
  end
end
# Prints 1, 2, 3 and returns 4
# Not for errors—use for early exit from nested loops
08

文件 I/O

读写文件

File.write/File.read 是简单的单次方法。File.open 带块自动关闭文件。File.foreach 逐行读取而不加载整个文件——对大文件节省内存。chomp 移除尾部换行符。

ruby
# Write
File.write("test.txt", "Hello, File!")
# Read
content = File.read("test.txt")
puts content  # Hello, File!
# Append
File.open("log.txt", "a") { |f| f.puts "new line" }
# Read line by line
File.foreach("test.txt") { |line| puts line.chomp }

文件块(自动关闭)

始终使用 File.open 带块——它保证文件被关闭,即使发生错误。块形式是处理文件的惯用、安全方式。不带块时,你必须手动调用 close。

ruby
File.open("data.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
end  # file auto-closed here
File.open("data.txt", "r") do |f|
  f.each_line.with_index { |line, i| puts "#{i}: #{line}" }
end

文件存在与信息

File 类提供文件系统查询。file?/directory? 区分类型。mtime/ctime/atime 提供时间戳。rename/delete 修改文件系统。操作前始终检查存在以避免错误。

ruby
puts File.exist?("test.txt")  # true
puts File.file?("test.txt")   # true (regular file)
puts File.directory?(".")     # true
puts File.size("test.txt")    # bytes
puts File.mtime("test.txt")   # modification time
File.rename("old.txt", "new.txt")
File.delete("new.txt") if File.exist?("new.txt")

目录操作

Dir 管理目录。mkdir 创建,chdir 更改(块形式之后恢复)。glob 匹配文件模式——* 匹配任意,** 递归匹配。适用于文件发现和批处理。

ruby
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
  File.write("a.txt", "a")
  puts Dir.pwd  # current path
end
Dir.glob("*.txt") { |f| puts f }  # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursive

CSV 与 JSON

CSV 和 JSON 在标准库中。CSV.foreach 流式传输行(节省内存)。JSON.parse 返回带字符串键的哈希/数组。to_json 序列化任何对象。这些是数据交换的基础。

ruby
require 'csv'
require 'json'
CSV.write("data.csv", [["a", 1], ["b", 2]])
CSV.foreach("data.csv") { |row| puts row.inspect }
data = { name: "Alice", age: 30 }
File.write("data.json", data.to_json)
parsed = JSON.parse(File.read("data.json"))
puts parsed["name"]  # Alice
09

日期/时间与正则表达式

Time 与 Date

Time 表示时刻(带时区)。Date 表示日历日期(无时间)。Time 上的算术使用秒。strftime 用 %Y(年)、%m(月)、%d(日)、%H:%M(时间)格式化。ISO8601 解析需要 require 'time'。

ruby
require 'time'
now = Time.now
puts now                    # 2024-01-15 14:30:00 +0800
puts now.strftime("%Y-%m-%d %H:%M")  # 2024-01-15 14:30
tomorrow = now + 86400      # +1 day (seconds)
puts tomorrow.strftime("%A") # weekday name
date = Date.today
puts date.next_day          # tomorrow

日期解析与算术

日期算术自然工作——加整数加天数。next_month/prev_month 处理月边界。Date - Date 返回 Rational(天)。upto/downto 遍历日期范围。日历逻辑使用 Date,时间戳使用 Time。

ruby
require 'date'
d = Date.parse("2024-01-15")
puts d.year   # 2024
puts d + 7    # 2024-01-22 (+7 days)
puts d.next_month  # 2024-02-15
diff = (Date.today - d).to_i
puts "#{diff} days since"
puts Date.today.upto(Date.today + 6).map(&:wday)

正则表达式匹配

=~ 返回匹配位置或 nil。$1、$2 在匹配后保存捕获组。.match 返回 MatchData 对象以获取更多细节。简单检查使用 =~,提取捕获使用 .match。正则字面量使用 /pattern/。

ruby
s = "Phone: 123-4567"
if s =~ /(\d+)-(\d+)/
  puts "Area: #{$1}, Number: #{$2}"
end
m = /\w+@(\w+)/.match("[email protected]")
puts m[1]  # example
puts "abc123" =~ /\d/ ? "has digit" : "no digit"

正则替换

gsub 替换所有匹配(sub 替换第一个)。传递块进行动态替换。scan 将所有匹配提取到数组。替换字符串中的反向引用(\1、\2)引用捕获组。强大的文本处理工具。

ruby
s = "Hello, World"
puts s.gsub(/o/, "0")       # Hell0, W0rld
puts s.gsub(/\w+/) { |w| w.capitalize }
puts s.scan(/\w+/).inspect  # ["Hello", "World"]
puts "2024-01-15".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
# => 15/01/2024

正则选项

正则选项:i(不区分大小写)、m(多行——点匹配换行)、x(扩展——允许模式中的空白/注释)。%r{} 是替代分隔符,当模式包含斜杠(如 URL)时很有用。

ruby
puts /hello/i =~ "HELLO"  # 0 (case-insensitive)
puts /line/m =~ "a\nb"    # 0 (multiline: . matches \n)
puts /x.y/x =~ "x y"       # 0 (extended: ignore whitespace)
# Common patterns
email = /[\w.]+@[\w]+\.[a-z]+/
url = %r{https?://[\w./]+}
puts email.match("[email protected]") ? "valid" : "invalid"
10

元编程与并发

动态方法定义

define_method 在运行时创建方法。用于生成多个相似方法或构建 DSL。这是元编程——编写代码的代码。强大但谨慎使用;它会使代码更难理解。

ruby
class Dynamic
  [:foo, :bar, :baz].each do |name|
    define_method(name) { puts "Called #{name}" }
  end
end
d = Dynamic.new
d.foo  # Called foo
d.bar  # Called bar
# Useful for generating similar methods

method_missing

method_missing 拦截对未定义方法的调用。用于构建灵活的 API 或代理。始终也重写 respond_to_missing?,以便反射工作。谨慎使用——它会隐藏 bug 并混淆静态分析。

ruby
class Proxy
  def method_missing(name, *args)
    puts "Called: #{name} with #{args.inspect}"
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end
p = Proxy.new
p.anything(1, 2)  # Called: anything with [1, 2]

线程

线程并发运行代码。join 等待完成。MRI(标准 Ruby)有 GIL,因此 CPU 密集型线程不会真正并行运行——I/O 密集型线程会。对于真正的并行,使用 Ractor(Ruby 3.0+)或多进程。

ruby
threads = [1, 2, 3].map do |n|
  Thread.new { puts "Thread #{n}: #{n * n}" }
end
threads.each(&:join)  # wait for all
# MRI has GIL—threads don't run truly parallel for CPU work
# Use for I/O concurrency (network, file)

Fiber(协作式)

Fiber 是轻量级、协作式并发——它们通过 yield/resume 手动暂停和恢复。与线程不同,它们不并行运行。用于生成器、惰性求值或可暂停的计算。开销比线程低。

ruby
fiber = Fiber.new do
  Fiber.yield 1
  Fiber.yield 2
  3
end
puts fiber.resume  # 1
puts fiber.resume  # 2
puts fiber.resume  # 3
# Fibers yield control cooperatively, not preemptively

Send 与 Eval

send 按名称调用任何方法(包括私有)。public_send 尊重可见性。eval 将字符串作为 Ruby 代码执行——极其强大但对不受信任的输入很危险(代码注入)。使用 send 进行动态派发,生产中避免 eval。

ruby
class Obj
  def secret; "hidden"; end
end
o = Obj.new
puts o.send(:secret)  # hidden (calls private too)
puts o.public_send(:secret)  # hidden (respects visibility)
x = 5
result = eval("x * 2")
puts result  # 10
# eval runs a string as code—avoid with untrusted input!
11

块、Proc 与 Lambda

块基础

块是 Ruby 最常见的闭包——通过 { } 或 do...end 传递给方法的匿名代码。yield 调用块。|n| 声明块参数。block_given? 检查是否传递了块。块不是对象(不能存储在变量中)——为此使用 Proc/Lambda。每个方法都可以接受隐式块,使 DSL 自然(Rails 大量使用这一点)。

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }          # do-end for multi-line
[1, 2, 3].each do |n|
  puts n * 2
end

# yield: call the block from a method
def greet
  puts "before"
  yield  # invokes the block
  puts "after"
end
greet { puts "in block" }
# before / in block / after

# yield with arguments
def compute
  result = yield(10, 20)
  puts "got: #{result}"
end
compute { |a, b| a + b }  # got: 30

# block_given? checks if a block was passed
def maybe_yield
  return "no block" unless block_given?
  yield
end

Proc 与 Lambda

Proc 和 Lambda 都是可调用对象(变成对象的块)。Proc 是宽松的:额外参数变为 nil,缺少的参数为 nil,'return' 退出封闭方法。Lambda 是严格的:它们检查参数数量且 'return' 仅退出 lambda。想要类方法行为时使用 lambda;想要类块行为时使用 Proc。->(stabby lambda)是现代简洁语法。

ruby
# Proc: a block stored as an object
p = Proc.new { |x| puts x * 2 }
p.call(5)      # 10
p.(5)          # 10 (shorthand)
p[5]           # 10 (another shorthand)

# Lambda: stricter Proc
l = lambda { |x| puts x * 2 }
l = ->(x) { puts x * 2 }  # stabby lambda syntax
l.call(5)

# Key differences:
# 1. Argument checking
p = Proc.new { |a, b| puts a }
p.call(1)        # 1 (b is nil, no error)
l = ->(a, b) { puts a }
l.call(1)        # ArgumentError (wrong number of args)

# 2. return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # never reached—Proc return exits method
end
proc_test  # 1

def lambda_test
  l = lambda { return 1 }
  l.call  # returns from lambda, not method
  return 2
end
lambda_test  # 2

方法对象

method(:name) 将方法检索为 Method 对象(像 Proc 一样可调用)。&:symbol 将符号转换为发送该方法的 proc——极其常见的惯用法:array.map(&:to_s)。& 前缀将 Proc 转换为块(或反之)。这使将方法作为参数优雅传递成为可能。方法对象保留其接收者,因此它们绑定到对象。

ruby
class Calculator
  def add(a, b) a + b end
end

calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3)  # 5
m.(2, 3)      # 5
m[2, 3]       # 5

# Convert method to Proc
p = m.to_proc
p.call(4, 5)  # 9

# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase)  # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }

# Symbol#to_proc
:upcase.to_proc.call("hello")  # "HELLOW"

# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts))  # prints 1, 2, 3

闭包与绑定

闭包(块、proc、lambda)按引用捕获变量——它们能看到捕获变量的更新。这实现了有状态的闭包(计数器、累加器)。Proc#binding 访问闭包的环境(用于高级元编程)。Kernel#binding 方法捕获当前执行上下文用于 eval。闭包是 Ruby 块在回调和迭代器中如此强大的原因。

ruby
# Closures capture surrounding variables
counter = 0
increment = lambda { counter += 1 }
increment.call  # 1
increment.call  # 2
puts counter    # 2 (closure modified it)

# Multiple closures share the same variables
x = 10
add = lambda { |n| x += n }
get = lambda { x }
add.call(5)
get.call  # 15

# Proc#binding: access the closure's environment
def make_counter
  count = 0
  lambda { count += 1 }
end
c = make_counter
c.call  # 1
# c.binding.eval("count")  # 1 (peek at captured var)

# Binding for eval in a specific context
b = binding
x = 42
eval("x", b)  # 42

带块的自定义迭代器

包含 Enumerable 并定义 #each 即可免费获得 map、select、reduce 和 50+ 方法——这是 Ruby 的迭代器协议。each_with_object 比inject 更适合构建累加器。tap 在方法链中插入副作用(非常适合调试)。带 yield 的自定义方法让你创建自己的 DSL(with_timing、with_database 等)。块使 Ruby 的迭代和回调模式优雅。

ruby
# Define your own iterator using yield
class LinkedList
  include Enumerable  # gets map, select, etc. for free

  def each
    node = @head
    while node
      yield node.value
      node = node.next
    end
  end
end

# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
  hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}

# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
         .tap { |arr| puts "after map: #{arr}" }
         .select { |n| n > 2 }

# Custom method with block
def with_timing
  start = Time.now
  yield
  puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }
12

Enumerable 与迭代器

核心 Enumerable 方法

Enumerable 是 Ruby 最强大的混入——包含它并定义 #each 即可获得 50+ 方法。map 转换,select/reject 过滤,reduce/inject 聚合,find 返回第一个匹配,group_by/partition 聚类。符号简写(reduce(:+))是惯用的。这些方法适用于数组、哈希、范围、文件——任何有 #each 的对象。掌握 Enumerable 是惯用 Ruby 的关键。

ruby
nums = [1, 2, 3, 4, 5, 6]

# map/collect: transform
nums.map { |n| n * 2 }        # [2, 4, 6, 8, 10, 12]

# select/filter: keep matching
nums.select { |n| n.even? }   # [2, 4, 6]
nums.reject { |n| n.even? }   # [1, 3, 5]

# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n }  # 21
nums.reduce(:+)                        # 21 (symbol shorthand)
nums.reduce(:*)                        # 720

# find/detect: first match
nums.find { |n| n > 3 }       # 4

# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}

# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]

# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_a

惰性求值

lazy 将可枚举转换为惰性的——值仅在需要时计算。这实现了无限序列,并避免为只需要少量结果的链计算整个集合。没有 lazy,map/select 创建中间数组。有 lazy,管道一次拉取一个值。对大型/无限数据集或昂贵转换使用 lazy。权衡:lazy 有每元素开销,因此对小集合更慢。

ruby
# Lazy: evaluate on demand (infinite sequences possible)
require 'prime'

# Without lazy: infinite loop!
# primes = (1..Float::INFINITY).select(&:prime?).first(5)

# With lazy: works!
primes = (1..Float::INFINITY).lazy.select(&:prime?).first(5)
# [2, 3, 5, 7, 11]

# Chain without intermediate arrays
(1..1_000_000).lazy
  .map { |n| n * 2 }
  .select { |n| n > 1_000_000 }
  .first(10)  # only computes what's needed

# Build a lazy enumerator
enum = Enumerator::Lazy.new(1..Float::INFINITY) do |yielder, n|
  yielder << n if n.prime?
end
enum.first(5)  # [2, 3, 5, 7, 11]

Enumerator 与外部迭代

Enumerator 将迭代包装为对象——你可以手动调用 .next(外部迭代)而非使用块(内部迭代)。这实现了暂停/恢复迭代、窥探和创建无限序列。带块的 Enumerator.new 让你构建自定义迭代器(生成器)。大多数 Enumerable 方法在不带块调用时返回 Enumerator:[1,2,3].map 返回你可以链式的 Enumerator。

ruby
# Enumerator: an iterator object you can control
enum = [10, 20, 30].each
enum.next  # 10
enum.next  # 20
enum.next  # 30
enum.next  # StopIteration

# External iteration with loop (handles StopIteration)
enum = [1, 2, 3].each
loop do
  puts enum.next
end

# Create custom Enumerator
fib = Enumerator.new do |yielder|
  a, b = 0, 1
  loop do
    yielder << a
    a, b = b, a + b
  end
end
fib.first(10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Convert to array
fib.take(10).to_a

排序与比较

sort 接受比较两个元素的块(返回 -1、0 或 1)。sort_by 更高效——它每个元素计算一次排序键(Schwartzian 变换)而非每次比较都计算。复杂键使用 sort_by。<=>(太空船)运算符返回 -1/0/1,是 Ruby 排序的基础。min/max/min_by/max_by 查找极值。包含 Comparable 并定义 <=> 以获得对象的自然排序。

ruby
arr = [3, 1, 4, 1, 5, 9, 2, 6]

# sort: returns new array
arr.sort                    # [1, 1, 2, 3, 4, 5, 6, 9]
arr.sort { |a, b| b <=> a } # descending (9, 6, 5, ...)

# sort_by: more efficient (Schwartzian transform)
arr.sort_by { |n| -n }      # descending
words = ["banana", "apple", "cherry"]
words.sort_by { |w| w.length }  # ["apple", "banana", "cherry"]

# min/max
arr.min        # 1
arr.max        # 9
arr.minmax     # [1, 9]
arr.min_by { |n| n.abs }  # 1

# min/max with block
people.max_by { |p| p[:age] }

# <=> (spaceship): -1, 0, 1
1 <=> 2   # -1
2 <=> 2   # 0
3 <=> 2   # 1

哈希迭代与转换

哈希也是 Enumerable。each/each_pair 迭代键值对。transform_keys/transform_values(Ruby 2.5+)创建具有修改键/值的新哈希。select/reject 过滤到新哈希。merge 组合哈希(块解决冲突)。group_by 从数组构建哈希。哈希迭代是 Ruby 数据处理的基础——掌握这些以获得干净、富有表现力的数据操作。

ruby
hash = { a: 1, b: 2, c: 3 }

# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }

# Transform keys/values
hash.transform_keys { |k| k.to_s }  # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}

# Filter
hash.select { |k, v| v > 1 }  # {b:2, c:3}
hash.reject { |k, v| v > 1 }  # {a:1}

# Merge
{ a: 1 }.merge({ b: 2 })  # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new }  # {a:3}

# Invert (values become keys)
{ a: 1, b: 2 }.invert  # {1=>:a, 2=>:b}

# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}
13

Gem 与 Bundler

Gem 基础

Gem 是 Ruby 包(库)。gem install 管理系统 gem。对于项目,使用 Bundler 配合 Gemfile 固定版本并管理依赖。版本说明符:'~> 1.4'(悲观,允许补丁),'>= 5.0'(乐观)。组(:development、:test、:production)让你每个环境只加载需要的 gem。require: false 表示 Bundler 不会自动 require 它(你在需要时手动 require)。

ruby
# Install a gem
$ gem install rails
$ gem install rails -v 7.0.0
$ gem install rails --pre  # pre-release

# List installed gems
$ gem list
$ gem list rails

# Uninstall
$ gem uninstall rails

# Gemfile: project dependencies
# Gemfile
source 'https://rubygems.org'
gem 'rails', '7.0.0'
gem 'pg', '~> 1.4'  # pessimistic: >= 1.4, < 2.0
gem 'puma', '>= 5.0'  # optimistic: >= 5.0
gem 'rspec', group: :test  # group-specific
gem 'pry', require: false  # don't auto-require

# Groups
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :production do
  gem 'newrelic_rpm'
end

Bundler 命令

Bundler 确保你的项目使用指定的确切 gem 版本。bundle install 读取 Gemfile 并写入 Gemfile.lock(确切版本以实现可重现——提交这个!)。bundle exec 用正确的 gem 版本运行命令(避免冲突)。bundle update 更改版本(小心——可能破坏东西)。始终使用 bundle exec 运行 rake/rspec/rails 以确保正确的 gem 加载。Gemfile.lock 使部署可重现。

ruby
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production  # skip group

# Update gems
$ bundle update              # update all
$ bundle update rails        # update specific gem
$ bundle outdated            # show outdated gems

# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate

# Check for dependency issues
$ bundle check
$ bundle doctor

# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!

# Add/remove gems
$ bundle add rspec
$ bundle remove rspec

# Clean old gems
$ bundle clean

Gemspec(创建 Gem)

.gemspec 文件定义 gem 的元数据和依赖。spec.files 列出包含的文件;require_paths 告诉 Ruby 在哪里找到它们。add_dependency 用于运行时依赖,add_development_dependency 用于测试/构建依赖。required_ruby_version 强制 Ruby 版本。gem build 创建 .gem 包;gem install 本地安装。用 gem push 发布到 rubygems.org。结构:lib/ 用于代码,spec/ 用于测试。

ruby
# my_gem.gemspec
Gem::Specification.new do |spec|
  spec.name = "my_gem"
  spec.version = "0.1.0"
  spec.summary = "A useful Ruby gem"
  spec.description = "Longer description..."
  spec.authors = ["Alice"]
  spec.email = ["[email protected]"]
  spec.homepage = "https://github.com/alice/my_gem"
  spec.license = "MIT"

  spec.files = Dir["lib/**/*.rb"] + ["README.md"]
  spec.require_paths = ["lib"]

  spec.required_ruby_version = ">= 3.0"

  spec.add_dependency "httparty", "~> 0.21"
  spec.add_development_dependency "rspec", "~> 3.12"
end

# Directory structure:
# my_gem/
#   lib/my_gem.rb        (main file)
#   lib/my_gem/version.rb
#   my_gem.gemspec
#   Gemfile
#   spec/                (tests)

# Build and install
$ gem build my_gem.gemspec  # creates my_gem-0.1.0.gem
$ gem install ./my_gem-0.1.0.gem

Rake 任务

Rake 是 Ruby 的 make——任务运行器。用 task :name do ... end 定义任务。命名空间分组相关任务。文件任务有依赖(源更改时重建)。sh 运行 shell 命令。Rake 用于测试、构建、部署和数据库任务(Rails 大量使用它)。用 rake task_name 运行。默认任务在你只输入 rake 时运行。用 rake greet[Alice] 传递参数。

ruby
# Rakefile
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec

# Custom task
task :greet, [:name] do |t, args|
  puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]

# Namespace
namespace :db do
  task :migrate do
    puts "migrating..."
  end
  task :seed do
    puts "seeding..."
  end
end
# $ rake db:migrate

# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
  sh "cp #{t.source} #{t.name}"
end

# Multi-line task
task :deploy do
  sh 'git push heroku main'
  sh 'heroku run rails db:migrate'
end

Rbenv 与 RVM(Ruby 版本)

rbenv 和 RVM 在一台机器上管理多个 Ruby 版本。rbenv 轻量(shim);RVM 更重(覆盖 shell 命令)。.ruby-version 文件(提交)确保每个人都使用相同的 Ruby 版本。Gemset(RVM)或 bundle config path 隔离项目 gem。对于生产,使用 bundle config set path 将 gem 本地安装到项目,避免系统 gem 污染。始终在 .ruby-version 中固定 Ruby 版本。

ruby
# rbenv: lightweight Ruby version manager
$ rbenv install 3.2.0        # install a version
$ rbenv global 3.2.0         # set global version
$ rbenv local 3.1.0          # set per-project (.ruby-version)
$ rbenv versions             # list installed

# .ruby-version file (committed to project)
# 3.2.0

# RVM: alternative version manager
$ rvm install 3.2.0
$ rvm use 3.2.0
$ rvm gemset create myapp    # isolated gem sets
$ rvm use 3.2.0@myapp

# Bundler config
$ bundle config set path 'vendor/bundle'  # install locally
$ bundle config set without 'development test'  # for production

# Check versions
$ ruby -v
$ gem -v
$ bundle -v
14

Rails 基础

MVC 结构

Rails 是 MVC 框架:模型(ActiveRecord)处理数据,控制器(ActionController)处理 HTTP 请求,视图(ActionView)渲染响应。resources 自动生成 7 个 RESTful 路由。路由将 URL 映射到控制器动作。约定优于配置:将模型命名为 User,控制器命名为 UsersController,Rails 就会连接一切。这种结构是每个 Rails 应用的骨干。

ruby
# Rails follows Model-View-Controller
# app/
#   models/    (ActiveRecord: data + business logic)
#   controllers/ (ActionController: handle requests)
#   views/     (ActionView: render responses)
#   helpers/   (view helpers)
# config/routes.rb (URL routing)

# Route → Controller → Model → View
# GET /users → UsersController#index → User.all → index.html.erb

# config/routes.rb
Rails.application.routes.draw do
  resources :users  # generates 7 RESTful routes
  # GET    /users          → index
  # GET    /users/new      → new
  # POST   /users          → create
  # GET    /users/:id      → show
  # GET    /users/:id/edit → edit
  # PATCH  /users/:id      → update
  # DELETE /users/:id      → destroy

  root 'pages#home'  # root path
  get 'about', to: 'pages#about'  # custom route
end

ActiveRecord 模型

ActiveRecord 是 Rails 的 ORM——模型映射到数据库表。validates 强制数据完整性。关联(has_many、belongs_to、has_one)定义关系。回调(before_save、after_create)挂钩到生命周期。作用域是可复用的查询片段。ActiveRecord 使用约定:User 模型 → users 表,created_at/updated_at 列。它是 Rails 的核心——掌握它以进行有效的 Rails 开发。

ruby
# app/models/user.rb
class User < ApplicationRecord
  # Validations
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, uniqueness: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }

  # Associations
  has_many :posts, dependent: :destroy
  has_one :profile
  belongs_to :company
  has_and_belongs_to_many :tags

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes (reusable queries)
  scope :active, -> { where(active: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  private
  def normalize_email
    self.email = email.downcase
  end
end

# Usage
User.create(name: "Alice", email: "[email protected]")
User.active.recent
User.find(1)
User.where("age > ?", 18)

控制器与强参数

控制器处理 HTTP 请求并协调模型/视图。before_action 运行过滤器(认证、加载资源)。强参数(permit)防止批量赋值漏洞——只有白名单字段可以设置。redirect_to 将用户发送到其他地方;render 显示视图。@instance 变量在视图中可用。RESTful 动作(index、show、new、create、edit、update、destroy)是约定的。

ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: %i[show edit update destroy]

  def index
    @users = User.all
  end

  def show; end  # @user set by before_action

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User created'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  # Strong parameters: whitelist allowed fields
  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

视图与辅助方法

ERB(嵌入式 Ruby)是 Rails 的默认模板:<%= %> 输出,<% %> 执行。link_to/button_to 生成 HTML 链接/表单。form_with 构建绑定到模型的表单。Partials(_form.html.erb)是用 render 渲染的可复用视图片段。路径辅助方法(new_user_path、user_path(user))从路由生成 URL。辅助方法保持视图整洁。对于复杂逻辑,使用视图辅助方法(app/helpers/)或装饰器。

ruby
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= link_to "New User", new_user_path %>
<ul>
  <% @users.each do |user| %>
    <li>
      <%= link_to user.name, user_path(user) %>
      (<%= user.email %>)
      <%= link_to "Edit", edit_user_path(user) %>
      <%= button_to "Delete", user, method: :delete %>
    </li>
  <% end %>
</ul>

<!-- Partials: reusable view fragments -->
<!-- app/views/users/_form.html.erb -->
<%= form_with model: user do |form| %>
  <% if user.errors.any? %>
    <div class="errors">
      <% user.errors.full_messages.each do |msg| %>
        <p><%= msg %></p>
      <% end %>
    </div>
  <% end %>
  <%= form.text_field :name %>
  <%= form.email_field :email %>
  <%= form.submit %>
<% end %>

<!-- Render partial -->
<%= render "form", user: @user %>

迁移与数据库

迁移随时间演进数据库模式,受版本控制。create_table 定义表;add_column/remove_column 修改它们。t.references 创建外键。t.timestamps 添加 created_at/updated_at。db:migrate 应用待处理的迁移;db:rollback 撤销最后一个。schema.rb 文件是当前模式的权威来源(自动生成)。切勿直接编辑 schema.rb——使用迁移。这使数据库更改在环境之间可重现。

ruby
# Generate a migration
$ rails generate migration CreateUsers name:string email:string

# db/migrate/20240101_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false, index: { unique: true }
      t.integer :age, default: 0
      t.text :bio
      t.references :company, foreign_key: true

      t.timestamps  # created_at, updated_at
    end
  end
end

# Run migrations
$ rails db:migrate
$ rails db:rollback      # undo last migration
$ rails db:seed          # load seed data
$ rails db:reset         # drop + create + migrate + seed

# Schema file (don't edit manually)
# db/schema.rb reflects current database structure

# Add a column later
$ rails generate migration AddAgeToUsers age:integer
# creates: add_column :users, :age, :integer
15

RSpec 测试

基本语法(describe、it、expect)

RSpec 是 Ruby 的主流测试框架。describe 分组相关测试;it 定义单个测试。expect(...).to / not_to 进行断言。let 定义惰性记忆化变量(首次访问时计算一次)。context 是 describe 的别名,用于分支(when...)。shoulda-matchers gem 为常见 Rails 验证/关联提供单行语法。测试放在 spec/ 中镜像 app/ 结构。

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: "Alice", email: "[email protected]") }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq("Alice")
    end

    # Multiple expectations with context
    context 'when name is blank' do
      let(:user) { User.new(name: "") }
      it 'is invalid' do
        expect(user).not_to be_valid
      end
    end
  end

  # One-liner syntax
  it { should validate_presence_of(:email) }
  it { should have_many(:posts) }
end

模拟与存根

存根(allow)替换方法返回值;模拟(expect)验证方法被调用。Double 是用于测试的假对象(比真实对象快)。使用存根将被测代码与外部依赖(API、数据库)隔离。使用模拟验证交互。过度模拟使测试脆弱——足够快时优先使用真实对象。FactoryBot 创建测试数据;使用 create(保存到数据库)或 build(仅内存)。

ruby
RSpec.describe PaymentService do
  let(:user) { create(:user) }

  it 'charges the card' do
    # Stub: replace a method's return value
    allow(user).to receive(:premium?).and_return(true)

    # Mock: expect a method to be called
    expect(Stripe::Charge).to receive(:create).with(
      amount: 1000, currency: 'usd'
    ).and_return(double(id: 'ch_123'))

    service = PaymentService.new(user)
    result = service.charge(10)
    expect(result).to eq('ch_123')
  end

  it 'handles failure' do
    # Stub to raise an error
    allow(Stripe::Charge).to receive(:create)
      .and_raise(Stripe::StripeError.new("card declined"))

    service = PaymentService.new(user)
    expect { service.charge(10) }.to raise_error(PaymentError)
  end
end

# Double: test stand-in for an object
fake_card = double('Card', last4: '4242', brand: 'Visa')

FactoryBot 与夹具

FactoryBot 创建具有合理默认值的测试对象。Trait 创建变体(:admin、:inactive)。序列生成唯一值(电子邮件)。create 持久化到数据库;build 不会。create_list 创建多个。通过传递覆盖任何属性。工厂比夹具(YAML)更灵活但更慢(数据库写入)。使用 build_stubbed 进行不触及数据库的快速测试。保持工厂简单——复杂工厂表明复杂模型。

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Alice" }
    email { "[email protected]" }
    age { 30 }
    active { true }

    # Traits: variations
    trait :admin do
      role { "admin" }
    end

    trait :inactive do
      active { false }
    end

    # Associations
    company { association(:company) }

    # Sequences for unique values
    sequence(:email) { |n| "user#{n}@example.com" }
  end
end

# Usage in specs
let(:user) { create(:user) }           # saved to DB
let(:admin) { create(:user, :admin) }  # with trait
let(:user) { build(:user) }            # not saved
let(:users) { create_list(:user, 5) }  # 5 users

# Override attributes
let(:alice) { create(:user, name: "Alice") }

Before/After 钩子

before 钩子运行设置代码:before(:each)(最常见)在每个测试之前,before(:all) 每组一次。after 钩子清理。DatabaseCleaner 管理测试数据库状态(事务用于速度,截断用于彻底)。优先使用 let 而非 before(:each)——let 是惰性的(仅在使用时计算)且记忆化的,而 before 即使测试不需要也会运行。对于必须发生的副作用(日志记录、时间冻结)使用 before。

ruby
RSpec.describe User do
  before(:all) do
    # Runs once before all tests in this group
    @shared_data = load_expensive_data
  end

  before(:each) do  # or just 'before'
    # Runs before each test
    @user = User.create(name: "test")
  end

  after(:each) do
    # Runs after each test (cleanup)
    User.delete_all
  end

  after(:all) do
    # Runs once after all tests
    @shared_data = nil
  end

  # Database cleaner for transactional tests
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
end

# Use before for setup, after for cleanup
# Prefer let over before(:each) for lazy evaluation

集成与系统测试

请求规范通过 HTTP 测试整个堆栈(路由 → 控制器 → 模型 → 视图)。系统规范(Capybara)驱动真实浏览器——填写表单、点击、检查页面内容。单元测试(模型规范)快速且隔离;集成/系统测试较慢但能捕获连接 bug。使用测试金字塔:许多快速单元测试,较少集成测试,最少系统测试。have_http_status 检查响应代码;visit/fill_in/click_button 驱动浏览器。

ruby
# spec/requests/users_spec.rb (integration)
require 'rails_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users'
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob' } }
      expect(response).to have_http_status(:created)
    end
  end
end

# spec/system/login_spec.rb (browser tests)
require 'rails_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'secret')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'secret'
    click_button 'Log in'
    expect(page).to have_content('Welcome')
  end
end
16

文件与目录操作

读取文件

File.read 将整个文件加载到内存——适用于小文件。File.foreach 逐行读取——对大文件必不可少(不会耗尽内存)。File.open 带块自动关闭文件(RAII)。readlines 返回行数组(带换行符——使用 chomp)。binread 用于二进制文件。如果文件可能不存在,读取前始终检查 File.exist?,或 rescue Errno::ENOENT。

ruby
# Read entire file
content = File.read("data.txt")

# Read line by line (memory efficient)
File.foreach("large.txt") do |line|
  puts line.chomp  # chomp removes trailing newline
end

# Read all lines into array
lines = File.readlines("data.txt")

# With a block (auto-closes)
File.open("data.txt") do |f|
  f.each_line { |line| puts line }
end

# Read with options
File.read("data.txt", encoding: "utf-8")
File.binread("image.png")  # binary mode

# Check existence
File.exist?("data.txt")
File.file?("data.txt")   # is it a regular file?
File.directory?("path")  # is it a directory?

写入文件

File.write 是写入文件的最简单方式(默认覆盖)。使用 mode: 'a' 追加。File.open 带块确保即使发生异常文件也会关闭。puts 添加换行符;write 不会。<< 是 write 的别名(Ruby 中常见)。对于日志,打开一次并多次写入(为性能缓冲)。始终关闭文件或使用块形式以避免资源泄漏。

ruby
# Write (overwrites)
File.write("output.txt", "Hello, World!")

# Append
File.write("output.txt", "More text\n", mode: "a")

# With a block (buffered, auto-closes)
File.open("log.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
  f << "appended"  # << is an alias for write
end

# Binary write
File.binwrite("data.bin", binary_data)

# Modes: r (read), w (write/truncate), a (append),
#        r+ (read/write), w+ (read/write/truncate),
#        b (binary, Windows)

# Flush buffer
f = File.open("log.txt", "w")
f.write("data")
f.flush  # write without closing
f.close

目录操作

Dir.glob 带模式查找文件(** 用于递归)。FileUtils 提供健壮的文件操作:mkdir_p 创建嵌套目录,cp_r 递归复制,rm_rf 强力移除(小心!)。Dir.chdir 更改工作目录(使用块形式临时更改)。Dir.entries 包含 . 和 ..;glob 不会。跨平台安全优先使用 FileUtils 而非手动 File 操作。

ruby
require 'fileutils'

# List directory contents
Dir.entries(".")  # [".", "..", "file.txt", ...]
Dir.glob("*.rb")  # ["script.rb", "test.rb"]
Dir.glob("**/*.rb")  # recursive
Dir.glob("src/**/*.{rb,erb}")  # multiple extensions

# Create directories
Dir.mkdir("new_dir")
Dir.mkdir("nested/path") rescue Errno::ENOENT  # fails if parent missing
FileUtils.mkdir_p("nested/path/deep")  # creates all parents

# Remove
Dir.rmdir("empty_dir")  # only works if empty
FileUtils.rm_rf("dir")  # recursive force (dangerous!)

# Copy/Move
FileUtils.cp("a.txt", "b.txt")
FileUtils.mv("old.txt", "new.txt")
FileUtils.cp_r("src_dir", "dest_dir")  # recursive

# Change directory
Dir.chdir("/tmp") { puts Dir.pwd }  # temporarily
Dir.chdir("/tmp")  # permanently for process

# Check if directory
File.directory?("path")
File.exist?("path")

Pathname 与 Tempfile

Pathname 是文件路径的面向对象包装器——比字符串操作更干净。它提供 dirname、basename、extname、join 和文件检查作为方法。Tempfile 创建自动删除的临时文件(使用块形式)。Dir.mktmpdir 创建临时目录。使用这些进行干净、安全的路径处理和临时文件管理。Pathname 跨平台安全组合路径(处理 / vs \)。

ruby
require 'pathname'
require 'tempfile'

# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname   # #<Pathname:/home/user/docs>
path.basename  # #<Pathname:file.txt>
path.extname   # ".txt"
path.parent    # #<Pathname:/home/user/docs>
path.join("sub", "file.rb")  # /home/user/docs/sub/file.rb

path.exist?
path.directory?
path.file?
path.readable?

# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
  f.write("temporary data")
  f.rewind
  puts f.read
end  # file deleted after block

# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink  # delete

# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
  # work in dir
end  # dir deleted after

CSV 与 JSON

CSV 和 JSON 内置于 Ruby 标准库。CSV.foreach 逐行读取(节省内存);CSV.read 加载所有内容。headers: true 将第一行视为列名。JSON.parse 将 JSON 转换为 Ruby 哈希/数组;to_json 序列化 Ruby 对象。symbolize_names 提供符号键(更干净)。对于 YAML,使用 require 'yaml' 和 YAML.load_file。这些是 Ruby 脚本和 Web 应用中数据交换的基础。

ruby
require 'csv'
require 'json'

# CSV reading
CSV.foreach("data.csv", headers: true) do |row|
  puts row['name']  # access by header
  puts row[0]       # access by index
end

# CSV reading all at once
rows = CSV.read("data.csv", headers: true)
rows.first['name']  # first row's name

# CSV writing
CSV.open("output.csv", "w") do |csv|
  csv << ["name", "age"]  # header
  csv << ["Alice", 30]
  csv << ["Bob", 25]
end

# JSON
data = { name: "Alice", age: 30 }
json = data.to_json  # '{"name":"Alice","age":30}'
parsed = JSON.parse(json)  # {"name"=>"Alice", "age"=>30}

# JSON with symbols
JSON.parse(json, symbolize_names: true)  # {name: "Alice", age: 30}

# Read/write JSON files
File.write("data.json", data.to_json)
loaded = JSON.parse(File.read("data.json"))
17

编码与字符串内部

字符串编码

Ruby 字符串携带其编码(通常为 UTF-8)。force_encoding 将字节重新解释为不同编码(无转换——当你知道字节已经是该编码时使用)。encode 实际在编码之间转换。valid_encoding? 检查字节对字符串编码是否有效。编码问题导致可怕的 Encoding::CompatibilityError。始终知道数据的编码;默认为 UTF-8。

ruby
# Ruby strings have an encoding
"hello".encoding  # #<Encoding:UTF-8>
"café".encoding   # #<Encoding:UTF-8>

# Default external encoding (for file I/O)
Encoding.default_external  # #<Encoding:UTF-8>

# Force an encoding (reinterprets bytes, doesn't convert)
bytes = "café".bytes  # [99, 97, 102, 195, 169]
latin1 = bytes.pack("C*").force_encoding("ISO-8859-1")
# "caf" + é (as single byte)

# Convert encoding (transcodes)
utf8 = "café"
latin1 = utf8.encode("ISO-8859-1")  # converts
back = latin1.encode("UTF-8")        # converts back

# Check if valid
"abc".valid_encoding?  # true
"\xff".valid_encoding?  # false (invalid byte)

# Common encodings: UTF-8, ASCII, ISO-8859-1, Windows-1252
# Always use UTF-8 unless you have a specific reason

编码转换与 I/O

文件 I/O 使用 Encoding.default_external 进行读取。用 encoding: 选项按文件指定编码。'source:target' 语法(ISO-8859-1:UTF-8)以源编码读取并转换为目标编码。设置 default_internal 使 Ruby 自动将所有读取的字符串转换为该编码。对于 Web 应用,一切都应为 UTF-8。处理遗留数据时,显式指定编码以避免损坏。

ruby
# Read a file with specific encoding
content = File.read("data.txt", encoding: "ISO-8859-1")
# content.encoding is ISO-8859-1

# Read and convert to UTF-8
content = File.read("data.txt", encoding: "ISO-8859-1:UTF-8")
# The "source:target" syntax converts while reading

# Write with specific encoding
File.write("output.txt", "café", encoding: "UTF-8")

# Open with encoding
File.open("data.txt", "r:ISO-8859-1") do |f|
  f.read  # ISO-8859-1 encoded string
end

# Convert while opening
File.open("data.txt", "r:ISO-8859-1:UTF-8") do |f|
  f.read  # UTF-8 encoded string
end

# Set default encodings
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

字符串方法深入

Ruby 字符串有丰富的方法。检查(length、include?、start_with?)检查属性。转换方法返回新字符串(Ruby 中字符串是可变的,但这些不会修改)。子字符串访问使用 [start, length] 或范围。sub 替换第一个匹配;gsub 替换所有(支持正则和块)。split/join 在字符串和数组之间转换。注意:Ruby 3.0+ frozen_string_literal 编译指示使字符串不可变以提升性能——使用 << 或 + 构建。

ruby
s = "Hello, World"

# Inspection
s.length    # 12
s.empty?    # false
s.include?("World")  # true
s.start_with?("Hello")  # true
s.end_with?("World")  # true

# Transformation (return new string)
s.upcase     # "HELLO, WORLD"
s.downcase   # "hello, world"
s.capitalize # "Hello, world"
s.swapcase   # "hELLO, wORLD"
s.reverse    # "dlroW ,olleH"
s.strip      # remove leading/trailing whitespace
s.chomp(",") # remove trailing substring
s.chop       # remove last char

# Substrings
s[0, 5]      # "Hello" (start, length)
s[7..11]     # "World" (range)
s[-5..]      # "World" (negative index)

# Replace
s.sub("World", "Ruby")    # first match
s.gsub("o", "0")          # all matches
s.gsub(/\w+/) { |w| w.upcase }  # with block

# Split/Join
"a,b,c".split(",")  # ["a", "b", "c"]
["a", "b"].join("-")  # "a-b"

冻结字符串与性能

frozen_string_literal: true(文件顶部的魔术注释)使所有字符串字面量不可变——这是一种性能优化(冻结字符串可以共享内存)并防止意外修改 bug。对于构建字符串,使用 <<(原地追加,O(n))而非 +=(每次创建新字符串,O(n²))。join 对数组最干净。StringIO 像文件一样但写入字符串——适用于构建复杂输出。Ruby 3.x 鼓励默认冻结字符串。

ruby
# frozen_string_literal: true  (at top of file)
# Makes all string literals in the file frozen (immutable)

# Mutable vs frozen
s = "hello"
s << " world"  # "hello world" (mutates)

f = "hello".freeze
# f << " world"  # Error: can't modify frozen String

# Why freeze? Performance: frozen strings share memory
# "abc".freeze is the same object everywhere
"a".freeze.equal?("a".freeze)  # true (same object)

# Building strings efficiently
# BAD: creates many intermediate strings
result = ""
items.each { |i| result += i.to_s }  # O(n²)

# GOOD: use << or join
result = ""
items.each { |i| result << i.to_s }  # O(n)
result = items.map(&:to_s).join  # cleanest

# StringIO for buffered building
require 'stringio'
io = StringIO.new
io << "line 1\n"
io << "line 2\n"
result = io.string

符号与字符串

符号(:name)是不可变的、单例标识符——内存中永远只有一个 :foo。字符串是可变文本数据,有多个实例。哈希键(更快的相等性检查)、方法名和类枚举值使用符号。实际文本使用字符串。符号作为哈希键和比较略快。在现代 Ruby(2.2+)中,符号可以被垃圾回收,因此旧的'符号内存泄漏'担忧已消失。Rails 大量使用符号作为键和状态值。

ruby
# Symbol: immutable, reusable identifier
:hello
:world
status = :active

# String: mutable, can have many instances
"hello"
"world"
status = "active"

# Key difference: identity
"hello".equal?("hello")  # false (different objects)
:hello.equal?(:hello)    # true (same object, singleton)

# Memory: symbols are singletons
1000.times { :foo }  # one :foo object
1000.times { "foo" }  # 1000 "foo" objects (unless frozen)

# Use symbols for:
# - Hash keys (faster comparison)
# - Method names (send(:method_name))
# - Enum-like values (:active, :pending, :closed)
# - Identifiers (not text data)

# Use strings for:
# - Text data (names, content)
# - Things that change

# Conversion
:hello.to_s  # "hello"
"hello".to_sym  # :hello
"hello world".to_sym  # :"hello world" (valid but ugly)
18

模块与混入

模块基础

模块有两个目的:命名空间(分组相关代码,防止名称冲突)和混入(无继承共享行为)。模块方法(def self.method)在模块上调用。实例方法(def method)用于混入类。模块不能被实例化。使用模块为类命名空间(MyApp::User)并组织常量和实用函数。这是 Ruby 对多重继承的替代方案。

ruby
# Module: a namespace + collection of methods
module MathUtils
  PI = 3.14159

  def self.circle_area(radius)  # module method
    PI * radius ** 2
  end

  def square(x)  # instance method (for mixins)
    x * x
  end
end

# Access constants and module methods
MathUtils::PI  # 3.14159
MathUtils.circle_area(5)  # 78.54

# Namespacing classes
module MyApp
  class User
    # MyApp::User
  end
end

# Prevent instantiation (modules can't be instantiated)
# MathUtils.new  # NoMethodError

Include 与 Extend 与 Prepend

三种混入模块的方式:include(添加实例方法,在查找中位于类之下),extend(添加类方法),prepend(添加实例方法,位于类之上——可以包装/重写)。prepend 对于 before/after 钩子很强大(调用 super 调用原始方法)。方法查找:prepend → 类 → include → 父类。普通混入使用 include,需要包装现有方法时使用 prepend,类级功能使用 extend。

ruby
module Greetable
  def greet
    "Hello from #{self.class}"
  end
end

class User
  include Greetable  # adds as instance methods
end
User.new.greet  # "Hello from User"

class Service
  extend Greetable  # adds as class methods
end
Service.greet  # "Hello from Service"

# Method lookup order:
# prepend → class → include → super
module Logging
  def save
    puts "before save"
    super  # calls the original save
    puts "after save"
  end
end

class Record
  prepend Logging  # Logging#save runs first
  def save; puts "saving"; end
end
Record.new.save
# before save / saving / after save

Enumerable 混入

包含 Enumerable 并定义 #each 即可获得 map、select、reduce、sort、min、max 和 40+ 更多方法——这是 Ruby 的迭代器协议。包含 Comparable 并定义 <=> 即可获得 <、>、==、between?、clamp 和排序支持。这些混入是 Ruby 集合如此强大的原因。任何表示集合或具有自然排序的类都应包含这些。这是组合优于继承。

ruby
# Include Enumerable + define #each = 50+ methods free
class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each
    @songs.each { |song| yield song }
  end
end

playlist = Playlist.new(["Song A", "Song B", "Song C"])
playlist.map { |s| s.upcase }   # ["SONG A", "SONG B", "SONG C"]
playlist.select { |s| s.include?("A") }  # ["Song A"]
playlist.reduce(:+)            # "Song ASong BSong C"
playlist.sort                  # ["Song A", "Song B", "Song C"]
playlist.first(2)              # ["Song A", "Song B"]
playlist.include?("Song B")    # true
playlist.count                 # 3

# Comparable: define <=> for natural ordering
class Temperature
  include Comparable
  attr_reader :celsius

  def initialize(c) @celsius = c end
  def <=>(other) celsius <=> other.celsius end
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2   # true (uses <=>)
t1 == t2  # false

单例方法与类方法

单例方法属于一个特定对象。类方法只是类对象上的单例方法。'class << self' 打开单例类(本征类)以干净地定义多个类方法。单例模式使用类变量持有一个实例。理解单例类是 Ruby 对象模型的关键——每个对象都有一个持有其唯一方法的单例类。这实现了每对象自定义和元编程。

ruby
# Singleton method: defined on one object only
str = "hello"
def str.shout
  upcase + "!"
end
str.shout  # "HELLO!"
# "world".shout  # NoMethodError (only str has it)

# Class methods are singleton methods on the class
class Calculator
  def self.add(a, b)  # singleton method on Calculator
    a + b
  end
end
Calculator.add(1, 2)  # 3

# Singleton class (eigenclass): where singleton methods live
class Calculator
  class << self  # opens the singleton class
    def multiply(a, b) a * b end
    def divide(a, b) a / b end
  end
end
Calculator.multiply(2, 3)  # 6

# Singleton pattern
class Logger
  @instance = Logger.new
  class << self
    attr_reader :instance
  end
end
Logger.instance.equal?(Logger.instance)  # true

细化(范围猴子补丁)

细化(Ruby 2.1+)允许范围猴子补丁——向现有类添加方法,但仅在你显式 'using' 细化的地方。这比全局猴子补丁(可能破坏其他代码)更安全。细化按范围(文件、类、方法)激活。它们适用于添加便利方法而不污染全局命名空间。由于性能和一些范围怪癖,它们比应有的更少见,但它们是扩展核心类的'正确'方式。

ruby
# Refinement: limited monkey patching
module StringRefinements
  refine String do
    def shout
      upcase + "!!!"
    end

    def palindrome?
      downcase == downcase.reverse
    end
  end
end

# Without using: shout is undefined
# "hello".shout  # NoMethodError

# With using: refinement is active in this scope
using StringRefinements
"hello".shout        # "HELLO!!!"
"racecar".palindrome?  # true

# Scoped to file or module
class App
  using StringRefinements
  def greet
    "hi".shout  # works here
  end
end
# "hi".shout  # NoMethodError (outside App)

# Safer than global monkey patching
# Refinements only affect code that opts in
19

块 Proc Lambda 深入

块基础

块是使用 { } 或 do...end 传递给方法的匿名闭包。yield 从方法内调用块。块可以通过 |var| 接受参数。方法可以用 block_given? 检查是否传递了块。块是 Ruby 迭代器模式和 DSL 的基础。它们从封闭作用域捕获变量(闭包)。

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }      # Single-line
[1, 2, 3].each do |n|              # Multi-line
  puts n
end

# yield invokes the block
def greet
  print "Hello, "
  yield          # Calls the block
  puts "!"
end

greet { print "World" }   # Hello, World!

# yield with arguments
def calculate(a, b)
  yield(a + b)
  yield(a * b)
end

calculate(3, 4) { |result| puts "Result: #{result}" }

Proc 与 Lambda

Proc 和 lambda 是包装块的对象,允许存储在变量中并传递。Proc 有宽松的参数检查(忽略额外参数)并从封闭方法返回。Lambda 有严格的参数检查且仅从自身返回。Proc 用于灵活性(像接受块的方法),lambda 用于具有可预测行为的匿名函数。->() {} 语法(stabby lambda)对单行很简洁。

ruby
# Proc: object wrapping a block
my_proc = Proc.new { |x| puts x * 2 }
my_proc.call(5)         # 10
my_proc.(5)             # 10 (shorthand)
my_proc[5]              # 10 (another shorthand)

# Lambda: stricter Proc
my_lambda = lambda { |x| puts x * 2 }
my_lambda = ->(x) { puts x * 2 }   # Stabby lambda

# Difference 1: argument checking
my_proc.call(1, 2, 3)    # OK (ignores extra args)
my_lambda.call(1, 2, 3)  # ArgumentError (wrong number of args)

# Difference 2: return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # Never reached
end

def lambda_test
  l = lambda { return 1 }
  l.call    # Returns 1 to the lambda, not the method
  return 2  # Reached
end

方法对象

method() 方法返回包装现有方法的 Method 对象。& 运算符将 Method 或 Proc 转换为块(反之亦然)。Symbol#to_proc 将 :upcase 转换为 { |x| x.upcase },实现了简洁的 &:symbol 语法。这是短块操作的惯用 Ruby。方法对象绑定到其接收者,因此传递时记住 self。

ruby
# Convert method to Proc with method()
class Calculator
  def add(a, b) a + b end
  def multiply(a, b) a * b end
end

calc = Calculator.new
add_proc = calc.method(:add)
puts add_proc.call(2, 3)   # 5

# Pass method as block with & operator
def apply_op(a, b, op)
  op.call(a, b)
end

puts apply_op(2, 3, calc.method(:add))       # 5
puts apply_op(2, 3, calc.method(:multiply))  # 6

# Symbol#to_proc
words = ["hello", "world"]
upcased = words.map(&:upcase)   # ["HELLO", "WORLD"]
# Equivalent to: words.map { |w| w.upcase }

# Common with &:map(&:to_i), select(&:positive?), sort_by(&:length)

闭包与变量

闭包按引用而非值捕获变量。多个闭包可以共享状态(计数器示例)。块局部变量(在参数中 ; 之后声明)遮蔽外部变量而不修改它们。这实现了累加器、生成器和记忆化等函数式模式。注意:持有引用的闭包如果不释放可能导致内存泄漏。使用此模式进行私有状态封装。

ruby
# Blocks/Procs/Lambdas capture variables (closures)
counter = 0
increment = lambda { counter += 1 }

increment.call
increment.call
puts counter   # 2

# Multiple closures sharing state
def make_counters
  count = 0
  [
    lambda { count += 1 },
    lambda { count },
    lambda { count = 0 }
  ]
end

inc, get, reset = make_counters
inc.call; inc.call
puts get.call   # 2
reset.call
puts get.call   # 0

# Block-local variables (shadow outer)
x = 10
[1, 2, 3].each do |y; x|   # x is block-local
  x = y * 2
end
puts x   # 10 (unchanged)

自定义迭代器

包含 Enumerable 并实现 each 即可让你的类免费获得所有迭代器方法(map、select、reduce、sort 等)。用 &block 传递块并调用它,或使用 yield。& 运算符将块转换为 Proc 并转换回来。这是使自定义集合可迭代的惯用方式。实现 each 进行前向迭代;添加 reverse_each 进行双向。

ruby
class Tree
  include Enumerable

  def initialize(value, children = [])
    @value = value
    @children = children
  end

  def each(&block)
    block.call(@value)
    @children.each { |child| child.each(&block) }
  end
end

tree = Tree.new(1, [
  Tree.new(2, [Tree.new(4), Tree.new(5)]),
  Tree.new(3)
])

tree.each { |v| puts v }            # 1 2 4 5 3
puts tree.map { |v| v * 2 }.inspect # [2, 4, 8, 10, 6]
puts tree.select(&:even?).inspect   # [2, 4]
puts tree.reduce(:+)                # 15

# Including Enumerable gives you map, select, reduce, etc.
20

元编程

动态方法

method_missing 拦截对未定义方法的调用,实现动态派发。始终重写 respond_to_missing? 以匹配。define_method 在运行时创建方法,适用于生成相似方法(如 ActiveRecord 的 find_by_*)。instance_variable_get/set 按名称访问实例变量。谨慎使用元编程——它使代码更难理解和调试。可能时优先使用显式定义。

ruby
class Person
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  # Method missing: catch undefined method calls
  def method_missing(name, *args, &block)
    if name.to_s =~ /^(.*)_with_prefix$/
      attr_name = $1
      "PREFIX_#{send(attr_name)}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s =~ /^(.*)_with_prefix$/ || super
  end
end

p = Person.new("Alice", 30)
puts p.name_with_prefix   # PREFIX_Alice

# Define methods dynamically
class Person
  [:home, :work, :mobile].each do |type|
    define_method("#{type}_phone") do
      instance_variable_get("@#{type}_phone")
    end

    define_method("#{type}_phone=") do |value|
      instance_variable_set("@#{type}_phone", value)
    end
  end
end

开放类与猴子补丁

Ruby 类是开放的:你可以向任何类添加方法,包括像 String 这样的内置类。这很强大但危险(猴子补丁可能破坏其他代码)。细化(Ruby 2.1+)提供范围猴子补丁:它们仅在使用该模块的文件/类中应用。优先使用细化而非全局猴子补丁以获得更安全的元编程。清楚地记录补丁并避免更改核心行为。

ruby
# Reopen existing classes (monkey patching)
class String
  def shout
    upcase + "!"
  end

  def word_count
    split.size
  end
end

puts "hello world".shout       # HELLO WORLD!
puts "one two three".word_count # 3

# Refinements: scoped monkey patches
module ShoutRefinement
  refine String do
    def shout
      upcase + "!"
    end
  end
end

class MyClass
  using ShoutRefinement

  def greet(name)
    "hello #{name}".shout   # Works here
  end
end

# "test".shout  # NoMethodError (refinement not active)

钩子与回调

Ruby 提供生命周期钩子:inherited(创建子类)、included(包含模块)、prepended(前置模块)、method_added(定义方法)、method_removed、method_undefined。这些使框架能够自动响应类更改。ActiveRecord 使用这些跟踪属性,Rails 使用它们进行路由。将钩子重写为类方法(self.inherited)用于类级事件,实例方法用于方法事件。

ruby
class Observable
  # Hook: called when class is subclassed
  def self.inherited(subclass)
    puts "#{subclass} inherits from #{self}"
  end

  # Hook: called when a module is included
  def self.included(base)
    puts "#{self} included in #{base}"
  end

  # Hook: called when a method is added
  def method_added(name)
    puts "Added method: #{name}"
  end

  # Hook: called when method is undefined
  def method_removed(name)
    puts "Removed method: #{name}"
  end
end

class Child < Observable
  def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method

# Other hooks: method_undefined, extended, prepended

eval 与 Binding

eval 将字符串作为 Ruby 代码执行,如果输入不受信任则危险(代码注入)。binding 捕获当前执行上下文(变量、self)用于稍后 eval。class_eval 在类上下文中执行代码(定义方法)。instance_eval 将 self 更改为接收者。用于 DSL 和代码生成,但避免对用户输入使用 eval。动态代码优先使用块和 define_method。如果 eval 不可避免,始终清理输入。

ruby
# eval: execute a string as Ruby code
result = eval("1 + 2 * 3")
puts result   # 7

# eval with binding (context)
def eval_in_context(code)
  x = 10
  binding   # Returns a Binding object capturing local variables
end

b = eval_in_context("binding")
puts eval("x * 2", b)   # 20

# class_eval: execute code in class context
String.class_eval do
  def palindrome?
    self == reverse
  end
end

puts "racecar".palindrome?   # true

# instance_eval: execute in instance context
"hello".instance_eval do
  puts length   # 5 (self is the string)
end

# NEVER eval user input (security risk)

反射与内省

Ruby 提供丰富的反射:methods 列出所有方法,instance_variables 列出实例变量,ancestors 显示继承链。parameters 揭示方法的参数名称和类型。respond_to? 检查对象是否响应方法。使用反射进行调试、序列化(检查属性)和构建通用工具(如 ORM)。避免过度使用反射——它绕过类型安全并使代码更难跟踪。

ruby
class User
  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def save; end
  def valid?; true; end
end

u = User.new("Alice", "[email protected]")

# Inspect methods
puts u.methods - Object.methods    # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false)       # Only this class's methods
puts User.instance_methods(false)  # [:save, :valid?, ...]

# Inspect variables
puts u.instance_variables.inspect   # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice

# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters

# Check if responds to method
puts u.respond_to?(:save)   # true
puts u.is_a?(User)          # true
21

测试(RSpec/Minitest)

RSpec 基础

RSpec 是 BDD 风格的测试框架。describe 分组相关测试,context 按条件分组,it 定义单个测试。let 创建惰性记忆化变量。expect(...).to matcher 是断言语法。常用匹配器:eq、be_valid、include、raise_error。subject + is_expected 减少样板。用 rspec(全部)或 rspec path/to/spec(特定)运行。使用工厂(FactoryBot)而非夹具作为测试数据。

ruby
# spec/spec_helper.rb
require 'rspec'
require_relative '../lib/my_app'

# spec/models/user_spec.rb
require 'spec_helper'

RSpec.describe User do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: 'Alice', email: '[email protected]') }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq('Alice')
    end
  end

  describe '#valid?' do
    context 'with valid attributes' do
      it 'is valid' do
        expect(user).to be_valid
      end
    end

    context 'without name' do
      let(:user) { User.new(email: '[email protected]') }
      it 'is invalid' do
        expect(user).not_to be_valid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end
  end

  # Multiple examples
  describe '#age' do
    subject { user.age }
    it { is_expected.to be >= 0 }
  end
end

# Run: rspec spec/models/user_spec.rb
# Run all: rspec

RSpec 模拟与存根

存根(allow)替换方法返回值;模拟(expect)验证方法被调用。double 创建测试替身(假对象)。and_return 设置返回值,and_raise 模拟错误,with 设置预期参数。谨慎使用模拟——过度模拟使测试脆弱。测试行为,而非实现。可行时优先使用真实对象;外部服务(API、电子邮件、支付网关)使用模拟。

ruby
RSpec.describe PaymentProcessor do
  let(:gateway) { double('PaymentGateway') }
  let(:processor) { PaymentProcessor.new(gateway) }

  describe '#charge' do
    it 'charges the gateway' do
      # Stub: replace method return value
      allow(gateway).to receive(:charge).and_return(success: true)

      result = processor.charge(100)
      expect(result[:success]).to be true
    end

    it 'raises on gateway error' do
      allow(gateway).to receive(:charge)
        .and_raise(GatewayError, 'Network failure')

      expect { processor.charge(100) }
        .to raise_error(GatewayError, /Network/)
    end

    it 'verifies the gateway was called' do
      # Mock: expect method to be called
      expect(gateway).to receive(:charge).with(100)
      processor.charge(100)
    end

    it 'receives multiple calls' do
      allow(gateway).to receive(:charge).and_return(true, false, true)
      expect(gateway.charge(1)).to be true
      expect(gateway.charge(2)).to be false
    end
  end
end

Minitest

Minitest 是 Ruby 的默认测试库(随 Ruby 提供)。它支持 Unit 风格(assert/refute)和 Spec 风格(must_equal)语法。setup 在每个测试之前运行。断言:assert_equal、assert_nil、assert_raises、assert_includes。Minitest 比 RSpec 快且依赖更少。使用 Minitest::Mock 进行模拟,或 Mocha gem 获得更多功能。Rails 默认使用 Minitest(ActiveSupport::TestCase)。

ruby
# test/test_helper.rb
require 'minitest/autorun'
require_relative '../lib/my_app'

# test/models/user_test.rb
require 'test_helper'

class UserTest < Minitest::Test
  def setup
    @user = User.new(name: 'Alice', email: '[email protected]')
  end

  def test_name
    assert_equal 'Alice', @user.name
  end

  def test_valid_with_attributes
    assert @user.valid?
  end

  def test_invalid_without_name
    @user.name = nil
    refute @user.valid?
    assert_includes @user.errors[:name], "can't be blank"
  end

  def test_raises_on_error
    assert_raises(ArgumentError) { User.new! }
  end
end

# Spec-style Minitest
class UserSpec < Minitest::Spec
  let(:user) { User.new(name: 'Alice') }

  it 'has a name' do
    _(user.name).must_equal 'Alice'
  end
end

# Run: rake test
# Run specific: ruby -Itest test/models/user_test.rb

测试数据与工厂

FactoryBot 生成具有默认值、变体 trait 和关联的测试对象。create 持久化到数据库;build 不会。Trait 可组合(factory :admin_with_posts, traits: [:admin, :with_posts])。工厂比夹具更灵活但更慢。夹具(YAML)更快但不太灵活。根据需要选择:简单数据用夹具,复杂关系用工厂。避免有太多 trait 的工厂——保持它们专注。

ruby
# FactoryBot (gem 'factory_bot')
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Alice' }
    email { '[email protected]' }
    age { 30 }

    trait :admin do
      role { 'admin' }
    end

    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
    factory :user_with_posts, traits: [:with_posts]
  end
end

# Usage in specs
let(:user) { create(:user) }            # Saved to DB
let(:admin) { create(:admin_user) }
let(:user_with_posts) { create(:user_with_posts) }
let(:unsaved_user) { build(:user) }     # Not saved

# Fixtures (alternative)
# test/fixtures/users.yml
# alice:
#   name: Alice
#   email: [email protected]

# In tests: users(:alice)

集成与系统测试

请求规范通过 HTTP 测试整个堆栈(路由、控制器、模型)。系统规范(Capybara)驱动真实浏览器,测试 JavaScript 交互。API 端点使用请求规范,用户流程使用系统规范。have_http_status 检查响应代码。visit/fill_in/click_button 模拟用户操作。系统测试较慢但能捕获集成 bug。用 js: true 运行 JavaScript 驱动页面的系统测试。

ruby
# spec/requests/users_spec.rb (integration)
require 'spec_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users', as: :json
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob', email: '[email protected]' } }
      expect(response).to have_http_status(201)
      expect(User.last.name).to eq('Bob')
    end
  end
end

# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'password123')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password123'
    click_button 'Log In'
    expect(page).to have_content('Welcome')
  end
end
22

线程与 Fiber

线程基础

Ruby(MRI)中的线程是由 VM 调度的绿色线程——由于 GIL(全局解释器锁),它们不会真正并行运行。但是,线程对于 I/O 并发(网络、文件操作)很有用。join 等待线程完成。value 检索返回值。对于 CPU 密集型并行,使用多进程(fork、Sidekiq)或 JRuby(无 GIL)。始终 join 线程以避免主线程退出时它们被杀死。

ruby
# Create and run threads
threads = (1..3).map do |i|
  Thread.new(i) do |n|
    puts "Thread #{n} started"
    sleep(1)
    puts "Thread #{n} finished"
  end
end

# Wait for all threads
threads.each(&:join)
puts "All done"

# Thread with return value
thread = Thread.new { 1 + 2 }
result = thread.value   # 3 (waits for completion)

# Thread status
thread = Thread.new { sleep(1) }
puts thread.status   # 'run', 'sleep', false (finished), nil (error)
thread.join

# Current thread
Thread.current
Thread.list   # All threads
Thread.main   # Main thread

线程同步

Mutex.synchronize 确保一次只有一个线程执行块,防止竞争条件。Queue 是线程安全的 FIFO——生产者 push,消费者 pop(为空时阻塞)。ConditionVariable 协调线程:wait 释放互斥锁并休眠,signal/broadcast 唤醒等待线程。共享可变状态始终使用同步。当线程相互等待时发生死锁——以一致顺序获取锁。

ruby
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new

threads = 10.times.map do
  Thread.new do
    1000.times do
      mutex.synchronize { counter += 1 }
    end
  end
end
threads.each(&:join)
puts counter   # 10000 (without mutex, would be less)

# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new

producer = Thread.new do
  5.times { |i| queue << "item #{i}" }
  queue << :done
end

consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    puts "Processed: #{item}"
  end
end

[producer, consumer].each(&:join)

# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.new

Fiber

Fiber 是协作式轻量级线程:它们手动 yield 控制而非被抢占。resume 启动/恢复 fiber;Fiber.yield 暂停它并返回值。Fiber 适用于生成器、惰性求值和解析状态机。与线程不同,一次只有一个 fiber 运行,因此无需同步。Fiber 比线程便宜但不能使用多核。用于 I/O 多路复用(EventMachine、Async)。

ruby
# Fiber: cooperative concurrency (manual scheduling)
fiber = Fiber.new do
  puts "Fiber started"
  Fiber.yield "first yield"
  puts "Fiber resumed"
  Fiber.yield "second yield"
  puts "Fiber ending"
  "fiber done"
end

puts fiber.resume   # "Fiber started", returns "first yield"
puts fiber.resume   # "Fiber resumed", returns "second yield"
puts fiber.resume   # "Fiber ending", returns "fiber done"

# Generator pattern with Fiber
def fibonacci
  Fiber.new do
    a, b = 0, 1
    loop do
      Fiber.yield a
      a, b = b, a + b
    end
  end
end

fib = fibonacci
10.times { print fib.resume, " " }
# 0 1 1 2 3 5 8 13 21 34

Async 与 Concurrent Ruby

async gem 在底层使用 Fiber 提供现代异步 I/O,实现高并发网络代码。concurrent-ruby 提供线程安全抽象:Future(异步结果)、Promise(可链式)、线程池和原子变量。I/O 密集型工作(HTTP、数据库)使用 async,CPU 密集型工作使用线程池。始终关闭池以避免挂起。这些 gem 绕过 MRI 的 GIL 实现实用并发。

ruby
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'

Async do
  internet = Async::HTTP::Internet.new

  # Run requests concurrently
  tasks = 3.times.map do |i|
    Async do
      response = internet.get("https://httpbin.org/delay/#{i}")
      puts "Request #{i}: #{response.status}"
    end
  end

  tasks.each(&:wait)
end

# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'

# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value   # 42 (blocks until ready)

# Promise chain
Concurrent::Promise.execute { 1 }
  .then { |v| v + 1 }
  .then { |v| v * 2 }
  .then { |v| puts v }   # 4

# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
  pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdown

Ractor(Ruby 3.0+)

Ractor(Ruby 3.0+)通过避免 GIL 提供真正的并行。每个 Ractor 有自己的堆,因此无数据竞争。通信通过消息(send/receive、take)。发送的对象被复制(或用 Ractor.move 移动)以保持隔离。冻结对象可以共享。Ractor 是并行 Ruby 的未来,但有限制:大多数 gem 尚不是 Ractor 安全的。在隔离可接受的 CPU 密集型并行计算中使用它们。

ruby
# Ractor: true parallelism (no GIL)
# Ruby 3.0+ feature for thread-safe parallel execution

ractor = Ractor.new do
  value = receive    # Receive from main
  value * 2
end

ractor.send(21)
puts ractor.take     # 42

# Parallel computation
ractors = (1..4).map do |i|
  Ractor.new(i) do |n|
    sleep(1)
    n * n
  end
end

results = Ractor.select(*ractors)
# Or: ractors.map(&:take)  # [1, 4, 9, 16]

# Ractor restrictions:
# - Cannot share mutable objects
# - Communication only via messages
# - Copy or move semantics for sending

# Shared frozen objects are OK
SHARED = Ractor.new { [1, 2, 3].freeze }
# Multiple ractors can read SHARED safely
23

编码与 I/O

字符串编码

每个 Ruby 字符串都有编码(默认 UTF-8)。bytesize 是字节数;length 是字符数(多字节编码不同)。force_encoding 更改编码标签而不转换字节(当你知道实际编码时使用)。encode 在编码之间转换。处理外部数据前始终检查 valid_encoding?。用魔术注释 # encoding: utf-8 设置源文件编码(尽管 Ruby 2.0+ 中 UTF-8 是默认的)。

ruby
# Ruby strings have encodings
s = "hello"
puts s.encoding              # <Encoding:UTF-8>
puts s.bytesize              # 5
puts s.length               # 5

# Multibyte characters
japanese = "こんにちは"
puts japanese.encoding       # UTF-8
puts japanese.bytesize       # 15 (3 bytes per char)
puts japanese.length         # 5 (5 characters)

# Force encoding (interpret bytes differently)
bytes = "caf\u00e9".force_encoding('ASCII-8BIT')
puts bytes.encoding          # ASCII-8BIT

# Encode (convert to different encoding)
utf8 = "café"
latin1 = utf8.encode('ISO-8859-1')
puts latin1.encoding         # ISO-8859-1

# Invalid bytes
bad = "\xFF\xFE".force_encoding('UTF-8')
puts bad.valid_encoding?     # false
fixed = bad.encode('UTF-8', invalid: :replace, replace: '?')

文件 I/O

File.read 将整个文件加载到内存;File.foreach 逐行读取(对大文件节省内存)。File.write 覆盖;mode 'a' 追加;'r+' 读写。二进制模式('rb'、'wb')防止编码转换。始终使用块形式(File.open)确保文件关闭。FileUtils 提供更高级操作(cp、mv、mkdir_p)。读取前用 File.exist? 检查存在。

ruby
# Read entire file
content = File.read('data.txt')

# Read line by line (memory efficient)
File.foreach('large.log') do |line|
  puts line.chomp
end

# Write to file
File.write('output.txt', 'Hello, World!')

# Append
File.open('log.txt', 'a') do |f|
  f.puts "New log entry"
end

# Read/Write modes
File.open('file.txt', 'r+') do |f|
  content = f.read
  f.rewind
  f.write("Updated: " + content)
end

# Binary mode
File.open('image.png', 'rb') do |f|
  bytes = f.read
  puts bytes.bytesize
end

# File operations
File.exist?('file.txt')
File.size('file.txt')
File.mtime('file.txt')
File.delete('file.txt')
File.rename('old.txt', 'new.txt')
FileUtils.cp('a.txt', 'b.txt')   # require 'fileutils'

StringIO 与 Tempfile

StringIO 用类似 IO 的接口包装字符串,适用于测试文件操作而不触及磁盘。Tempfile 创建在块退出时(Tempfile.create)或取消链接时(Tempfile.new)自动删除的临时文件。对于不适合内存的大数据或通过路径传递给外部程序,使用 Tempfile。始终用 ensure 块确保清理。StringIO 非常适合 IO 代码的单元测试。

ruby
require 'stringio'
require 'tempfile'

# StringIO: in-memory file-like object
io = StringIO.new
io.puts "Hello"
io.puts "World"
io.rewind
puts io.read   # "Hello\nWorld\n"

# Use like a file
io = StringIO.new("line1\nline2\nline3")
io.each_line { |line| puts line.chomp }

# Tempfile: auto-deleted temporary file
Tempfile.create('myapp') do |f|
  f.write('temporary data')
  f.rewind
  puts f.read
end   # File automatically deleted

# Tempfile with explicit cleanup
temp = Tempfile.new('myapp')
begin
  temp.write('data')
  temp.rewind
  # Use temp.path to pass to external programs
ensure
  temp.close
  temp.unlink
end

网络 I/O

TCPSocket/TCPServer 提供低级 TCP 访问。Net::HTTP 是标准 HTTP 客户端(内置)。对于复杂 HTTP 需求(会话、cookie、重试),使用 httparty 或 faraday gem。HTTPS 始终设置 use_ssl = true。对于高性能 HTTP,考虑 async-http 或 typhoeus。URI 安全解析 URL。处理 Net::ReadTimeout 和 Errno::ECONNREFUSED 以获得健壮的网络代码。

ruby
require 'socket'
require 'net/http'
require 'uri'

# TCP Client
TCPSocket.open('example.com', 80) do |socket|
  socket.write("GET / HTTP/1.0\r\n\r\n")
  puts socket.read
end

# TCP Server
server = TCPServer.new(2000)
loop do
  client = server.accept
  client.puts "Hello from server"
  client.close
end

# HTTP Client
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
puts response.code          # 200
puts response.body

# HTTP POST
response = Net::HTTP.post_form(uri, key: 'value')

# HTTP with custom headers
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
response = http.request(request)

STDIO 与管道

$stdout、$stderr、$stdin 是全局 IO 对象(使用常量 STDOUT/STDERR/STDIN 获取原始对象)。通过重新分配全局变量重定向。StringIO 捕获输出用于测试。Open3.popen3 完全控制子进程的 stdin/stdout/stderr。Open3.pipeline 像管道一样链式命令。Capture3 返回 stdout、stderr 和状态。始终关闭 stdin 以向子进程发出 EOF 信号。使用 wait.value 获取退出状态。

ruby
# Standard IO
$stdout.puts "To stdout"
$stderr.puts "To stderr"
input = $stdin.gets   # Read from stdin

# Redirect IO
$stdout = File.open('log.txt', 'w')
puts "This goes to file"
$stdout = $stdout   # Restore (or use STDOUT constant)

# Capture output
captured = StringIO.new
original = $stdout
$stdout = captured
puts "Captured"
$stdout = original
puts captured.string   # "Captured\n"

# Open3: run commands with full IO control
require 'open3'

Open3.popen3('grep hello') do |stdin, stdout, stderr, wait|
  stdin.puts "hello world"
  stdin.puts "goodbye"
  stdin.close
  puts stdout.read      # "hello world"
  puts wait.value.exitstatus
end

# Pipeline
output = Open3.pipeline('ls', 'grep .rb', 'wc -l')
24

模式匹配(3.0+)

基本模式匹配

模式匹配(in 关键字)解构数组和哈希,绑定变量。=> 将匹配值绑定到变量。*rest 捕获剩余数组元素。哈希匹配是部分的:额外键被忽略。| 匹配多个模式。case/in 是主要形式;单行形式是 expression in pattern。模式匹配在解析结构化数据(JSON、AST)和替换复杂 if-else 链时特别强大。

ruby
# in: pattern matching (Ruby 2.7+, stable in 3.0)
case [1, 2, 3]
in [1, *rest]
  puts "Starts with 1, rest: #{rest}"
end

case {name: 'Alice', age: 30}
in {name: String => name, age: Integer => age}
  puts "#{name} is #{age}"
end

# Multiple patterns with |
case status
in :success | :ok
  puts "Good"
else
  puts "Other"
end

# Array patterns
case [1, 2, 3, 4]
in [_, _, *rest]
  puts "Rest: #{rest}"   # [3, 4]
end

# Hash patterns (partial match by default)
case {a: 1, b: 2, c: 3}
in {a: Integer}
  puts "Has a"   # Matches (ignores b, c)
end

变量绑定与守卫

=> 将匹配值绑定到变量。pin 运算符(^var)根据变量当前值匹配(而非绑定)。守卫(if/unless)添加条件。数组模式支持 * 在任意位置的 splat。查找模式 [*, target, *] 在数组中搜索元素。模式匹配对于复杂数据提取是声明式且简洁的。模式中绑定的变量在 case 语句之后可用。

ruby
user = {name: 'Alice', age: 30, role: :admin}

case user
in {name: String => name, age: Integer => age, role: :admin} if age > 18
  puts "#{name} is an adult admin"
end

# Pin operator (^): match against variable value
expected = 'Alice'
case user
in {name: ^expected}
  puts "Matched expected name"
end

# Array destructuring with binding
case [1, 2, 3]
in [first, *middle, last]
  puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end

# Find pattern (search within array)
case [1, 2, 3, 4, 5]
in [*, 3 => three, *]
  puts "Found 3: #{three}"
end

类与类型模式

类模式按类型匹配。自定义类通过实现 deconstruct(数组模式)和 deconstruct_keys(哈希模式)支持解构。模式 Class[args] 使用 deconstruct;Class(key:) 使用 deconstruct_keys。许多内置类(Time、Date、MatchData)支持解构。这实现了对领域对象的表达性匹配。实现这些方法使你的类可模式匹配。

ruby
# Match by class
case value
in Integer
  puts "Integer"
in String
  puts "String"
in Array
  puts "Array"
end

# Class with destructuring
class Point
  attr_reader :x, :y
  def initialize(x, y) @x, @y = x, y end
  def deconstruct = [@x, @y]              # Array pattern
  def deconstruct_keys(keys) = {x: @x, y: @y}  # Hash pattern
end

p = Point.new(1, 2)
case p
in Point[x, y]
  puts "Point at (#{x}, #{y})"
end

case p
in Point(x:, y:)
  puts "X: #{x}, Y: #{y}"
end

# Built-in classes support destructuring
case Time.now
in Time(hour: h) if h < 12
  puts "Morning"
end

单行模式匹配

单行形式(expr in pattern)返回 true/false 而非引发 NoMatchingPatternError。它在成功匹配时绑定变量。适用于守卫子句和条件提取。右向形式(pattern => var)绑定整个匹配。单行匹配对简单情况很简洁;复杂多分支逻辑使用 case/in。失败匹配中绑定的变量保持 nil。

ruby
# One-line form (Ruby 3.0+)
{status: :ok, data: 42} in {status: :ok, data: Integer => data}
puts data   # 42

# Returns true/false (does not raise)
[1, 2, 3] in [Integer, Integer, Integer]   # true
[1, 2, 3] in [String, *, *]                # false

# Useful for conditionals
if user in {role: :admin}
  puts "Admin access"
end

# Guard clauses
def process(data)
  return unless data in {type: String, value: Integer}
  # ...
end

# Extracting from JSON
json = JSON.parse('{"user":{"name":"Alice","age":30}}')
json in {"user" => {"name" => String => name, "age" => Integer => age}}
puts "#{name}, #{age}"   # Alice, 30

实际用例

模式匹配擅长:解析结构化数据(JSON、XML)、状态机、AST 遍历和按数据形状派发。它用声明式模式替换冗长的 if-else 链。else 子句处理意外格式。与守卫结合用于复杂条件。模式匹配使复杂数据处理的代码更可读、更可维护。它是 Ruby 3 最强大的功能之一,用于干净、富有表现力的代码。

ruby
# 1. Parse JSON API responses
def handle_response(response)
  case JSON.parse(response)
  in {"status" => "success", "data" => Array => items}
    items.each { |item| process(item) }
  in {"status" => "error", "message" => String => msg}
    raise "API error: #{msg}"
  in {"status" => "error", "code" => Integer => code} if code >= 500
    retry_request
  else
    raise "Unknown response format"
  end
end

# 2. State machines
case state
in :idle, event: :start
  transition_to :running
in :running, event: :pause
  transition_to :paused
in :paused, event: :resume
  transition_to :running
in :running | :paused, event: :stop
  transition_to :idle
end

# 3. AST traversal
def evaluate(node)
  case node
  in {type: 'number', value: n}
    n
  in {type: 'add', left:, right:}
    evaluate(left) + evaluate(right)
  in {type: 'mul', left:, right:}
    evaluate(left) * evaluate(right)
  end
end
25

元编程

send 与 define_method

define_method 动态创建方法。send 按名称调用方法(甚至私有)。public_send 尊重可见性。适用于 DSL 和减少样板。小心用户输入以避免安全问题。元编程强大但会使代码更难理解。

ruby
class Foo
  define_method(:greet) do |name|
    "Hello, #{name}"
  end
end
Foo.new.greet("Alice")  # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob")  # "Hello, Bob"
Foo.new.public_send(:greet, "Bob")  # Respects visibility

method_missing

method_missing 拦截对未定义方法的调用。适用于动态派发和 DSL。始终重写 respond_to_missing? 以匹配。可能很慢并隐藏 bug。方法集合已知时优先使用 define_method。ActiveRecord 用于属性访问器。

ruby
class DynamicHash
  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      self[name.to_s.chomp('=')] = args.first
    else
      self[name.to_s]
    end
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end

eval

eval 将字符串作为 Ruby 代码执行。极其强大但危险。切勿 eval 不受信任的输入(代码注入)。使用 binding.eval 获得特定上下文。对于安全求值,使用 Ripper 等解析器或沙箱。大多数用例有更安全的替代方案。

ruby
# Execute a string as Ruby code
result = eval("1 + 2 * 3")  # 7
# Dynamic method definition
eval("def dynamic_method; 42; end")
dynamic_method  # 42
# WARNING: never eval untrusted input!

开放类

Ruby 类是开放的:你可以向任何类添加方法,包括内置类。这称为猴子补丁。对于快速修复很强大但可能导致冲突和混淆。使用细化进行范围修改。清楚地记录更改。优先使用组合而非猴子补丁。

ruby
class String
  def shout
    upcase + "!"
  end
end
"hello".shout  # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflicts

类宏

类宏是定义其他方法的类方法。has_many、belongs_to、attr_accessor 是示例。它们内部使用 define_method。这是 Rails 创建动态方法的方式。RSpec 和 Sinatra 等 DSL 使用此模式。使代码声明式且可读。

ruby
class ActiveRecord::Base
  def self.has_many(name)
    define_method(name) { [] }
  end
end
class Post < ActiveRecord::Base
  has_many :comments
end
Post.new.comments  # []

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。