CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Ruby 语言入门/课时/元编程
RawBlame
课时目录12 节

Ruby 语言入门

  1. 01Ruby 环境
  2. 02数据类型
  3. 03语法基础
  4. 04控制流
  5. 05方法与 Blocks
  6. 06OOP 与 Mixin
  7. 07元编程
  8. 08IO 与 Enumerable
  9. 09并发
  10. 10Gems 与框架
  11. 11测试与质量
  12. 12综合练习

元编程

method_missing/DSL。

30 分钟

元编程

这一课你将能做到

  1. 用 send、define_method、method_missing 动态调用和定义方法
  2. 理解 class_eval、instance_eval 的执行上下文切换
  3. 写一个简单的 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
# 输出:
# 执行任务: 部署
#   - 拉取代码
#   - 运行测试
#   - 重启服务
# 执行任务: 备份
#   - 导出数据库

讲清楚要点

  1. send 是「绕过访问控制」的调用:obj.send(:method) 等价 obj.method,但能调 private 方法。测试私有方法时很有用,生产代码滥用会破坏封装。
  2. define_method 比 def 更灵活:def 是关键字,方法名必须字面量;define_method 是方法,方法名可以是变量/字符串。Rails 的 has_many :posts 内部就是 define_method(:posts) { ... }。
  3. method_missing 必须配 respond_to_missing?:只定义 method_missing 不定义 respond_to_missing?,respond_to? 会返回 false,破坏鸭子类型。这是面试高频考点。
  4. class_eval vs instance_eval:class_eval 的 self 是类,定义的 def 是实例方法;instance_eval 的 self 是对象,定义的 def 是 singleton 方法。Rails 插件常用 class_eval 给宿主类打补丁。
  5. 元编程的代价:代码可读性下降、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 调私有方法当常规操作破坏封装只在测试或框架代码中使用

卡点急救

  1. NoMethodError 但方法明明定义了:检查是不是 define_method 的 block 里 self 不对,或方法名拼写错误。
  2. respond_to? 返回 false 但能调用:忘了定义 respond_to_missing?。补上即可。
  3. 元编程代码在类加载后没生效: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 和新人无法理解的代码。

自测三问

  1. send 和普通方法调用有什么区别?为什么测试私有方法常用它?
  2. method_missing 和 respond_to_missing? 的关系?缺了后者会怎样?
  3. class_eval 和 instance_eval 的 self 分别指向谁?

小结一句话

元编程是 Ruby 的「代码写代码」能力——send 动态调用、define_method 动态定义、method_missing 动态拦截;用好了是魔法,用砸了是灾难。

延伸

  • Ruby 元编程官方文档
  • 下一课:IO 与 Enumerable——文件读写、正则、日期与强大的集合操作。
加载本节练习…
← 上一课OOP 与 Mixin下一课 →IO 与 Enumerable