Ruby 入门指南 / 第 04 章:变量与数据类型
第 04 章:变量与数据类型
“数据!数据!数据!没有黏土我做不出砖。” —— 福尔摩斯
4.1 变量类型
Ruby 有五种变量类型,通过前缀区分:
4.1.1 变量类型概览
| 类型 | 前缀 | 作用域 | 示例 |
|---|---|---|---|
| 局部变量 | 无 | 当前作用域 | name = "Matz" |
| 实例变量 | @ | 当前对象 | @name = "Matz" |
| 类变量 | @@ | 类及其子类 | @@count = 0 |
| 全局变量 | $ | 整个程序 | $stdout, $LOAD_PATH |
| 常量 | 大写字母 | 定义的模块/类 | PI = 3.14159 |
4.1.2 局部变量
# 局部变量:小写字母或下划线开头
name = "Ruby"
user_age = 30
_max_value = 100
# 作用域限制
def greet
message = "Hello" # 只在方法内有效
puts message
end
# puts message # => NameError: undefined local variable
# 块的作用域(Ruby 2.x+)
(1..3).each do
block_var = "在块内"
puts block_var # => "在块内"
end
# puts block_var # => NameError(Ruby 1.9 之前是外部作用域)
# 块参数不影响外部
x = 10
[1, 2, 3].each { |x| puts x }
puts x # => 10(外部 x 未被修改)
4.1.3 实例变量
class User
def initialize(name, age)
@name = name # 实例变量,属于当前对象
@age = age
end
def info
"#{@name}, #{@age}岁"
end
def name
@name # 访问器(通常用 attr_reader 替代)
end
end
user1 = User.new("Alice", 25)
user2 = User.new("Bob", 30)
puts user1.info # => "Alice, 25岁"
puts user2.info # => "Bob, 30岁"
# 实例变量在初始化之前是 nil
class Demo
def check
puts @undefined_var.inspect # => nil
end
end
Demo.new.check
4.1.4 类变量
class Counter
@@count = 0
def initialize
@@count += 1
end
def self.total
@@count
end
end
Counter.new
Counter.new
Counter.new
puts Counter.total # => 3
# ⚠️ 注意:类变量在继承中共享
class Parent
@@shared = "parent"
def self.shared; @@shared; end
end
class Child < Parent
@@shared = "child"
end
puts Parent.shared # => "child"(被子类修改了!)
⚠️ 警告:类变量在继承中共享,容易引发意外副作用。推荐使用实例变量配合类方法。
4.1.5 全局变量
# 全局变量以 $ 开头
$app_name = "MyApp"
$app_version = "1.0.0"
# Ruby 预定义的全局变量
puts $PROGRAM_NAME # 当前脚本名
puts $LOAD_PATH # 加载路径数组
puts $stdin # 标准输入
puts $stdout # 标准输出
puts $stderr # 标准错误
puts $DEBUG # 调试模式标志
# 建议:尽量避免使用自定义全局变量
# 优先使用常量、配置对象或依赖注入
4.1.6 常量
# 常量:大写字母开头
PI = 3.14159265358979
MAX_RETRIES = 3
API_BASE_URL = "https://api.example.com"
# 常量可以被重新赋值(但会发出警告)
PI = 3.14 # warning: already initialized constant PI
# 类和模块也是常量
String # => String
Math::PI # => 3.14159265358979
# 嵌套常量
module Payment
class Gateway
TIMEOUT = 30
MAX_AMOUNT = 1_000_000
end
end
puts Payment::Gateway::TIMEOUT # => 30
4.2 数字类型
4.2.1 整数(Integer)
# 整数类型
42.class # => Integer
-10.class # => Integer
0.class # => Integer
# 不同进制表示
0b1010 # => 10(二进制)
0o17 # => 15(八进制)
0xFF # => 255(十六进制)
0d42 # => 42(十进制,显式)
# 下划线分隔(提高可读性)
1_000_000 # => 1000000
123_456_789 # => 123456789
# 常用方法
42.even? # => true
42.odd? # => false
42.positive? # => true
42.negative? # => false
42.zero? # => false
42.abs # => 42
(-42).abs # => 42
42.next # => 43
42.pred # => 41
42.digits # => [2, 4]
# 数学运算
2 + 3 # => 5(加法)
10 - 4 # => 6(减法)
3 * 4 # => 12(乘法)
10 / 3 # => 3(整数除法,截断)
10.0 / 3 # => 3.3333...(浮点除法)
10 % 3 # => 1(取模)
2 ** 10 # => 1024(幂运算)
# 整数除法陷阱
1 / 2 # => 0(不是 0.5!)
1.0 / 2 # => 0.5
1 / 2.0 # => 0.5
1.fdiv(2) # => 0.5(强制浮点除法)
1.quo(2) # => (1/2)(有理数)
4.2.2 浮点数(Float)
# 浮点数
3.14.class # => Float
-0.5.class # => Float
1.0e10 # => 10000000000.0(科学计数法)
1_000.50 # => 1000.5
# 浮点数精度问题
0.1 + 0.2 # => 0.30000000000000004
0.1 + 0.2 == 0.3 # => false!
# 解决方案:使用有理数或 BigDecimal
require "bigdecimal"
require "bigdecimal/util"
a = BigDecimal("0.1")
b = BigDecimal("0.2")
(a + b).to_f # => 0.3
# 或使用有理数
1r / 10 + 2r / 10 # => (3/10)
(1r / 10 + 2r / 10) == 3r / 10 # => true
# 常用方法
3.14.round # => 3
3.14.ceil # => 4
3.14.floor # => 3
3.14.truncate # => 3
3.14.to_i # => 3
3.14.finite? # => true
Float::INFINITY.finite? # => false
Float::NAN.nan? # => true
4.2.3 有理数(Rational)和复数(Complex)
# 有理数
1/3r # => (1/3)
2/3r + 1/3r # => (1/1)
0.5.to_r # => (1/2)
Rational(2, 6) # => (1/3)
# 复数
2 + 3i # => (2+3i)
Complex(2, 3) # => (2+3i)
(2 + 3i).real # => 2
(2 + 3i).imag # => 3
4.2.4 数字格式化
# 数字格式化
format("%d", 42) # => "42"
format("%05d", 42) # => "00042"
format("%.2f", 3.14159) # => "3.14"
format("%e", 1234.5) # => "1.234500e+03"
# 千位分隔符(Ruby 没有内置,需自定义)
def format_number(n)
n.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
end
format_number(1234567) # => "1,234,567"
# 字节数格式化
def human_size(bytes)
units = %w[B KB MB GB TB]
return "0 B" if bytes.zero?
exp = (Math.log(bytes) / Math.log(1024)).to_i
exp = units.length - 1 if exp >= units.length
"%.1f %s" % [bytes.to_f / 1024**exp, units[exp]]
end
human_size(1536) # => "1.5 KB"
human_size(1048576) # => "1.0 MB"
4.3 字符串类型
4.3.1 字符串创建
# 单引号字符串(不支持插值和转义)
'Hello, World!'
'No escape: \n' # => "No escape: \n"
'It\'s Ruby' # => "It's Ruby"
# 双引号字符串(支持插值和转义)
"Hello, #{name}!"
"New line: \n"
"Unicode: \u{1F600}" # => "Unicode: 😀"
# 多行字符串 - heredoc
sql = <<~SQL
SELECT *
FROM users
WHERE age > 18
ORDER BY name
SQL
# heredoc 类型
<<~HEREDOC # 去除缩进的 heredoc
This is
a multi-line
string
HEREDOC
# %q 和 %Q(类似 Perl 的引号运算符)
%q[单引号字符串,不插值]
%Q[双引号字符串,支持插值 #{1 + 1}]
%q{Hello World}
%q(Hello World)
%Q|Hello #{name}|
# 字符串数组
%w[hello world foo bar] # => ["hello", "world", "foo", "bar"]
%W[hello #{name} foo] # => ["hello", "Ruby", "foo"](支持插值)
%i[foo bar baz] # => [:foo, :bar, :baz](符号数组)
4.3.2 字符串操作
# 字符串拼接
"Hello" + " " + "World" # => "Hello World"
"Hello" << " " << "World" # 原地修改(更高效)
"Hello" * 3 # => "HelloHelloHello"
# 字符串长度
"Hello".length # => 5
"Hello".size # => 5(同 length)
"Hello".bytesize # => 5
"你好".length # => 2
"你好".bytesize # => 6(UTF-8 编码)
# 访问字符
"Hello"[0] # => "H"
"Hello"[-1] # => "o"
"Hello"[1..3] # => "ell"
"Hello"[1, 3] # => "ell"(从位置 1 开始,取 3 个字符)
"Hello".chars # => ["H", "e", "l", "l", "o"]
"Hello".bytes # => [72, 101, 108, 108, 111]
# 查找
"Hello World".include?("World") # => true
"Hello World".index("World") # => 6
"Hello World".rindex("l") # => 9(从右向左)
"Hello World".match(/World/) # => MatchData
"Hello World".start_with?("Hell") # => true
"Hello World".end_with?("rld") # => true
# 替换
"Hello World".sub("World", "Ruby") # => "Hello Ruby"(第一次)
"llo".gsub("l", "L") # => "LLo"(所有)
"Hello World".gsub(/\s+/, "-") # => "Hello-World"
"Hello World".tr("A-Z", "a-z") # => "hello world"
# 大小写
"hello".upcase # => "HELLO"
"HELLO".downcase # => "hello"
"hello world".capitalize # => "Hello world"
"hello world".swapcase # => "HELLO WORLD"
# 去除空白
" hello ".strip # => "hello"
" hello ".lstrip # => "hello "
" hello ".rstrip # => " hello"
"hello\n".chomp # => "hello"
"hello\n".chop # => "hell"(删除最后一个字符)
# 分割和合并
"hello world".split(" ") # => ["hello", "world"]
"a,b,c".split(",") # => ["a", "b", "c"]
"hello".split("") # => ["h", "e", "l", "l", "o"]
["a", "b", "c"].join(", ") # => "a, b, c"
# 填充
"42".rjust(5, "0") # => "00042"
"42".ljust(5, ".") # => "42..."
"hello".center(11, "-") # => "---hello---"
# 编码
"Hello".encoding # => #<Encoding:UTF-8>
"Hello".encode("ASCII") # => "Hello"
"Hello".force_encoding("ISO-8859-1")
4.4 符号(Symbol)
4.4.1 符号基础
# 符号是不可变的标识符
:name.class # => Symbol
:hello.class # => Symbol
# 符号是唯一的
:name.object_id == :name.object_id # => true
"hello".object_id == "hello".object_id # => false(每次创建新对象)
# 创建符号
:hello # 字面量
"hello".to_sym # 字符串转符号
"hello".intern # 同上
Symbol.new("hello") # 构造函数
# 符号转字符串
:hello.to_s # => "hello"
:hello.inspect # => ":hello"
# 带空格和特殊字符的符号
:"hello world"
:"user@name"
:"#{dynamic_name}" # 动态符号(支持插值)
4.4.2 符号的用途
# 1. 哈希键(最常见的用途)
user = { name: "Alice", age: 25, city: "Beijing" }
user[:name] # => "Alice"
# 2. 方法参数标识
def connect(host:, port: 80, ssl: false)
puts "Connecting to #{host}:#{port} (SSL: #{ssl})"
end
connect(host: "example.com", ssl: true)
# 3. 枚举值
DIRECTIONS = %i[north south east west].freeze
# 4. 反射和元编程
String.instance_method(:upcase) # => #<UnboundMethod: String#upcase>
:to_s.to_proc # => #<Proc:>
# 5. Rails 路由和配置
# get '/users', to: 'users#index'
# validates :name, presence: true
4.4.3 符号 vs 字符串
| 特性 | 符号 (Symbol) | 字符串 (String) |
|---|---|---|
| 可变性 | 不可变 | 可变(默认) |
| 唯一性 | 全局唯一 | 每次创建新对象 |
| 内存 | 创建后常驻内存 | 可被垃圾回收 |
| 比较速度 | 非常快(比较 ID) | 较慢(比较内容) |
| 适用场景 | 标识符、键名 | 文本内容、数据处理 |
| 创建代价 | 首次创建后不再分配 | 每次都分配内存 |
# 性能对比
require "benchmark"
n = 1_000_000
Benchmark.bm do |x|
x.report("Symbol:") { n.times { :name } }
x.report("String:") { n.times { "name" } }
end
# Symbol 明显更快,因为它是同一个对象
4.5 范围(Range)
4.5.1 范围基础
# 范围类型
(1..10) # 闭区间:1 到 10(包含 10)
(1...10) # 半开区间:1 到 10(不包含 10)
("a".."z") # 字符范围
("A".."Z") # 大写字母范围
# 范围转数组
(1..5).to_a # => [1, 2, 3, 4, 5]
(1...5).to_a # => [1, 2, 3, 4]
# 范围判断
(1..10).include?(5) # => true
(1..10).include?(10) # => true
(1...10).include?(10) # => false
(1..10).cover?(5) # => true(更快,但只适用于连续范围)
# 范围属性
(1..10).first # => 1
(1..10).last # => 10
(1..10).min # => 1
(1..10).max # => 10
(1..10).size # => 10
4.5.2 范围的应用
# 1. 迭代
(1..5).each { |i| puts i }
(1..5).map { |i| i ** 2 } # => [1, 4, 9, 16, 25]
(1..10).select(&:even?) # => [2, 4, 6, 8, 10]
# 2. case 语句
score = 85
grade = case score
when 90..100 then "A"
when 80...90 then "B"
when 70...80 then "C"
when 60...70 then "D"
else "F"
end
puts grade # => "B"
# 3. 字符串范围
("a".."e").to_a # => ["a", "b", "c", "d", "e"]
("aa".."ad").to_a # => ["aa", "ab", "ac", "ad"]
# 4. 日期范围
require "date"
(Date.today..Date.today + 7).each do |date|
puts date.strftime("%Y-%m-%d")
end
# 5. 无限范围(Ruby 2.6+)
(1..).first(5) # => [1, 2, 3, 4, 5]
(..10).to_a # => [1, 2, 3, ..., 10](实际不工作,需要自定义)
4.6 布尔值和 Nil
4.6.1 真值和假值
# Ruby 中只有 false 和 nil 是"假值"
# 其他一切都是"真值",包括:
# 0, "", [], {}, "false", 0.0 都是 true!
if 0
puts "0 是真值!" # 会执行
end
if ""
puts "空字符串是真值!" # 会执行
end
if nil
puts "nil 是假值" # 不会执行
end
if false
puts "false 是假值" # 不会执行
end
4.6.2 nil 对象
# nil 是 NilClass 的唯一实例
nil.class # => NilClass
nil.nil? # => true
nil.to_s # => ""
nil.to_i # => 0
nil.to_a # => []
nil.to_h # => {}
nil.to_f # => 0.0
nil.inspect # => "nil"
nil.object_id # => 8(固定值)
# nil 检查
value = nil
value.nil? # => true
value&.upcase # => nil(安全导航操作符)
value || "default" # => "default"
value&.length # => nil
4.7 真值和假值判断
4.7.1 安全导航操作符
# Ruby 2.3+ 安全导航操作符 (&.)
user = nil
user&.name # => nil(不会报错)
user&.name&.length # => nil(链式安全导航)
user = { name: "Alice" }
user&.[](:name) # => "Alice"
# 等价于
user && user[:name]
4.7.2 条件赋值
# ||= 条件赋值
name ||= "default" # 如果 name 是 nil 或 false,则赋值
# 等价于
name = name || "default"
# Ruby 3.1+ 条件赋值简化
# data ||= [] 等价于 data ||= []
# &&= 条件执行
user = { name: "Alice", email: nil }
user[:email] &&= user[:email].downcase # 只在非 nil 时执行
4.8 类型检查与转换
4.8.1 类型检查
# 类型检查
42.is_a?(Integer) # => true
42.is_a?(Numeric) # => true
42.is_a?(String) # => false
42.instance_of?(Integer) # => true
42.kind_of?(Numeric) # => true(is_a? 的别名)
# 类型比较
Integer === 42 # => true(用于 case 语句)
String === "hello" # => true
# case 语句中的类型匹配
case value
when Integer then puts "整数"
when String then puts "字符串"
when Array then puts "数组"
end
4.8.2 类型转换
# 字符串转数字
"42".to_i # => 42
"3.14".to_f # => 3.14
"0xFF".to_i(16) # => 255(指定进制)
"abc".to_i # => 0(转换失败返回 0)
Integer("abc") # => ArgumentError(更严格)
# 数字转字符串
42.to_s # => "42"
3.14.to_s # => "3.14"
255.to_s(16) # => "ff"(指定进制)
# 字符串转数组/哈希
"[1,2,3]".scan(/\d+/).map(&:to_i) # => [1, 2, 3]
# 转换方法一览
# to_i - 转整数
# to_f - 转浮点
# to_s - 转字符串
# to_sym - 转符号
# to_a - 转数组
# to_h - 转哈希
# to_r - 转有理数
# to_c - 转复数
4.9 常量和冻结对象
4.9.1 冻结字符串
# frozen_string_literal: true(文件顶部魔法注释)
# 所有字符串字面量自动冻结
# 手动冻结
str = "hello".freeze
str << " world" # => FrozenError: can't modify frozen string
# frozen_string_literal: true
# 该文件中的所有字符串字面量都是冻结的
str = "hello"
str.frozen? # => true
4.9.2 不可变数据
# 冻结对象
data = { name: "Alice", age: 25 }.freeze
data[:name] = "Bob" # => FrozenError
# 深冻结(Ruby 3.x)
data = { name: "Alice" }.freeze
# 注意:freeze 只冻结顶层,嵌套对象需要递归冻结
4.10 动手练习
- 变量练习
# 创建以下变量并验证类型
name = "Ruby Learner"
age = 25
scores = [95, 87, 92, 78, 100]
config = { debug: true, log_level: :info }
PI = 3.14159
# 打印每个变量的类型
[name, age, scores, config, PI].each do |var|
puts "#{var.inspect} => #{var.class}"
end
- 数字格式化
# 格式化数字为千位分隔符格式
numbers = [1234, 1234567, 1234567890]
numbers.each do |n|
# 你的代码...
end
# 预期输出:1,234 | 1,234,567 | 1,234,567,890
- 字符串处理
# 统计字符串中每个单词出现的次数
text = "the quick brown fox jumps over the lazy dog the fox"
# 你的代码...
# 预期输出:{"the"=>3, "quick"=>1, "brown"=>1, "fox"=>2, ...}
4.11 本章小结
| 要点 | 说明 |
|---|---|
| 变量类型 | 局部、实例、类、全局、常量,通过前缀区分 |
| 整数 | 无限精度,支持多种进制表示 |
| 浮点数 | 注意精度问题,可用 BigDecimal 或 Rational |
| 字符串 | 可变,支持插值、heredoc、丰富的操作方法 |
| 符号 | 不可变标识符,适合做键名和参数标识 |
| 范围 | 表示连续区间,用于迭代和条件判断 |
| 真值 | 只有 false 和 nil 是假值 |
📖 扩展阅读
- Ruby 数字文档
- Ruby 字符串文档
- Ruby 符号文档
- Ruby 范围文档
- 《Effective Ruby》—— Peter J. Jones 著
上一章:← 第 03 章:Hello World 下一章:第 05 章:控制流程 →