Sorbet 入门:给 Ruby 加上渐进式类型检查
Sorbet 是 Stripe 开源的 Ruby 渐进类型检查器,从零注解起步逐步收紧。本文讲解 sig 注解语法、严格级别体系、可选与联合类型、流敏感收窄与项目落地路径。
Sorbet 通过在方法前写 sig { params(x: Integer).returns(String) } 注解类型,在开发期(编辑器实时)和 CI 中静态检查类型错误。渐进式设计允许老项目从 typed: false 起步,按文件逐步提升严格级别,百万行级代码库也能平滑接入。
Sorbet 的渐进哲学
与"要么全部类型化要么没有"的方案不同,Sorbet 接受现实:大型 Ruby 项目不可能一夜加完类型。它的策略是按文件分级——从不检查到最严格共五档,团队按节奏推进,类型保障随时间单调增强。Stripe 用它在数百万行代码上运行多年,验证了这条路线的可行性。
安装与初始化
gem install sorbet sorbet-runtime
# 项目初始化(生成 sorbet/ 目录与 RBI 文件)
bundle exec srb init
初始化会扫描项目与依赖 gem,生成 RBI(Ruby Interface)签名文件——Rails 的动态方法靠 tapioca gem 生成 RBI 补齐。
sig 注解与严格级别
# typed: true
extend T::Sig
sig { params(name: String, age: Integer).returns(String) }
def greet(name, age)
"#{name},#{age + 1} 岁生日快乐"
end
文件头的 magic comment 决定检查强度:
# typed: false:不检查(存量代码起点)# typed: true:检查已注解的方法调用、基本类型错误# typed: strict:所有方法和实例变量必须有注解# typed: strong:全部代码可静态推导(新代码的理想目标)
srb tc 命令执行全项目检查,CI 中运行拦截类型回归。
类型系统速览
sig { params(user: T.nilable(User)).returns(T::Array[String]) } # 可空、数组
sig { params(id: T.any(Integer, String)).void } # 联合类型
sig { params(opts: T::Hash[Symbol, T.untyped]).void } # 哈希
T.let(0, Integer) # 局部变量标注
T.nilable(X):X 或 nil;T.any(A, B):联合类型;T.untyped:放弃检查(渐进期的逃生舱,逐步消除)。
流敏感类型收窄
Sorbet 理解控制流:
def display(user)
return '游客' if user.nil?
user.name # 此处 user 自动收窄为 User,不再报 nil 警告
end
判空之后、case 分支内、is_a? 检查之后,类型自动收窄——写法和普通 Ruby 一致,检查器跟得上你的思路。
与 RBS 的分工想象
Sorbet 注解写在源码里(sig 块),带运行时校验(开发环境参数错误立即抛异常);RBS 类型写在独立文件,纯静态。详细对比见本系列《Sorbet 和 RBS 怎么选》。大型 Rails 项目 Sorbet 生态(tapioca、sorbet-rails)目前更成熟。
常见问题(FAQ)
Q:Sorbet 会拖慢运行速度吗?
静态检查(srb tc)不影响运行;运行时校验(sig 的运行时部分)在测试环境开启、生产环境可配置关闭或采样——性能开销可控,且 Sorbet 编译器本身是 Rust 级速度,大项目检查以秒计。
Q:Rails 的动态方法(find_by_xxx)怎么通过检查?
用 tapioca 生成 RBI 文件描述这些动态方法:bin/tapioca dsl 生成模型相关 RBI,bin/tapioca gem 生成 gem 的 RBI,提交进仓库即可。
Q:团队推进 Sorbet 的正确节奏?
第一阶段全项目 typed: false 跑通 CI;第二阶段新文件默认 typed: true;第三阶段核心领域模型升 strict;配合 spoom bump 工具批量提升文件级别。切忌运动式强推——类型系统是工具不是 KPI。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。