强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 动手练习

  1. 变量练习
# 创建以下变量并验证类型
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
  1. 数字格式化
# 格式化数字为千位分隔符格式
numbers = [1234, 1234567, 1234567890]
numbers.each do |n|
  # 你的代码...
end
# 预期输出:1,234 | 1,234,567 | 1,234,567,890
  1. 字符串处理
# 统计字符串中每个单词出现的次数
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 是假值

📖 扩展阅读


上一章← 第 03 章:Hello World 下一章第 05 章:控制流程 →