深入 Ruby 01:表达式与控制流
if 0 在 Ruby 中进入真分支,空字符串和空数组也是如此。用其他语言的“空值”经验判断 Ruby 条件,会把合法输入误当作缺失,也会把需要拒绝的数据放行。本篇从真实条件值开始,再观察控制结构产生的结果。
文章卡
| 项目 | 内容 |
|---|---|
| 先修 | 第 00 篇;赋值、方法与数组的基础概念 |
| 核心问题 | 条件如何判真,控制结构返回什么,短路跳过了什么? |
| 实验 | bundle exec ruby labs/01/run.rb |
| 验收 | 六种值、短路轨迹、赋值优先级、case 接收者和循环结果 |
| 边界 | CRuby 3.4.11;不把 Rails 的 blank? 当成语言规则 |
真值不等于有效输入
本篇 lab 将六个值放进相同的条件表达式:
1 | |
结果是 [false, false, true, true, true, true]。这段原创实验同时锁定条件的规则与验证方式:只把 nil 和 false 当作假。0 是否合格,取决于业务范围;'' 是否可接受,取决于字符串契约。官方语法文档给出语言条件的定义,业务校验需要另外表达。控制表达式
Taskbook 的优先级范围是 1 到 5。if priority 只排除两个假值,不能排除 0、6、'high' 或空数组。库因此检查 priority.is_a?(Integer) 与 (1..5).cover?(priority),把类型和取值范围写在同一个边界上。
同理,name || 'default' 不会替换空字符串;它只在左侧是假值时求右侧。enabled ||= true 会把用户提供的 false 改成 true,也就无法表达“明确关闭”。需要仅对缺失值设默认时,可以显式检查 nil?;需要区分 Hash 中不存在的键与已有 nil,检查 key?。这些是不同的契约,不能合并成一种“空”。
短路运算返回操作数
&& 与 || 不负责把任意结果归一化为布尔值。观察:
1 | |
两个右侧表达式都没有运行,result 是符号 :fallback。如果右侧执行的是打开文件、数据库查询或修改集合,短路会影响副作用是否发生。用轨迹数组记录调用,比只比较最终真假更容易发现错误求值顺序。
程序可以利用短路表达可选检查,例如 tag.nil? || task[:tags].include?(tag)。但不要把多个重要步骤压进一行 a && b && c 再靠真假猜测哪个失败;某个步骤返回 nil 可能阻止清理或后续动作。对外返回契约需要布尔值时,直接写清判定;返回对象时,文档应说明对象与缺失值。
and 与 && 的差别先看绑定
下面两行中的赋值结果不同:
1 | |
第一行相当于 (value = true) and false;第二行相当于 other = (true && false)。整条表达式是否为假,与已经赋给变量的值是什么,不能混为一件事。实验输出 assignment=[true, false] 正是对绑定位置的观察。运算符优先级
and、or 有时用于流程语句,但写筛选谓词和赋值结果时,括号与 &&、|| 更容易保持局部含义。方法调用还可以省略括号,block 的 {} 与 do...end 又有不同结合强度。初学阶段保留调用括号,先理解解析结果,再讨论减少标点的写法。少写几个字符不值得换取一处隐含的控制流错误。
分支也是表达式
任务状态的说明可以直接接收分支结果:
1 | |
结果来自实际执行分支的末尾表达式。如果末尾为了调试追加了 puts description,方法可能从原本返回字符串变为返回 nil。方法通常返回最后求值的表达式,所以这种变化会跨出分支,影响调用者。测试应检查公开方法的返回值,而不是只检查终端是否有文字。
没有命中的 if 且无 else 会得到 nil。这可以表示缺失,也可能是漏写分支。选择哪种语义取决于调用者;如果下游必须得到字符串,就应让每个分支都产生字符串。unless 适合单一否定条件,条件里再叠加否定和 else 会增加推理负担。
case 使用匹配对象的 ===
case item; when pattern 的判断不是固定写成 item == pattern。本篇用只有 === 的对象记录实际参数:
1 | |
轨迹只有一个 4,接收者是 matcher。这解释了为何 Range 可以匹配范围、Regexp 可以匹配文本、Class 可以判断对象类别:when 的对象决定匹配协议。这个机制在第 15 篇的模式匹配之外也能独立使用,两种语法的绑定与失败规则仍需要分别研究。
多个 when 按顺序检查,首次匹配就执行对应分支。将宽泛条件排在严格条件前面,会令后者无法命中。例如先匹配 Numeric 再匹配 Integer,对整数会选择前者。业务规则的顺序属于程序行为,重排不能当成无影响的格式调整。
循环结果与每轮结果分开看
lab 中 while 执行两轮,正常结束后得到 nil;loop { break :finished } 得到 :finished。循环体最后一行的值不是自动聚合结果。需要集合时使用第 08 篇的 map;需要累积量时明确维护状态或使用 reduce。
break、next 与 return 的目标也不同。此处只验证循环结果,第 07 篇会追踪 block 中的控制流。把代码从循环体提取到一个 Proc,可能改变退出目标,因此这种重构需要行为检查。
实验与练习
运行 labs/01/run.rb 应打印真值数组、赋值结果、case=matched 和 loops=[nil, :finished],最后出现 PASS 01。每个观察都有断言;缺少最后的 PASS 或非零退出都需要读取具体异常,不能把前半段输出当成通过。
练习是给配置 Hash 写一个 enabled 读取函数:键不存在时默认 true;已有 false 时保留 false;已有 nil 时按契约拒绝。用 key? 和分支表达规则,再加三项断言。另一个练习是给上面的 case 增加第二个 matcher,用轨迹证明首次命中后第二个匹配没有执行。
参考资料
系列导航
导读 · 上一篇:00:导读与可重复运行 · 下一篇:02:数字与转换 · 完整源码包
