CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby 语言入门/课时/IO 与 Enumerable
RawBlame
课时目录12 节

Ruby 语言入门

  1. 01Ruby 环境
  2. 02数据类型
  3. 03语法基础
  4. 04控制流
  5. 05方法与 Blocks
  6. 06OOP 与 Mixin
  7. 07元编程
  8. 08IO 与 Enumerable
  9. 09并发
  10. 10Gems 与框架
  11. 11测试与质量
  12. 12综合练习

IO 与 Enumerable

File/Regex/Enumerable。

30 分钟

IO 与 Enumerable

这一课你将能做到

  1. 用 File 类读写文件、遍历目录,理解 File.open 的 block 自动关闭模式
  2. 用正则表达式匹配、提取、替换字符串,掌握 Ruby 特有的 =~ 和命名捕获
  3. 熟练使用 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"

讲清楚要点

  1. File.open 的 block 模式是 RAII:block 结束自动 close,异常时也会关闭。不用 block 就要手动 f.close,容易泄露。Ruby 的 File 设计比 Python 的 with 更早。
  2. Enumerable 是「协议」不是「类」:任何类只要定义了 each 并 include Enumerable,就免费获得 map/select/reduce 等 50+ 方法。这是 Ruby 模块设计的巅峰。
  3. 正则的 =~ 返回索引,match 返回 MatchData:=~ 是遗留语法,返回匹配位置或 nil;match 返回 MatchData 对象,功能更强。Ruby 2.4+ 推荐 match?(只判断,不构建对象,更快)。
  4. group_by vs partition:group_by 按任意键分组(返回 Hash);partition 按布尔二分(返回两个数组)。分组用 group_by,筛选用 partition。
  5. 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] 不判空无匹配时 NoMethodErrorif match = text.match(...)
orders.map { ... }.select { ... }中间数组浪费内存用 filter_map(Ruby 2.7+)或 lazy
^...$ 做整串验证多行文本绕过用 \A...\z
group_by 后访问不存在的键返回 nilgroup_by 的 Hash 默认无默认值,用 Hash.new { ... }

卡点急救

  1. 文件编码错误(Encoding::InvalidByteSequenceError):File.read("file.txt", encoding: "UTF-8"),或 File.open(..., "r:UTF-8")。Windows 文件可能是 GBK。
  2. 正则匹配不到中文:确认文件编码是 UTF-8,正则里用 [\u4e00-\u9fa5] 匹配汉字。
  3. 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 简洁。

自测三问

  1. File.open 的 block 模式和非 block 模式在异常安全上有什么区别?
  2. group_by 和 partition 分别返回什么结构?什么场景用哪个?
  3. lazy 解决了什么问题?为什么 (1..Float::INFINITY).map 会卡死?

小结一句话

File 管外部数据进出,Enumerable 管内部数据加工;block 模式保安全,链式调用提表达力。

延伸

  • Ruby Enumerable 官方文档
  • Ruby File 官方文档
  • 下一课:并发——Thread、Fiber、Ractor 与 GIL 的真相。
加载本节练习…
← 上一课元编程下一课 →并发