Ruby 语法速查表 - Ruby 常用方法与惯用法大全
这张速查给刚开始写 Ruby(包括 Rails)或已写了一阵、想快速回忆惯用法的开发者。内容从变量、符号与常见内置方法开始,覆盖数组/哈希、控制流(含后置 if 和 unless)、方法与块/Proc/Lambda、类与模块(attr_accessor、include/extend、继承),最后是 begin-rescue-ensure 异常处理。与多数语言不同,Ruby 里几乎一切皆对象、方法默认返回最后一个表达式的值,块能优雅地封装遍历逻辑。读完你应能写出惯用且简洁的 Ruby 代码:用迭代器替代手写循环、用符号做哈希键、用模块组织可复用行为。
典型使用场景
面向用 Ruby 写脚本、处理数据或维护 Rails 项目的开发者。本表重点覆盖语言层最容易混淆的部分:符号 :symbol 与字符串的本质差别、块的三种传递与调用方式(yield / Proc.new / lambda)、attr_accessor 一行生成读写方法、以及 map/select/reject/partition/group_by 这条完整的集合处理链。日常任务里最常见的是「遍历数组做过滤再分组统计」,用 select + map + group_by + reduce 组合即可;遇到循环跳不出、变量意外为 nil、方法名冲突时,可直接翻易错点与排障段定位。类与混入、异常处理与 catch/throw 则对应更长期的代码组织需求。
基础语法 Basics 7
name = "Ruby"name = "Hello, #{name}":symbol# 这是单行注释=begin
注释块
=endniltrue / false数组与哈希 Array & Hash 9
arr = [1, 2, 3]arr << 4arr[0]arr.first / arr.lasthash = { a: 1, b: 2 }hash[:a]hash[:c] = 3hash.key?(:a)hash.keys / hash.values控制流 Control Flow 10
if x > 0
puts "positive"
endputs "positive" if x > 0unless x > 0
puts "not positive"
endcase x
when 1 then "one"
else "other"
end3.times { puts "hi" }1.upto(5) { |n| puts n }5.downto(1) { |n| puts n }loop { break if done }while x > 0
x -= 1
enduntil x == 0
x -= 1
end方法定义 Methods 9
def greet(name)
"Hello, #{name}"
enddef greet(name = "World")def sum(*args)def method(a:, b:)return valuedef method
yield if block_given?
endmy_proc = Proc.new { |x| x*2 }my_lambda = ->(x) { x*2 }proc.call(5)类与模块 Class & Module 10
class MyClass
endattr_accessor :nameattr_reader :nameattr_writer :nameclass Child < Parentmodule MyModule
endinclude MyModuleextend MyModuleself.class_methodclass << self
end异常处理 Exception 7
begin
risky_call
rescue
"error"
endrescue SpecificError => eensure
cleanup
endraise "error message"raise ArgumentError, "msg"retrycatch(:done) { throw(:done) }常用内置方法 Built-in Methods 10
arr.map { |x| x*2 }arr.select { |x| x.even? }arr.reject { |x| x.even? }arr.reduce(0) { |s, x| s + x }hash.merge({ c: 3 })"hello".upcase"hello".split("")"hello".gsub(/l/, "L")arr.flattenarr.uniq块与迭代器 Blocks & Iterators 7
[1, 2].each { |n| puts n }[1, 2].each do |n|
puts n
end(1..5).map { |n| n**2 }[1, 2, 3].select(&:even?)arr.partition { |x| x.even? }arr.chunk { |x| x > 0 }arr.group_by { |x| x % 3 }参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
name = "Ruby" | 动态类型变量赋值,无需声明类型 | name = "Ruby" |
"Hello, #{name}" | 双引号内字符串插值 | puts "Hello, #{name}" |
:symbol | 不可变且唯一的符号,适合做哈希键 | hash = { a: 1, b: 2 } |
arr << 4 | 向数组末尾追加元素 | arr = [1, 2, 3]; arr << 4 |
arr.first / arr.last | 取首元素 / 末元素 | arr.first |
hash.key?(:a) | 判断键是否存在 | hash.key?(:a) |
hash.keys / hash.values | 取出全部键 / 值 | hash.keys |
puts "positive" if x > 0 | 后置 if 条件修饰写法 | puts "positive" if x > 0 |
unless x > 0 ... end | 条件不成立时执行,相当于 if ! | unless x > 0; puts "not positive"; end |
case x when 1 then ... else ... end | 多分支匹配 | case x; when 1 then "one"; else "other"; end |
3.times { ... } | 按次数重复执行 | 3.times { puts "hi" } |
def greet(name = "World") | 定义带默认参数的方法 | def greet(name = "World"); "Hello, #{name}"; end |
def sum(*args) | splat 接收可变数量参数 | def sum(*args); args.sum; end |
def method(a:, b:) | 关键字参数,调用顺序无关 | def config(a:, b:); end |
def method; yield if block_given?; end | 在方法内调用传入的块 | def each_x; yield if block_given?; end |
Proc.new { |x| x*2 } / ->(x) { x*2 } | 创建 Proc 对象 / Lambda(箭头语法) | my_lambda = ->(x) { x * 2 }; my_lambda.call(5) |
attr_accessor :name | 一次生成 getter 与 setter | attr_accessor :name |
include MyModule / extend MyModule | 混入实例方法 / 混入类方法 | include MyModule |
arr.map { |x| x*2 } | 转换数组,返回新数组 | arr.map { |x| x * 2 } |
arr.select / arr.reject | 筛选保留 / 排除符合条件的元素 | arr.select { |x| x.even? } |
arr.reduce(0) { |s, x| s + x } | 归约累积求和 | arr.reduce(0) { |s, x| s + x } |
arr.group_by { |x| x % 3 } | 按键值分组,返回哈希 | arr.group_by { |x| x % 3 } |
arr.partition { |x| x.even? } | 一次拆成满足与不满足两组 | arr.partition { |x| x.even? } |
rescue SpecificError => e | 捕获指定类型异常并绑定变量 | begin; risky_call; rescue SpecificError => e; end |
ensure ... end | 无论成败都执行清理代码 | begin; risky_call; ensure; cleanup; end |
raise ArgumentError, "msg" | 抛出指定类型异常 | raise ArgumentError, "msg" |
catch(:done) { throw(:done) } | 非局部跳转,可跳出深层嵌套循环 | catch(:done) { throw(:done) } |
易错点与避坑指南
现象哈希用字符串键查不到值,输出恒为 nil,明明刚写进去过
原因符号 :a 和字符串 "a" 是两种不同的对象,哈希按键的类型与相等性严格匹配,键不会被自动转换。
处置统一约定使用符号键(hash[:a]),或在读取端显式写 hash[sym.to_s] / hash[key.to_sym] 完成转换。
现象用 map 想「原地」改数组,结果原数组没变,脚本后续还用旧数据
原因map、select、reject 都返回新数组,不修改原对象;只有 <<、[]= 之类的写法会改动原数组。
处置显式接住返回值:arr = arr.map { |x| x * 2 };或改用 select! / map! 这类带叹号的原地版本。
现象循环里用 break 后外层代码被跳过一大段,逻辑像随机执行
原因块中的 break 会终止整个方法调用所在的循环(跳出方法),而不是只跳出块自身。
处置需要只跳出迭代时用 next(跳过本次);需要跳出多层嵌套用 catch(:done) { throw(:done) } 配对使用。
现象方法执行到中途没有 return,外部拿到的却是块内最后一次表达式的中间值
原因Ruby 方法返回最后一条表达式的值;块内的赋值或遍历会改变「最后一句」的含义,容易误判返回值。
处置把真正的返回值写在方法最后一行,或显式写 return value。
现象定义了带 yield 的方法,但直接当普通方法调用时报 LocalJumpError 之类的问题
原因yield 只有在调用方传入块时才有效,没有块就等于没有可执行的块体。
处置用 block_given? 判断后再 yield,或给调用处补上 { ... } / do ... end 块。
现象begin...rescue 没拦住错误,异常照样冒到顶层终止脚本
原因rescue 必须紧跟在其保护的 begin 块中;把 rescue 写在与 begin 不同的层级会形成两块不同作用域,异常不会被捕获。
处置把可能抛错的调用完整包进 begin ... rescue => e ... ensure ... end 结构里,检查缩进层级是否正确。
现象给类加了一堆方法后,同名实例方法被覆盖,行为悄悄变了
原因include 混入的模块方法会进入祖先链,优先级高于类自身定义,容易覆盖同名逻辑。
处置明确职责边界:通用能力放模块用 include,只服务于该类自身实例的逻辑直接定义在类里;类方法用 extend 单独混入。
现象调试时把 {} 写很长一行,块与相邻方法调用黏在一起难以断点
原因{} 绑定优先级较高,长表达式中容易和后续代码混淆;do...end 与 {} 在部分场景绑定规则不同。
处置多行块一律改用 do ... end,单行才用 {};配合 Alt+Enter(编辑器)等快速修复能力定位。
排障路径
1确认哈希键类型与内容
hash.keys; hash.key?(:a); hash[:a]; hash["a"]keys 输出能看出键究竟是符号还是字符串;同一份数据分别用 :a 和 "a" 取值,只有匹配类型的那次有值,另一次第返回 nil,据此即可定位符号/字符串不匹配问题。
2查清数组里到底装了什么
arr = [1, 2, 3]; arr.map { |x| x * 2 }; arr; arr.select { |x| x.even? }; arr.uniqmap 的返回值是新数组,arr 本身不变;select 返回筛选后的新数组;uniq 去重。想确认修改是否生效,请看打印出的 arr 本身而不是链式中间结果。
3区分一次拆分与多次筛选
arr = [1,2,3,4,5]; even, odd = arr.partition { |x| x.even? }; even; oddpartition 一次遍历把元素分成两组并返回两个数组,多重赋值可直接解包。若结果不符合预期,说明判断条件 x.even? 与元素类型不匹配(例如元素本身是字符串)。
4按条件分组统计
arr.group_by { |x| x % 3 }; arr.group_by { |x| x % 3 }.map { |k, v| [k, v.size] }group_by 返回「键 => 元素数组」的哈希;再对每个值取 size 就得到各组数量,能快速确认分组逻辑是否符合预期。
5确认块是否真的被执行
def each_x; yield if block_given?; "done"; end; each_x { |v| puts v }如果没有任何输出,说明块_given? 判断失败或块未传入;"done" 仍被返回说明方法体走到了末尾,可以据此区分「块没执行」与「方法没被调用」。
6规约求和并处理空数组
arr.reduce(0) { |s, x| s + x }; [].reduce(0) { |s, x| s + x }给出初始值 0 时空数组也能安全返回 0;若省略初始值直接 arr.reduce { |s, x| s + x },空数组会因无初值而报错,脚本里建议始终显式传初值。
命令示例
链式枚举把数据清洗写清楚
arr = [1, 2, 3, 4, 5, 6]
arr.select(&:even?).map { |x| x * 10 }.reduce(0) { |s, x| s + x }select/map/reduce 各自返回新集合、不改原数组,可以放心串起来;&:even? 是把方法名变成 symbol 传给块的简写,少一次 |x| 样板。
输出
=> 120
按条件分组看分布
arr = [1, 2, 3, 4, 5]
arr.group_by { |x| x % 3 }
arr.partition { |x| x > 3 }group_by 返回哈希便于按桶统计;partition 返回两个数组,第一个元素是通过的那组,顺序固定但容易记反。
输出
{0=>[3], 1=>[1, 4], 2=>[2, 5]}
[[4, 5], [1, 2, 3]]字符串替换与切分
log = "2024-05-01 ERROR timeout"
log.gsub(/\d{4}-\d{2}-\d{2}/, "DATE")
log.split(" ")gsub 里传正则才能处理日期这类模式,传字符串只做字面替换。gsub 返回新字符串,原变量不变,不需要 destructive_ 前缀那套写法。
输出
=> "DATE ERROR timeout" => ["2024-05-01", "ERROR", "timeout"]
关键字参数让调用处自解释
def connect(host:, port: 443, timeout: 30)
puts "#{host}:#{port} in #{timeout}s"
end
connect(host: "db.internal")关键字参数顺序无关、忘传会直接 ArgumentError 而不是错位到别的参数上,调用 connect("a", "b") 这种位置参数的笔误因此被挡在编译期之外。
输出
=> db.internal:443 in 30s
类骨架与 attr_accessor
class User
attr_accessor :name
def initialize(name)
@name = name
end
end
User.new("Tom").nameattr_accessor 一行生成读取与赋值两个方法;用 attr_reader 则外部只能读。实例变量 @name 必须在 initialize 里赋值,否则读取时得到 nil 而不报错。
输出
=> "Tom"
常见坑与注意事项
- { } 块比 do...end 绑定得更紧:array.each { puts method } 里的 method 会被当成块参数传入并 ArgumentError,必须改用 do...end。
- 数组索引从 0 开始,但 arr[-1] 取的是最后一个元素而不是倒数第一之前的语义直觉值(它是倒数第一个);off-by-one 在切片时会连带出错。
- 方法体最后一个表达式的值会自动成为返回值,中途写了一句 puts 或日志语句就把返回值改掉了,返回 nil 时调用方很难看出原因。
- rescue 不带类型会捕获 StandardError 之外的大量异常,写成裸 rescue 还可能把 Ctrl+C 的 Interrupt 一起吞掉,使脚本无法中断。
- proc.call 与 lambda.call 的参数检查不同:lambda 参数个数必须严格匹配,少传一个就 ArgumentError;把 lambda 当 proc 用会省掉这层保护。
- 字符串在 Ruby 中是可变的,str << "x" 会原地修改。若这个字符串来自 hash 的键或常量,直接改会连键一起污染,报 key not found。
提示
- Ruby 中一切皆对象,包括 nil 和数字
- each 是最常用的迭代器,map 用于转换,select 用于筛选
- 符号 :symbol 不可变且唯一,适合做哈希键
- attr_accessor 自动生成 getter 和 setter
- 块(block)是 Ruby 最强大的特性,do...end 和 {} 等价
常见问题
map、select、reject 该挑哪一个,差别体现在返回值上吗?
三者的差别正是返回值:map 逐元素转换并返回等长的新数组;select 只返回块为真的元素,长度会变;reject 返回块为假的元素,常与 select 配对使用,例如 arr.partition { |x| x.even? } 一次就拿到 [偶数, 奇数] 两组。只求遍历副作用时用 each,它的返回值是原数组本身,误用 map 只会白做一份拷贝。
符号 :symbol 和字符串 "symbol" 用在哈希键上该怎么选?
符号不可变且在程序运行期只有一份实例,适合做哈希键和 attr_accessor :name 这类方法名,查找快、内存省;字符串可变,每次字面量求值都可能新建对象,用作键时哈希还要额外计算字符串内容哈希。约定是:标识符一律用符号 :a,需要拼接、来自用户输入或要与外部 JSON 字符串互换时才用字符串,注意 hash.key?(:a) 与 hash.key?("a") 在 Ruby 里互不命中。
Proc 和 Lambda 用起来有什么区别,选错了会有什么后果?
两者都用块语法创建(Proc.new { |x| x*2 } 与 ->(x) { x*2 }),差别在调用检查:Lambda 是严格的,参数个数必须与定义一致,return 会从定义它的方法返回;Proc 是宽松的,参数可多可少,块内的 return 会引发 LocalJumpError。回调若由外部代码决定传几个参数,或依赖 return 语义,用 Lambda 更安全;不确定时优先 -> 箭头语法。
怎么给类批量生成读写访问器,为什么不手写两个方法?
用 attr_accessor :name 一次性生成 :name 的读取与赋值方法,attr_reader :name 只生成 getter(字段对外只读时用,避免外部绕过校验直接改状态),attr_writer :name 只生成 setter。手写重复的 get/set 既冗长又容易在改名时漏改。定义在类体里的是实例方法;要让模块的方法变成类方法,改用 extend MyModule,而 include 只混入实例方法。
循环里想提前退出,break、next 和 catch/throw 怎么选?
break 立即跳出最内层的 loop 或 3.times { } 块;next 相当于 continue,跳过本次迭代继续下一轮。两者都只影响一层循环,嵌套结构里想从最深处终止外层循环就不够用了——这时用非局部跳转:catch(:done) { throw(:done) },配合 break 或 return 从任意深度退出单个块,是 Ruby 处理生成器式提前终止的惯用手段。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。