大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
IO 与 Enumerable
这一课你将能做到
- 用 File 类读写文件、遍历目录,理解 File.open 的 block 自动关闭模式
- 用正则表达式匹配、提取、替换字符串,掌握 Ruby 特有的
=~和命名捕获 - 熟练使用 Enumerable 的 20+ 个核心方法做集合变换、过滤、分组、排序
为什么要学
IO 和集合操作是日常开发的「水电煤」。读日志、解析 CSV、处理 JSON、批量转换数据——Ruby 的 File API 和 Enumerable 模块就是为此而生。Enumerable 是 Ruby 最强大的模块,Array、Hash、Range 都 include 它,学会它等于学会了所有集合类的操作。
人话:File 是「读写外部世界」的接口,Enumerable 是「加工内部数据」的流水线。File.open 的 block 模式保证文件一定关闭,不会泄露句柄;Enumerable 的 map/select/reduce 让你用声明式风格处理数据,不用手写循环。
先建立图像
Enumerable 核心方法分类
┌─────────────┬─────────────────────────────────────┐
│ 变换 │ map, flat_map, compact, uniq, sort │
│ 过滤 │ select, reject, find, take_while │
│ 聚合 │ reduce/inject, sum, count, min/max │
│ 分组 │ group_by, partition, chunk_while │
│ 判断 │ all?, any?, none?, include? │
│ 遍历 │ each, each_with_index, each_slice │
└─────────────┴─────────────────────────────────────┘
File 打开模式
"r" 只读(默认) "w" 只写,清空原内容
"a" 追加 "r+" 读写,不清空
跟着做
步骤 1:File 读写——block 模式最安全
# 推荐:block 模式,自动关闭文件
File.open("test.txt", "w") do |f|
f.puts "第一行"
f.puts "第二行"
end
# 读取整个文件
content = File.read("test.txt")
puts content
# 逐行读取(大文件推荐,不一次性加载内存)
File.foreach("test.txt") do |line|
puts line.chomp
end
# 读成数组
lines = File.readlines("test.txt")
puts lines.inspect
# 写文件(覆盖)
File.write("test.txt", "新内容\n")
# 追加
File.open("test.txt", "a") { |f| f.puts "追加一行" }
# 文件存在性、大小、修改时间
puts File.exist?("test.txt") # => true
puts File.size("test.txt") # => 字节数
puts File.mtime("test.txt") # => 修改时间 Time 对象
# 目录操作
Dir.mkdir("backup") unless Dir.exist?("backup")
Dir.entries(".").each { |e| puts e } # 列出目录
步骤 2:CSV 与 JSON——日常数据格式
require "csv"
require "json"
# CSV 读取
csv_text = <<~CSV
name,age,city
Tom,25,Beijing
Lisa,30,Shanghai
CSV
CSV.parse(csv_text, headers: true) do |row|
puts "#{row['name']} 住在 #{row['city']}"
end
# CSV 写入
CSV.open("users.csv", "w") do |csv|
csv << ["name", "age"]
csv << ["Tom", 25]
csv << ["Lisa", 30]
end
# JSON 解析与生成
json_str = '{"name":"Tom","tags":["admin","dev"]}'
data = JSON.parse(json_str)
puts data["name"] # => Tom
puts data["tags"].inspect # => ["admin", "dev"]
hash = { name: "Lisa", scores: [90, 85] }
puts JSON.generate(hash) # => {"name":"Lisa","scores":[90,85]}
puts JSON.pretty_generate(hash) # 格式化输出
步骤 3:正则表达式
text = "订单号:ORD-2024-00123,金额:¥1,299.00"
# 匹配判断
puts text =~ /ORD-\d+/ # => 匹配到的起始索引,或 nil
puts text.match?(/ORD-\d+/) # => true(Ruby 2.4+ 推荐)
# 提取内容
match = text.match(/ORD-(\d+)-(\d+)/)
puts match[0] # => ORD-2024-00123(完整匹配)
puts match[1] # => 2024(第一个捕获组)
puts match[2] # => 00123(第二个捕获组)
# 命名捕获(Ruby 1.9+)
pattern = /ORD-(?<year>\d+)-(?<seq>\d+)/
match = text.match(pattern)
puts match[:year] # => 2024
puts match[:seq] # => 00123
# 全局扫描
puts text.scan(/\d+/).inspect # => ["2024", "00123", "1", "299", "00"]
# 替换
puts text.gsub(/\d+/, "N")
# => 订单号:ORD-N-N,金额:¥N,N.N
# 常用正则模式
email = "user@example.com"
puts email.match?(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i)
# 中文匹配
puts "你好世界".scan(/[\u4e00-\u9fa5]/).inspect
步骤 4:Enumerable 核心方法实战
orders = [
{ id: 1, amount: 100, status: :paid },
{ id: 2, amount: 200, status: :pending },
{ id: 3, amount: 150, status: :paid },
{ id: 4, amount: 80, status: :cancelled }
]
# map:变换
amounts = orders.map { |o| o[:amount] }
puts amounts.inspect # => [100, 200, 150, 80]
# select / reject:过滤
paid = orders.select { |o| o[:status] == :paid }
puts paid.map { |o| o[:id] }.inspect # => [1, 3]
# find:找第一个
first_paid = orders.find { |o| o[:status] == :paid }
puts first_paid[:id] # => 1
# group_by:分组
by_status = orders.group_by { |o| o[:status] }
puts by_status.keys.inspect # => [:paid, :pending, :cancelled]
# partition:二分(真/假)
high, low = orders.partition { |o| o[:amount] >= 100 }
puts "高价: #{high.size}, 低价: #{low.size}"
# sort_by:排序
sorted = orders.sort_by { |o| -o[:amount] } # 降序
puts sorted.map { |o| o[:amount] }.inspect
# reduce / inject:聚合
total = orders.reduce(0) { |sum, o| sum + o[:amount] }
puts total # => 530
# each_with_object:构建新结构
status_count = orders.each_with_object(Hash.new(0)) do |o, h|
h[o[:status]] += 1
end
puts status_count.inspect # => {:paid=>2, :pending=>1, :cancelled=>1}
# 链式组合:真实业务场景
result = orders
.select { |o| o[:status] == :paid }
.sort_by { |o| -o[:amount] }
.map { |o| "订单#{o[:id]}: ¥#{o[:amount]}" }
puts result
# => 订单3: ¥150
# 订单1: ¥100
步骤 5:lazy 与 Date/Time
# lazy:无限序列或大文件处理
# 不用 lazy 会卡死
# (1..Float::INFINITY).map { |n| n * n }.first(10)
# 用 lazy 按需计算
squares = (1..Float::INFINITY).lazy.map { |n| n * n }
puts squares.first(10).inspect # => [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
# Date/Time
require "date"
today = Date.today
puts today # => 2026-10-07
puts today + 7 # => 7 天后
puts today.strftime("%Y年%m月%d日")
now = Time.now
puts now.strftime("%H:%M:%S")
puts Time.parse("2024-01-15 10:30:00") # 需要 require "time"
讲清楚要点
- File.open 的 block 模式是 RAII:block 结束自动 close,异常时也会关闭。不用 block 就要手动
f.close,容易泄露。Ruby 的 File 设计比 Python 的 with 更早。 - Enumerable 是「协议」不是「类」:任何类只要定义了
each并 include Enumerable,就免费获得 map/select/reduce 等 50+ 方法。这是 Ruby 模块设计的巅峰。 - 正则的
=~返回索引,match 返回 MatchData:=~是遗留语法,返回匹配位置或 nil;match返回 MatchData 对象,功能更强。Ruby 2.4+ 推荐match?(只判断,不构建对象,更快)。 - group_by vs partition:
group_by按任意键分组(返回 Hash);partition按布尔二分(返回两个数组)。分组用 group_by,筛选用 partition。 - lazy 是「按需计算」:对无限序列或大文件,先 map 再 first 会卡死;lazy 让操作延迟到取值时才执行。Rails 的
find_each批量处理也用了类似思想。
你可能会懵的地方
-
File.read和File.foreach什么时候用?read一次性读入内存,适合小文件(<100MB);foreach逐行读,适合大文件。Rails 日志分析、数据导入都用foreach。 -
map和collect一样吗? 完全一样,collect是map的别名。Ruby 社区习惯用map,Smalltalk 传统用collect。同理select=filter,reduce=inject。 -
inject的括号参数是干嘛的?inject(0)是初始值。不给的话,第一个元素当初始值:[1,2,3].inject(:+)=1+2+3。给初始值更安全,空数组不会报错:[].inject(0, :+)= 0。 -
正则里的
\A和^有什么区别?\A匹配字符串绝对开头;^匹配行开头(多行模式)。验证整个字符串用\A...\z,防止换行绕过。 -
each_with_object和reduce怎么选? 构建 Hash/Array 时each_with_object更简洁(不用写累加器返回);纯数值聚合用reduce。each_with_object的 block 参数顺序是|元素, 累加器|,和reduce相反。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
f = File.open(...); f.read 不关闭 | 文件句柄泄露 | 用 File.open(...) { |f| ... } |
match 后直接用 match[1] 不判空 | 无匹配时 NoMethodError | if match = text.match(...) |
orders.map { ... }.select { ... } | 中间数组浪费内存 | 用 filter_map(Ruby 2.7+)或 lazy |
^...$ 做整串验证 | 多行文本绕过 | 用 \A...\z |
group_by 后访问不存在的键 | 返回 nil | group_by 的 Hash 默认无默认值,用 Hash.new { ... } |
卡点急救
- 文件编码错误(Encoding::InvalidByteSequenceError):
File.read("file.txt", encoding: "UTF-8"),或File.open(..., "r:UTF-8")。Windows 文件可能是 GBK。 - 正则匹配不到中文:确认文件编码是 UTF-8,正则里用
[\u4e00-\u9fa5]匹配汉字。 - Enumerable 链式调用结果不对:中间步骤打印检查:
orders.tap { |o| p o }.select { ... }。用tap调试不破坏链式。
面试常问
-
Enumerable 和 Enumerator 的区别? Enumerable 是模块,提供集合操作方法;Enumerator 是对象,封装迭代过程,支持外部迭代(
next)和链式懒加载(lazy)。each返回 Enumerator,map返回 Array。 -
map和each的区别?为什么 Rails 里常用each?map返回新数组(变换结果);each返回原数组(副作用)。Rails 视图里@users.each { ... }是为了渲染 HTML,不需要新数组;map会创建多余数组浪费内存。 -
Ruby 的正则和其他语言有什么独特之处?
=~操作符、命名捕获(?<name>...)、Regexp.union合并多个正则、String#scan全局提取。Ruby 的正则语法和 Perl 最接近,比 Java 简洁。
自测三问
File.open的 block 模式和非 block 模式在异常安全上有什么区别?group_by和partition分别返回什么结构?什么场景用哪个?lazy解决了什么问题?为什么(1..Float::INFINITY).map会卡死?
小结一句话
File 管外部数据进出,Enumerable 管内部数据加工;block 模式保安全,链式调用提表达力。
延伸
- Ruby Enumerable 官方文档
- Ruby File 官方文档
- 下一课:并发——Thread、Fiber、Ractor 与 GIL 的真相。