元编程
这一课你将能做到
- 用
send、define_method、method_missing 动态调用和定义方法
- 理解
class_eval、instance_eval 的执行上下文切换
- 写一个简单的 DSL(领域特定语言),并知道什么时候不该用元编程
为什么要学
元编程是 Ruby 的「终极武器」,也是 Rails 魔法的源泉。has_many :posts、attr_accessor :name、validates :email, presence: true——这些声明式语法背后都是元编程。懂了元编程,Rails 源码从「天书」变成「套路」。
人话:元编程就是「代码写代码」。普通程序是代码操作数据,元程序是代码操作代码本身——动态定义方法、拦截不存在的方法调用、在运行时修改类结构。
先建立图像
元编程三大武器
┌─────────────────┬──────────────────────────────┐
│ 武器 │ 作用 │
├─────────────────┼──────────────────────────────┤
│ send │ 动态调用方法(绕过 public 限制) │
│ define_method │ 运行时定义方法 │
│ method_missing │ 拦截「找不到方法」的调用 │
├─────────────────┼──────────────────────────────┤
│ 辅助工具 │ class_eval / instance_eval / │
│ │ define_singleton_method │
└─────────────────┴──────────────────────────────┘
跟着做
步骤 1:send——动态调用
class Greeter
def hello(name)
"你好,#{name}"
end
private
def secret
"这是私有方法"
end
end
g = Greeter.new
# 普通调用
puts g.hello("小明")
# send 动态调用:方法名可以是变量
method_name = :hello
puts g.send(method_name, "Ruby")
# send 能绕过 private(谨慎使用)
puts g.send(:secret) # 能调通!
# g.secret # 报错:private method
# respond_to? 检查对象是否响应某方法
puts g.respond_to?(:hello) # => true
puts g.respond_to?(:secret) # => false(private 不算)
puts g.respond_to?(:secret, true) # => true(第二个参数包含私有)
步骤 2:define_method——运行时定义方法
class Product
# 批量定义 getter
[:name, :price, :stock].each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
# 批量定义状态检查方法
[:in_stock?, :out_of_stock?].each do |predicate|
define_method(predicate) do
predicate == :in_stock? ? @stock > 0 : @stock <= 0
end
end
def initialize(name, price, stock)
@name, @price, @stock = name, price, stock
end
end
p = Product.new("键盘", 299, 10)
puts p.name # => 键盘
puts p.price # => 299
puts p.in_stock? # => true
p.stock = 0
puts p.out_of_stock? # => true
步骤 3:method_missing——拦截不存在的方法
class DynamicFinder
def initialize(data)
@data = data
end
def method_missing(name, *args)
# 拦截 find_by_xxx 模式
if name.to_s.start_with?("find_by_")
field = name.to_s.sub("find_by_", "").to_sym
@data.find { |row| row[field] == args.first }
else
super # 不认识的交给父类,保证正常报错
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?("find_by_") || super
end
end
users = [
{ id: 1, name: "Tom", role: :admin },
{ id: 2, name: "Lisa", role: :user }
]
finder = DynamicFinder.new(users)
puts finder.find_by_name("Tom").inspect # => {:id=>1, :name=>"Tom", :role=>:admin}
puts finder.find_by_role(:user).inspect # => {:id=>2, ...}
puts finder.respond_to?(:find_by_name) # => true
# finder.unknown_method # NoMethodError(super 保证)
步骤 4:class_eval 与 instance_eval
# class_eval:在类的上下文执行代码(self 是类)
String.class_eval do
def shout
upcase + "!"
end
end
puts "hello".shout # => HELLO!
# instance_eval:在对象的上下文执行代码(self 是对象)
obj = Object.new
obj.instance_eval do
@secret = 42
def reveal
@secret
end
end
puts obj.reveal # => 42
步骤 5:写一个迷你 DSL
class TaskDSL
def initialize
@tasks = []
end
def task(name, &block)
@tasks << { name: name, steps: [] }
current = @tasks.last
TaskContext.new(current).instance_eval(&block)
end
def run_all
@tasks.each do |t|
puts "执行任务: #{t[:name]}"
t[:steps].each { |s| puts " - #{s}" }
end
end
class TaskContext
def initialize(task)
@task = task
end
def step(description)
@task[:steps] << description
end
end
end
dsl = TaskDSL.new
dsl.task "部署" do
step "拉取代码"
step "运行测试"
step "重启服务"
end
dsl.task "备份" do
step "导出数据库"
end
dsl.run_all
# 输出:
# 执行任务: 部署
# - 拉取代码
# - 运行测试
# - 重启服务
# 执行任务: 备份
# - 导出数据库
讲清楚要点
send 是「绕过访问控制」的调用:obj.send(:method) 等价 obj.method,但能调 private 方法。测试私有方法时很有用,生产代码滥用会破坏封装。
define_method 比 def 更灵活:def 是关键字,方法名必须字面量;define_method 是方法,方法名可以是变量/字符串。Rails 的 has_many :posts 内部就是 define_method(:posts) { ... }。
method_missing 必须配 respond_to_missing?:只定义 method_missing 不定义 respond_to_missing?,respond_to? 会返回 false,破坏鸭子类型。这是面试高频考点。
class_eval vs instance_eval:class_eval 的 self 是类,定义的 def 是实例方法;instance_eval 的 self 是对象,定义的 def 是 singleton 方法。Rails 插件常用 class_eval 给宿主类打补丁。
- 元编程的代价:代码可读性下降、IDE 跳转失效、错误延迟到运行时才暴露。Ruby 社区共识:能用普通代码就不用元编程。
你可能会懵的地方
-
define_method 里的 self 是谁?
block 是闭包,define_method 里的 self 是定义时的 self(类),不是运行时接收者。要访问实例变量用 instance_variable_get。
-
为什么 method_missing 里要调 super?
super 会触发 BasicObject#method_missing,抛出标准的 NoMethodError。不调 super 的话,拼错的方法名会被静默吞掉,调试地狱。
-
class_eval 和 class << self 一样吗?
不一样。class_eval 打开类本身(self 是类);class << self 打开 singleton class(定义类方法)。class_eval 里 def foo 是实例方法;class << self; def foo; end; end 是类方法。
-
instance_exec 和 instance_eval 的区别?
instance_eval 只能传 block 或字符串;instance_exec 能传参数给 block:obj.instance_exec(1, 2) { |a, b| a + b }。更灵活。
-
Ruby 3 的 ruby2_keywords 是干嘛的?
Ruby 3 分离了位置参数和关键字参数,旧代码里用 *args 转发关键字参数会丢信息。ruby2_keywords 标记方法保留 Hash 到关键字参数的隐式转换。写库时才会遇到。
错法对照
| 错法 | 问题 | 正解 |
|---|
method_missing 不定义 respond_to_missing? | respond_to? 失效,破坏接口契约 | 成对定义 |
| 元编程定义的方法名冲突 | 覆盖已有方法 | 先 method_defined? 检查 |
class_eval 里想用 def self.foo | 定义的是类方法,不是实例方法 | 用 define_method 或 class << self |
| 过度使用 method_missing | 性能差、难调试 | 优先 define_method 或普通方法 |
send 调私有方法当常规操作 | 破坏封装 | 只在测试或框架代码中使用 |
卡点急救
NoMethodError 但方法明明定义了:检查是不是 define_method 的 block 里 self 不对,或方法名拼写错误。
respond_to? 返回 false 但能调用:忘了定义 respond_to_missing?。补上即可。
- 元编程代码在类加载后没生效:
define_method 在类定义外调用时,要确保类已加载。Rails 里用 ActiveSupport.on_load 钩子。
面试常问
-
method_missing 和 respond_to_missing? 为什么要成对出现?
method_missing 让对象「能响应」不存在的方法;respond_to_missing? 让 respond_to? 返回 true。两者配合才能保证 obj.respond_to?(:dynamic_method) 和 obj.dynamic_method 行为一致,维护鸭子类型的契约。
-
Rails 的 has_many :posts 是怎么实现的?
元编程。has_many 是类方法,内部调用 define_method(:posts) { ... } 动态定义实例方法,查询关联数据。还定义了 post_ids=、posts= 等一系列方法。
-
元编程什么时候该用,什么时候不该用?
该用:框架/库开发(DSL、声明式 API)、批量定义相似方法、拦截器模式。不该用:业务代码里炫技、替代简单的继承/组合、让 IDE 和新人无法理解的代码。
自测三问
send 和普通方法调用有什么区别?为什么测试私有方法常用它?
method_missing 和 respond_to_missing? 的关系?缺了后者会怎样?
class_eval 和 instance_eval 的 self 分别指向谁?
小结一句话
元编程是 Ruby 的「代码写代码」能力——send 动态调用、define_method 动态定义、method_missing 动态拦截;用好了是魔法,用砸了是灾难。
延伸