Ruby 练习题(15 题)
Ruby 的优雅要靠动手才能真正掌握。这一页整理 15 道精选练习题,按难度分三级——基础 6 题(变量 / 运算 / 控制流 / 方法)、进阶 5 题(block / Proc / Hash)、实战 4 题(综合应用)。每题都包含题目描述、提示、可折叠的参考答案。
使用建议:先别急着展开答案。读题后自己打开本地 Ruby 环境(或 在线 Ruby)试着写,卡住了再看提示,实在写不出再对照参考答案。Ruby 的写法很多,你的写法可能比参考更好——能跑通、可读性强就是好代码。
基础(6 题):变量 / 运算 / 控制流 / 方法
这一组覆盖 Ruby 的基础语言元素,目标是用最短的代码解决一个小问题,训练"把思路翻译成代码"的能力。Ruby 一切皆对象,5 是 Integer、"str" 是 String,方法调用上没有"基本类型"和"包装类"的区别。
第 1 题:翻转字符串
题目:写一个方法 reverse_string(s),把传入的字符串翻转过来。比如 "hello" 翻转成 "olleh"。
提示:Ruby 字符串直接支持 .reverse;也可 s.chars.reverse.join 拆字再拼回。
参考答案
# 方法 1:内置 .reverse 最简洁(字符串、数组都支持)
def reverse_string(s)
s.reverse
end
# 方法 2:手动用 chars + reverse + join(理解原理)
def reverse_manual(s)
s.chars.reverse.join
end
puts reverse_string("hello") # olleh
puts reverse_string("Ruby") # ybuR
puts reverse_manual("world") # dlrow
# 输出: olleh / ybuR / dlrow第 2 题:判断素数
题目:写一个方法 prime?(n),判断正整数 n 是否为素数(只能被 1 和自己整除)。Ruby 习惯用 ? 结尾命名返回布尔的方法。
提示:小于 2 的都不是素数;只需检查从 2 到 sqrt(n) 的整除性;(2..Integer.sqrt(n)).none? { |i| n % i == 0 } 最优雅。
参考答案
def prime?(n)
return false if n < 2
# 只需检查到 sqrt(n),Integer::sqrt 是整数平方根,无浮点误差
(2..Integer.sqrt(n)).none? { |i| n % i == 0 }
end
# none? { ... } 遍历集合,块内"全为 false"时返回 true
# 等价于 (2..Math.sqrt(n)).all? { |i| n % i != 0 }
puts prime?(2) # true
puts prime?(7) # true
puts prime?(10) # false
puts prime?(1) # false
# 输出: true / true / false / false第 3 题:FizzBuzz
题目:用 puts 打印 1 到 15。3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是 3 和 5 的倍数打印 FizzBuzz,其余打印数字本身。
提示:if / elsif / else 的判断顺序很关键——必须先判断"同时是 3 和 5 的倍数"(即 15 的倍数);遍历用 (1..15).each { |i| ... }。
参考答案
# 经典题:3 的倍数 Fizz,5 的倍数 Buzz,15 的倍数 FizzBuzz
(1..15).each do |i|
if i % 15 == 0 # 必须先判断 15,否则会被 3 或 5 截胡
puts "FizzBuzz"
elsif i % 3 == 0
puts "Fizz"
elsif i % 5 == 0
puts "Buzz"
else
puts i
end
end
# 输出:
# 1
# 2
# Fizz
# 4
# Buzz
# Fizz
# 7
# 8
# Fizz
# Buzz
# 11
# Fizz
# 13
# 14
# FizzBuzz第 4 题:数组最大值
题目:写一个方法 find_max(arr),返回数组中的最大值。
提示:Ruby 数组直接调 .max 最简洁;也可用 inject 逐步比较,理解底层逻辑。
参考答案
# 方法 1:内置 .max 最直接
def find_max(arr)
arr.max
end
# 方法 2:inject 逐次比较(理解原理)
def find_max_inject(arr)
arr.inject { |m, x| x > m ? x : m }
end
puts find_max([3, 1, 4, 1, 5, 9, 2, 6]) # 9
puts find_max([-5, -2, -9]) # -2
puts find_max_inject([10, 20, 30]) # 30
puts [1, 2, 3].max # 3(直接对数组调 .max)
# 输出: 9 / -2 / 30 / 3第 5 题:数组求和
题目:写一个方法 sum_arr(arr),返回数值数组的总和;空数组返回 0。
提示:Ruby 2.4+ 数组自带 .sum;函数式风格 inject(0, :+) 也很常见——:+ 是符号形式的 block 简写。
参考答案
# 方法 1:Ruby 2.4+ 数组内置 .sum(Rails 早已支持,纯 Ruby 也已加入)
def sum_arr(arr)
arr.sum
end
# 方法 2:inject(:+) 函数式风格,符号作为 block 简写
def sum_inject(arr)
arr.inject(0, :+) # 0 是初始值,避免空数组返回 nil
end
puts sum_arr([1, 2, 3, 4, 5]) # 15
puts sum_inject([10, 20, 30]) # 60
puts sum_arr([]) # 0(空数组 .sum 返回 0)
# 输出: 15 / 60 / 0第 6 题:计算阶乘
题目:写一个方法 factorial(n),返回 n!。要求 factorial(0) == 1,且对负数抛 ArgumentError。
提示:最 Rubyist 的写法 (1..n).inject(1, :*)——n=0 时 Range 为空,inject 直接返回初始值 1;也可递归 n < 2 ? 1 : n * factorial(n-1)。
参考答案
# 方法 1:Range#inject 最 Rubyist 的写法
def factorial_range(n)
raise ArgumentError, "阶乘不接受负数" if n < 0
(1..n).inject(1, :*) # n=0 时 Range 为空,inject 返回初始值 1
end
# 方法 2:递归(更贴近数学定义)
def factorial_rec(n)
raise ArgumentError, "阶乘不接受负数" if n < 0
n < 2 ? 1 : n * factorial_rec(n - 1)
end
puts factorial_range(5) # 120
puts factorial_range(0) # 1
puts factorial_rec(6) # 720
# 输出: 120 / 1 / 720进阶(5 题):block / Proc / Hash
这一组围绕 Ruby 最具特色的特性——block。block 是"匿名的代码块",几乎所有 Ruby 内置迭代方法都靠它驱动。理解 block 和 Proc 的转换、掌握 yield、链式调用 map/select,是写出地道 Ruby 代码的关键。
第 7 题:自定义 each 方法用 yield
题目:写一个 MyList 类,内部封装数组,通过 yield 暴露 each 方法,让外部能像普通数组一样遍历它。
提示:在 each 里遍历内部数组,每个元素 yield 给外部 block;再 include Enumerable 就能白嫖 map / select / max 等几十个方法。
参考答案
# 自己写一个 MyList 类,用 yield 暴露 each 方法,
# 让外部能像操作普通数组一样遍历它
class MyList
include Enumerable # 加这个 mixin 自动获得 map / select / max 等
def initialize(items)
@items = items
end
def each
@items.each { |x| yield x } # yield 把元素交给块
end
end
list = MyList.new([1, 2, 3, 4, 5])
list.each { |x| print x, " " } # 1 2 3 4 5
puts
puts list.map { |x| x * 2 }.inspect # [2, 4, 6, 8, 10]
puts list.select(&:even?).inspect # [2, 4]
puts list.max # 5
# 输出:
# 1 2 3 4 5
# [2, 4, 6, 8, 10]
# [2, 4]
# 5第 8 题:map / select 链式处理
题目:给定 (1..10).to_a,链式调用 select + map 取出偶数再平方;再过滤长度 ≥ 5 的单词并转大写。
提示:select 过滤、map 变换,链式写法每步返回新数组;&:upcase 和 &:even? 是符号转 block 的简写,等价于 { |x| x.even? }。
参考答案
# 函数式风格:链式调用 map / select / reduce,逐步变换数据
nums = (1..10).to_a
# 1. 取偶数 → 再平方
result = nums.select { |n| n.even? }.map { |n| n * n }
puts result.inspect # [4, 16, 36, 64, 100]
# 2. 过滤长度 >= 5 的单词 → 转大写
words = %w[ruby python go javascript rust]
long_upper = words.select { |w| w.length >= 5 }.map(&:upcase)
puts long_upper.inspect # ["PYTHON", "JAVASCRIPT"]
# 3. 链式 + 求和:1..10 中偶数的平方和
sum_of_squares = nums.select(&:even?).map { |n| n * n }.sum
puts sum_of_squares # 220(4+16+36+64+100)
# 输出: [4, 16, 36, 64, 100] / ["PYTHON", "JAVASCRIPT"] / 220第 9 题:block 转 Proc 对象(&)
题目:写一个方法接收 block 并把它转成 Proc 对象存起来;再演示把 Proc 转回 block 调用 .map;最后用 &:upcase 把符号当 block 用。
提示:& 在方法参数列表里把 block 收成 Proc(def foo(&block)),在调用时把 Proc 或 Symbol 转成 block(map(&square) / map(&:upcase))。
参考答案
# & 前缀:把 block 转成 Proc 对象(或反之)
# 场景:想把"块"存起来、传给别的函数,就必须先转成 Proc
# 1. 方法参数加 & 把 block 收成 Proc
def capture(&block)
puts block.class # Proc
puts block.call(10) # 调用块
end
capture { |x| x * 3 } # 输出 Proc / 30
# 2. Proc 转回 block(& 同样适用)
square = Proc.new { |x| x * x }
puts [1, 2, 3].map(&square).inspect # [1, 4, 9]
# 3. & :symbol 把符号转 block(方法名简写)
puts %w[apple banana].map(&:upcase).inspect # ["APPLE", "BANANA"]
puts [3, 1, 2].sort.reverse.inspect # [3, 2, 1]
# 输出: Proc / 30 / [1, 4, 9] / ["APPLE", "BANANA"]第 10 题:Hash 遍历与 group_by 分组
题目:给定一组员工(姓名 + 部门),用 group_by 按部门分组输出;再用 tally 统计数组中每个元素的出现次数。
提示:group_by { |u| u[:dept] } 返回 { dept => [users] };Array#tally(Ruby 2.7+)一行统计频次,等价于 group_by(&:itself).transform_values(&:size)。
参考答案
# group_by:按 block 返回值分组,返回 {key => [items]}
users = [
{ name: "小明", dept: "技术" },
{ name: "小红", dept: "市场" },
{ name: "小刚", dept: "技术" },
{ name: "小丽", dept: "市场" },
]
# 按部门分组
by_dept = users.group_by { |u| u[:dept] }
by_dept.each do |dept, members|
puts "#{dept}(#{members.size}): #{members.map { |m| m[:name] }.join(', ')}"
end
# 输出:
# 技术(2): 小明, 小刚
# 市场(2): 小红, 小丽
# tally:Ruby 2.7+ 一行统计元素出现次数
puts %w[apple banana apple cherry apple banana].tally.inspect
# => {"apple"=>3, "banana"=>2, "cherry"=>1}
# 输出: 如上所示第 11 题:类接收 block(initialize 或 yield)
题目:写一个类,既能用 yield 在方法里直接执行 block,也能在 initialize 接收 block 存起来延迟执行。
提示:方法内直接 yield(a, b) 把控制权交给块;initialize(&block) 把块转成 Proc 存到实例变量,之后 @block.call 触发。
参考答案
# 类方法或 initialize 都可以接收 block
# 1. 方法用 yield 直接调块
class Calculator
def operate(a, b)
yield(a, b) # 把控制权交给调用方传入的块
end
end
calc = Calculator.new
puts calc.operate(10, 3) { |x, y| x + y } # 13
puts calc.operate(10, 3) { |x, y| x * y } # 30
# 2. &block 把块存成实例变量,延迟执行
class Action
def initialize(name, &block)
@name = name
@block = block # block 转成 Proc 存起来
end
def run
puts "[#@name] 开始"
@block.call
puts "[#@name] 结束"
end
end
Action.new("备份") { puts " 正在复制文件..." }.run
# 输出: 13 / 30 + 备份任务的开始/复制/结束日志实战(4 题):综合应用
这一组综合运用类、Hash、文件 IO、异常处理——做出几个接近真实场景的小程序。Ruby 的优雅在这些题目里会充分体现。
第 12 题:学生管理(class Student + array 排序)
题目:定义 Student 类(姓名 + 多科成绩 Hash,能算均分),管理多个学生并按均分降序排名。
提示:attr_accessor :name, :scores 自动生成读写器;sort_by(&:average).reverse 按均分降序;each_with_index(1) 从 1 开始带序号遍历。
参考答案
class Student
attr_accessor :name, :scores # 自动生成 getter/setter
def initialize(name, scores)
@name = name
@scores = scores # Hash: { 数学: 90, 语文: 80 }
end
def average
@scores.values.sum.to_f / @scores.size
end
def to_s
"#{name}(均分 #{format('%.1f', average)})"
end
end
students = [
Student.new("小明", { 数学: 90, 语文: 80 }),
Student.new("小红", { 数学: 95, 语文: 88 }),
Student.new("小刚", { 数学: 70, 语文: 100 }),
]
# 按均分降序排名(sort_by + reverse,或 sort { ... })
ranked = students.sort_by(&:average).reverse
puts "成绩排名:"
ranked.each_with_index(1) { |s, i| puts " #{i}. #{s}" }
# 输出:
# 成绩排名:
# 1. 小红(均分 91.5)
# 2. 小明(均分 85.0)
# 3. 小刚(均分 85.0)第 13 题:词频统计(Hash + group_by 或 tally)
题目:统计英文文本中每个单词的出现次数,按频次降序输出 Top 5。
提示:text.downcase.scan(/[a-z]+/) 提取单词并统一小写;tally 自动计数;sort_by { |_, c| -c } 按次数降序(用负号代替 reverse)。
参考答案
# 统计文本中每个单词的出现次数,按频次降序输出 Top N
def word_count(text)
# 用正则提取所有英文单词,统一小写避免 The/the 分开计数
words = text.downcase.scan(/[a-z]+/)
# group_by + size 或直接 tally(Ruby 2.7+)自动计数
words.tally
end
text = <<-TEXT
The quick brown fox jumps over the lazy dog.
The dog was not amused, but the fox just laughed.
TEXT
counts = word_count(text)
puts "Top 5 高频词:"
counts.sort_by { |_, c| -c }.first(5).each do |word, c|
puts " #{word}: #{c}"
end
# 输出:
# Top 5 高频词:
# the: 4
# dog: 2
# fox: 2
# quick: 1
# brown: 1第 14 题:文件读写(File.read / File.foreach)
题目:写代码完成四件事:(a) 整文件读;(b) 逐行读(大文件友好);(c) 覆盖写;(d) 追加写。
提示:File.read(path) 一次读全文;File.foreach(path) 逐行读(常量内存);File.write(path, content) 覆盖;File.open(path, "a") { |f| f.puts ... } 追加。
参考答案
# 1. 整文件读取(小文件最简单)
# content = File.read("article.txt")
# puts content
# 2. 逐行读取(大文件友好,常量内存)——File.foreach 不一次读全文件
# File.foreach("log.txt").with_index(1) do |line, i|
# puts "#{i}: #{line.chomp}"
# end
# 3. 写入文件(覆盖 / 追加)
File.write("output.txt", "第一行\n") # 覆盖写
File.open("output.txt", "a") { |f| f.puts "追加行" } # 追加
# 4. 演示:用 StringIO 模拟文件(避免依赖真实文件)
require "stringio"
fake = StringIO.new("apple\nbanana\napple\n")
counts = Hash.new(0)
fake.each_line { |line| counts[line.chomp] += 1 }
puts counts.inspect
# 输出(同时 output.txt 写入两行):
# {"apple"=>2, "banana"=>1}第 15 题:简易计算器(case when)
题目:实现一个支持 + - * / 的计算器方法 calc(a, op, b)。要求:除数为 0 抛 ZeroDivisionError,未知运算符抛 ArgumentError。
提示:case when "+"; ... when "-"; ... end 比 if/elsif 更清晰;除法时 a.to_f / b 才能得到浮点结果(整数除法会截断);rescue puts $!.message 优雅接住异常。
参考答案
def calc(a, op, b)
case op # case when 比 if/elsif 更适合多分支
when "+" then a + b
when "-" then a - b
when "*" then a * b
when "/"
raise ZeroDivisionError, "除数不能为 0" if b == 0
a.to_f / b # to_f 让除法返回浮点
else
raise ArgumentError, "不支持的运算符: #{op}"
end
end
# 命令行测试
puts calc(3, "+", 5) # 8
puts calc(10, "/", 4) # 2.5
puts calc(2, "*", 9) # 18
puts calc(10, "-", 3) # 7
# 异常演示(注释掉以免中断):
# puts calc(10, "/", 0) rescue puts "错误: #{$!.message}"
# 输出: 8 / 2.5 / 18 / 7
# $! 是最后抛出的异常(rescue 修饰符形式的隐式变量)练习建议
不要只看答案。把每道题在本地或在线环境亲手敲一遍,改一改输入试试边界情况(空数组、负数、超长字符串、嵌套 Hash)。编程是手艺活,键盘上练出来的肌肉记忆比眼睛看十遍都管用。
试着扩展:做完第 12 题学生管理,能不能加上"按单科排名""导出 CSV"?做完第 13 题词频统计,能不能加上停用词过滤?做完第 15 题计算器,能不能加上幂运算和括号?每个扩展都是一次小型的真实开发。
更多练习资源:刷题去 Exercism Ruby;做小项目去 GitHub 找灵感;遇到不懂的先查 Ruby 官方文档(有中文版)。
← 上一篇 Ruby 实战项目
返回 Ruby 教程目录