数据类型
这一课你将能做到
- 正确使用 Integer、Float、String、Symbol、Array、Hash、Range、nil 八种核心类型
- 说清 Symbol 与 String 的本质区别,知道什么时候该用哪个
- 用 Array/Hash 的常用方法完成增删改查,并理解 Ruby 的「一切皆对象」
为什么要学
Ruby 代码 80% 的操作是在字符串、数组、哈希之间倒腾数据。Rails 里从数据库取一行是 Hash 风格的访问,路由参数是 Hash,配置是 Hash——Hash 和 Symbol 不熟,Rails 源码一行都读不下去。
人话:类型是盛放数据的容器。写业务代码就是不停地把数据从一种容器倒进另一种容器,再筛一遍。容器认不全,等于进了厨房不认识锅碗瓢盆。
先建立图像
Ruby 常见类型一览
┌──────────┬───────────────────┬────────────────────┐
│ 类型 │ 字面量示例 │ 一句话定位 │
├──────────┼───────────────────┼────────────────────┤
│ Integer │ 42, -7 │ 任意精度整数 │
│ Float │ 3.14 │ 双精度浮点 │
│ String │ "hello" │ 可变字符序列 │
│ Symbol │ :name │ 不可变「名字标签」 │
│ Boolean │ true / false │ 仅两个实例 │
│ NilClass │ nil │ 「什么都没有」的对象 │
│ Array │ [1, "a", :x] │ 有序、可混合类型 │
│ Hash │ {name: "Tom"} │ 键值对(保持插入序) │
│ Range │ (1..5), ("a".."z")│ 区间 │
└──────────┴───────────────────┴────────────────────┘
关键心智模型:Ruby 中一切值都是对象,包括数字和 nil。1.class #=> Integer,nil.class #=> NilClass。没有 Java 那种基本类型/包装类的分裂。
跟着做
步骤 1:数字与布尔
新建 types.rb:
# 数字:Integer 没有溢出问题,自动变大数
big = 2 ** 100
puts big # 一个 31 位数
puts 7 / 2 # => 3 整数除法
puts 7.0 / 2 # => 3.5 有一个是浮点就是浮点结果
puts 7.divmod(2).inspect # => [3, 1] 商和余数
# 只有 false 和 nil 是「假」,其余全为真——包括 0 和空字符串!
puts 0 ? "0 是真" : "0 是假" # => 0 是真(Java/JS 程序员最易踩)
puts "" ? "空串是真" : "空串是假" # => 空串是真
步骤 2:字符串与插值
name = "小明"
# 双引号支持插值 #{},单引号完全不解析
puts "你好,#{name},明年 #{18 + 1} 岁" # => 你好,小明,明年 19 岁
puts '你好,#{name}' # => 你好,#{name}(原样输出)
s = "ruby"
puts s.upcase # => "RUBY" 不改原串
puts s # => "ruby"
s.upcase! # 带 ! 的方法原地修改
puts s # => "RUBY"
# 常用:分割、去空白、包含判断
"a,b,c".split(",") # => ["a", "b", "c"]
" hi ".strip # => "hi"
"hello world".include?("wor") # => true
步骤 3:Symbol——Ruby 特有
# Symbol 是不可变的「名字」,写一次内存里只存一份
:name.object_id == :name.object_id # => true,同一个对象
"a".object_id == "a".object_id # => false,两个字符串对象
# 典型用途:Hash 的键、方法名、状态标签
status = :pending
user = { name: "Tom", role: :admin } # name: 是 :name 的语法糖
puts user[:name] # => Tom
puts status == "pending" # => false!Symbol 不等于 String
步骤 4:Array 增删改查
arr = [3, 1, 2]
arr << 4 # 尾部追加 => [3,1,2,4]
arr.push(5) # 同上
arr.unshift(0) # 头部插入 => [0,3,1,2,4,5]
arr.pop # 删除尾部 => 5
puts arr.first.inspect # => 0
puts arr.last.inspect # => 4
puts arr[1..2].inspect # => [3, 1] 区间切片
puts arr.sort.inspect # => [0,1,2,3,4] 不改原数组
puts arr.map { |n| n * 10 }.inspect # => [0,30,10,20,40]
puts arr.select { |n| n.even? }.inspect # => [0,2,4]
puts arr.sum # => 10
步骤 5:Hash 与 Range
# Hash:保持插入顺序
prices = { apple: 3, banana: 2 }
prices[:orange] = 5 # 增
prices[:apple] = 4 # 改
prices.delete(:banana) # 删
puts prices[:apple] # => 4
puts prices[:missing].inspect # => nil(不存在不报错)
puts prices.fetch(:missing, 0) # => 0(fetch 可给默认值)
prices.each do |fruit, price|
puts "#{fruit}: #{price} 元"
end
puts prices.keys.inspect # => [:apple, :orange]
# Range:连续区间
(1..5).to_a # => [1,2,3,4,5] 双点含尾
(1...5).to_a # => [1,2,3,4] 三点不含尾
("a".."e").to_a # => ["a","b","c","d","e"]
(1..10).step(2).to_a # => [1,3,5,7,9]
运行 ruby types.rb 确认每一步输出。
讲清楚要点
- 一切皆对象:
nil 是 NilClass 的唯一实例,有自己的方法(nil.to_s、nil.nil?)。这统一了模型,也是 Ruby 优雅的根基。
- Symbol vs String:Symbol 不可变、同一名字全局唯一、比较是 O(1) 指针比较;String 可变、每次字面量都新建对象。规则:当「名字/标签」用 Symbol,当「内容数据」用 String。
- 可变性与
! 约定:方法名带 ! 表示原地修改接收者(危险版),不带则返回新对象。sort vs sort!、upcase vs upcase!。注意 ! 只是命名约定,不是语言机制。
- Hash 键不自动转换:
{name: "x"}["name"] 取不到值——:name(Symbol)和 "name"(String)是两个键。Rails 的 HashWithIndifferentAccess 才做这种宽容处理。
- 真值表极简:全 Ruby 只有
false 和 nil 为假。0、""、[] 都是真。与 JavaScript 完全不同,务必刻进脑子。
你可能会懵的地方
-
为什么 Ruby 要发明 Symbol 这种怪东西?
为了表达「这是一个标识符,不是一段文本」。Hash 键、方法名这类场景需要快速比较和不可变性,Symbol 正好是。它还省了字符串驻留的开销。
-
** frozen_string_literal 是什么?**
Ruby 3 中文件首行加 # frozen_string_literal: true,该文件所有字符串字面量自动冻结不可变,提升性能。新文件建议都加。
-
Array 能装不同类型吗?
能。[1, "a", :sym, [2, 3]] 完全合法。但实务上保持同类型,遍历处理时才不出幺蛾子。
-
arr[10] 越界会怎样?
返回 nil,不抛异常。这是 Ruby 的宽容风格,但也意味着拼错下标不会报错——调试时注意。
-
Range 的 .. 和 ... 哪个常用?
..(含尾)更常用。... 主要用于数组切片 arr[0...3](取前 3 个)和 Rails 时间区间。
错法对照
| 错法 | 问题 | 正解 |
|---|
if count == 0 当假判断后写 else | Ruby 中 0 为真,逻辑反了 | 显式 count.zero? 或牢记 0 是真 |
| 用字符串当 Hash 键又混用 symbol | 取值莫名 nil | 全项目统一用 symbol 键 |
s.upcase 后直接打印 s | 以为已修改,其实返回新串 | 要改原串用 upcase! 或重新赋值 |
prices[:miss] 报错预期异常 | Hash 缺键返回 nil 静默通过 | 需要严格时用 fetch(:key) |
"5" + 3 | TypeError,Ruby 不做隐式转换 | "5".to_i + 3 显式转换 |
卡点急救
- Hash 取不到值但 puts 明明有:99% 是 Symbol/String 键混用。
puts hash.keys 看真实键类型。
- 数字计算结果全是整数丢了小数:两个 Integer 相除。把任一操作数加
.0 或用 7.fdiv(2)。
- 字符串里中文乱码:文件保存为 UTF-8,且首行加
# encoding: UTF-8(Ruby 3 默认 UTF-8,一般不用加,Windows 旧编辑器除外)。
面试常问
-
Symbol 和 String 的区别?
可变性(Symbol 不可变)、内存(同名 Symbol 单例,同内容 String 多份)、用途(Symbol 做标识,String 装数据)、比较成本(Symbol O(1) 比较对象ID)。补一句 Ruby 2.2+ Symbol 会被 GC,不再有符号泄露 DoS 问题,更加分。
-
Ruby 的 nil 和其他语言的 null 有什么不同?
nil 是 NilClass 的对象实例,有方法(nil.to_a => []、nil.nil? => true),可以参与链式调用而不立刻 NPE(配合 &.)。null 只是空指针关键字。
-
==、eql?、equal? 区别?
== 值相等;eql? 更严格(区分 1 和 1.0),Hash 键比较用它;equal? 是对象身份(object_id)相等。
自测三问
0、""、[]、nil、false 中哪些在 if 里是「假」?
- 什么时候 Hash 键应该用 Symbol,什么时候用 String?
map 和 map! 的差别?! 后缀是语法还是约定?
小结一句话
记住「一切皆对象、只有 false 和 nil 为假、Symbol 是名字 String 是内容」,Ruby 类型系统就掌握了。
延伸