语法基础
这一课你将能做到
- 正确声明和使用局部变量、实例变量、类变量、全局变量和常量,说清各自的可见范围
- 掌握 Ruby 的运算符优先级、三目表达式、
||= 赋值风格以及 safe navigation 操作符 &.
- 写出符合 Ruby 社区命名规范(snake_case、
?/ ! 方法后缀)的代码
为什么要学
Ruby 语法表面看和 Python 很像,但有自己的一套规则——变量前缀 $/@/@@ 一眼就能看出作用域;||= 是处理默认值的惯用语法;safe navigation &. 防止 NPE 比 Java 更优雅。这些语法糖写对了极爽,写错了调试到怀疑人生。
人话:语法是语言的交通规则。你只有认了 def/end 之间的红绿灯、分得清 @name 和 name 的区别,才不至于一上路就撞。
先建立图像
变量作用域速查
┌─────────────────┬────────────┬──────────────────────┐
│ 变量类型 │ 前缀 │ 可见范围 │
├─────────────────┼────────────┼──────────────────────┤
│ 局部变量 │ 无前缀 │ 方法/块内部 │
│ 实例变量 │ @name │ 单个对象内 │
│ 类变量 │ @@count │ 整个类及子类(少用) │
│ 全局变量 │ $debug │ 全局(极少用) │
│ 常量 │ CONST_NAME │ 所在类/模块及外层 │
└─────────────────┴────────────┴──────────────────────┘
跟着做
步骤 1:变量与常量声明
# 局部变量:小写/下划线开头,只活在当前作用域
score = 88
puts score # => 88
# 实例变量:@,在类的方法之间共享
class Player
def initialize(name)
@name = name
end
def greet
"我是 #{@name}"
end
end
p = Player.new("阿强")
puts p.greet # => 我是 阿强
# 类变量:@@,类和所有实例共享,但子类继承可能踩坑,建议用类实例变量代替
class Counter
@@total = 0
def self.total
@@total
end
def self.increment!
@@total += 1
end
end
Counter.increment!
puts Counter.total # => 1
# 全局变量:$ 开头,所有地方可见,能不用就不用
$log_level = :info
puts $log_level
# 常量:全大写,定义后不能重新赋值(会报警告)
PI = 3.1415926
puts PI
# PI = 3 # => warning: already initialized constant PI
步骤 2:运算符与条件表达式
# ||= 惯用法:变量为 nil/false 时才赋值
config = nil
config ||= { theme: "dark" }
puts config.inspect # => {:theme=>"dark"}
config ||= { theme: "light" }
puts config.inspect # => {:theme=>"dark"} 已被赋值过,不覆盖
# safe navigation:对象的链式调用里防 nil
user = { profile: { name: "Lisa" } }
puts user[:profile]&.fetch(:name, "未知") # => Lisa
puts user[:ghost]&.fetch(:name, "未知") # => nil(如果没 & 会抛 NoMethodError)
# 三目表达式
age = 20
status = age >= 18 ? "成年" : "未成年"
puts status
# 区间判断(include)
puts (1..10).include?(5) # => true
puts ("a".."z").cover?("m") # => true,cover? 更快,不做范围转换
步骤 3:命名约定与条件语句
# 命名:snake_case
snake_case_variable = 1
my_method_name = ->(x){ x + 1 }
# 方法后缀:
# ? 返回 boolean(命名约定)
# ! 有副作用或更危险(命名约定)
def palindrome?(s)
s == s.reverse
end
puts palindrome?("racecar") # => true
puts palindrome?("hello") # => false
# if / unless 后置(一行时推荐)
puts "成年" if age >= 18
puts "未成年" unless age >= 18
# case 语句(Ruby 里不是 switch,是 case-when)
grade = "B"
level = case grade
when "A" then "优秀"
when "B" then "良好"
when "C" then "及格"
else "不及格"
end
puts level # => 良好
步骤 4:parallel assignment 与解构
# 并行赋值
a, b = 1, 2
a, b = b, a # 优雅地交换值,不用临时变量
puts "a=#{a}, b=#{b}"
# 数组解构,多余元素归到尾部
first, second, *rest = [10, 20, 30, 40]
puts "first=#{first}, rest=#{rest.inspect}" # => first=10, rest=[30, 40]
# 忽略不需要的变量用下划线
a, _, c = [1, 2, 3]
puts a, c # => 1, 3
讲清楚要点
- 变量前缀就是作用域身份证:没有前缀是局部变量,
@ 是当前对象实例变量,@@ 是类变量,$ 是全局变量。看到前缀就知道它从哪来、活到哪。
||= 是 Ruby 最常用的默认值写法:cache ||= expensive_compute 意为「如果 cache 还没值(nil/false),就赋上」。用熟了会写出极其紧凑的初始化代码。
&. safe navigation 是 Ruby 2.3 引入:相当于 Java 的 Optional 链式调用或 Groovy 的 ?.。user&.name 在 user 为 nil 时安全返回 nil,而不是抛 NoMethodError。
- case-when 不是 switch:它隐式调用
===(case equality),所以 when 1..5 能匹配区间;when String 能匹配类型。这让它比 C 语言的 switch 灵活得多。
- 常量其实可变:
CONST = 1 后再次赋值只会发 warning,不会报错。Ruby 的常量更像个强提示,不是锁。
你可能会懵的地方
-
为什么 Ruby 不用花括号 {} 而是 end?
Ruby 也支持 { },但只用于代码块(block)和单行哈希。类/方法/if/case 全部用 end。读代码时数 def 和 end 配对。
-
局部变量和零参数方法调用长得一样?
name 可能是局部变量,也可能是调用 name() 方法。Ruby 在解析时优先视为变量;如果有同名的无参数方法,前面加 self. 显式调用:self.name。
-
||= 在 boolean 为 false 时会不会赋值?
会。flag = false; flag ||= true 后 flag 变成 true——因为 false 在 || 里算假。想要仅 nil 时赋值:flag = nil if flag.nil?。
-
and/or vs &&/|| 的区别?
优先级不同:and/or 优先级极低,适合控制流:open_file and process。日常比较运算用 &&/||,避免优先级陷阱。
-
=== 是什么黑魔法?
在 case-when 里,when expr 等价于 expr === target。所以 Integer 类的 === 实现是「判断目标是不是这个类的实例」,区间 (1..5) 的 === 是「目标是否落在区间内」。
错法对照
| 错法 | 问题 | 正解 |
|---|
A = 1; a = 2 以为一样 | Ruby 大小写敏感,A 和 a 是不同变量 | 常量全大写,局部变量全小写 |
if score = 90 | 赋值返回 90 永远真,打错一个 = | if score == 90 双等号比较 |
@name 在类外面直接用 | 外部访问不到实例变量 | 用 attr_reader :name 暴露 getter |
CONST = [1,2]; CONST << 3 | 常量引用不能改,但引用的内容可变 | 冻结数组:CONST = [1,2].freeze |
user.profile.name 链式调用 user 可能 nil | NoMethodError on nil | user&.profile&.name |
卡点急救
- undefined local variable or method:要么变量没定义,要么和方法重名 Ruby 当变量解析了。检查拼写、是否在前面赋值过。
- warning: already initialized constant:常量重复赋值,虽然能跑但不安全。把它提取成配置类或冻结对象。
- Unexpected
end / 少 end:Ruby 是 def ... end、class ... end、if ... end 层层嵌套。VS Code 括号彩虹 + 缩进整理一眼解决。
面试常问
-
=、==、===、equal?、eql? 的区别?
= 赋值;== 值比较,Integer 和 Float 可能相等;=== case equality,由 when 右侧调用;equal? 对象身份;eql? 严格值+类型比较(Hash 键)。
-
@ 和 @@ 何时用?为什么不推荐 @@?
@ 实例变量存对象状态;@@ 类变量跨实例共享,但子类继承时会共享同一个变量,容易污染。现代 Ruby 建议用类实例变量 @count + 类方法访问。
-
||= 的原理和坑?
a ||= b 等价 a || a = b,不是 a = a || b。如果 a 未定义,Ruby 默认视为 nil,所以不会报错。坑:boolean false 会被重新赋值。
自测三问
@name、name、@@name、$name 各自的作用域和访问方式是什么?
&. 和 Java 的 Optional 链式调用有什么异同?
case-when 里 when String 为什么能匹配字符串类型?
小结一句话
Ruby 语法的基础是「前缀即作用域、命名即意图、运算符都有返回值」,把这套规则用熟,代码就写对了。
延伸