當呼叫不存在的方法時,Ruby 會沿著繼承鏈尋找 method_missing。這是「最後防線」,預設在 BasicObject 中定義,拋出 NoMethodError。覆寫它可實現動態方法、委派、DSL 等進階模式。
# 慢:每次呼叫都走完整繼承鏈尋找 method_missing
class BadDelegator
def initialize(target)
@target = target
end
def method_missing(name, *args, &block)
@target.send(name, *args, &block)
end
end
問題:效能差、respond_to? 回傳 false、除錯困難、堆疊追蹤雜訊。
class GoodDelegator
def initialize(target)
@target = target
# 一次性定義所有委派方法
@target.class.instance_methods(false).each do |name|
define_singleton_method(name) do |*args, &block|
@target.send(name, *args, &block)
end
end
end
end
優點:首次定義後直接呼叫真實方法、respond_to? 正常、堆疊乾淨、IDE 可辨識。
方法名稱在執行期才決定,數量龐大且不確定會被呼叫:
find_by_* 動態查詢user.name_with_cent
class DynamicFinder
def method_missing(name, *args, &block)
# 1. 檢查:是否符合我們處理的模式
if name.to_s =~ /^find_by_(.+)$/
attributes = $1.split('_and_')
# 2. 包裹:用 define_method 快取,下次直接呼叫
self.class.define_method(name) do |*search_args|
# 實際查詢邏輯
where(attributes.zip(search_args).to_h).first
end
# 3. 執行:立即呼叫新定義的方法
send(name, *args, &block)
else
# 4. 善後:不屬於我們處理的,交給 super
super
end
end
# 關鍵:配合 respond_to_missing? 讓 respond_to? 正常運作
def respond_to_missing?(name, include_private = false)
name.to_s.start_with?('find_by_') || super
end
end
class MyClass
define_method(:hello) { "Hello!" }
define_method(:greet) { |name| "Hi, #{name}!" }
end
MyClass.new.hello # => "Hello!"
MyClass.new.greet("Ruby") # => "Hi, Ruby!"
class APIClient
ENDPOINTS = [:users, :posts, :comments, :albums]
ENDPOINTS.each do |endpoint|
define_method("get_#{endpoint}") do |params = {}|
request(:get, "/#{endpoint}", params)
end
define_method("create_#{endpoint.to_s.singularize}") do |data|
request(:post, "/#{endpoint}", data)
end
end
private
def request(method, path, payload)
# HTTP 請求實作
end
end
client = APIClient.new
client.get_users(page: 1)
client.create_post(title: "Hello")
class Base
def complex_calculation(a, b, c)
a * b + c
end
end
class Derived < Base
# 複製 Base 的方法並重新命名
define_method(:calc, Base.instance_method(:complex_calculation))
end
Derived.new.calc(2, 3, 4) # => 10
class Foo; end
# 1. instance_eval:定義單例方法(類別方法)
Foo.instance_eval do
def class_method
"I'm a class method"
end
end
Foo.class_method # => "I'm a class method"
# 2. class_eval:定義實例方法
Foo.class_eval do
def instance_method
"I'm an instance method"
end
end
Foo.new.instance_method # => "I'm an instance method"
class XmlBuilder
def initialize
@xml = []
end
def method_missing(name, *args, &block)
attrs = args.first.is_a?(Hash) ? args.shift : {}
content = args.first || (block_given? ? yield : '')
attr_str = attrs.map { |k,v| %Q(#{k}="#{v}") }.join(' ')
tag = "<#{name}#{attr_str.empty? ? '' : ' ' + attr_str}>"
if block_given?
@xml << tag
instance_eval(&block)
@xml << "#{name}>"
else
@xml << "#{tag}#{content}#{name}>"
end
self # 支援鏈式呼叫
end
def to_xml
@xml.join
end
end
builder = XmlBuilder.new
builder.html do
head { title "My Page" }
body { h1 "Welcome", class: "header" }
end
puts builder.to_xml
class SmartDelegator
def initialize(target)
@target = target
# 先定義已知方法
define_known_methods
end
def define_known_methods
@target.public_methods(false).each do |name|
define_singleton_method(name) { |*a, &b| @target.send(name, *a, &b) }
end
end
# 備援:處理動態新增的方法
def method_missing(name, *args, &block)
if @target.respond_to?(name)
# 動態定義並快取
define_singleton_method(name) { |*a, &b| @target.send(name, *a, &b) }
send(name, *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name) || super
end
end
class NullObject
def method_missing(name, *args, &block)
self # 任何呼叫都回傳自己,鏈式安全
end
def respond_to_missing?(name, include_private = false)
true # 宣稱回應所有方法
end
def nil?
true
end
end
# 使用
user = User.find(1) || NullObject.new
user.profile.avatar.url # 不會噴錯,回傳 NullObject
require 'benchmark/ips'
class StaticDelegator
def initialize(target)
@target = target
target.class.instance_methods(false).each do |name|
define_singleton_method(name) { |*a, &b| @target.send(name, *a, &b) }
end
end
end
class DynamicDelegator
def initialize(target)
@target = target
end
def method_missing(name, *a, &b)
@target.send(name, *a, &b)
end
def respond_to_missing?(name, *); @target.respond_to?(name); end
end
target = Object.new
def target.foo; "bar"; end
static = StaticDelegator.new(target)
dynamic = DynamicDelegator.new(target)
Benchmark.ips do |x|
x.report("static (define_method)") { static.foo }
x.report("dynamic (method_missing)") { dynamic.foo }
x.compare!
end
# 典型結果:static 快 10-50 倍
method_missing 內呼叫不存在的方法 → 再次觸發 method_missing
def method_missing(name, *args)
puts name # puts 會呼叫 to_s,若未定義又觸發 method_missing!
super
end
# 修正:用 Kernel.puts 或 __send__
NoMethodError 變成靜默失敗respond_to_missing?method_missing 會攔截私有方法呼叫,需檢查 include_privatemethod_missing,改用 define_method
module MethodMissingTracer
def method_missing(name, *args, &block)
caller_locations(1, 3).each { |loc| puts " from #{loc}" }
super
end
end
# 臨時混入除錯
MyClass.prepend(MethodMissingTracer)
module ActiveRecord
module DynamicMatchers
class FindBy < Matcher
def match(method_name)
/^find_by_(.+)/.match(method_name)
end
def attribute_names
match.captures.first.split('_and_')
end
end
end
class Base
def self.method_missing(name, *args, &block)
if matcher = DynamicMatchers.match(name)
# 定義快取方法
define_dynamic_finder(name, matcher)
send(name, *args, &block)
else
super
end
end
def self.define_dynamic_finder(name, matcher)
attributes = matcher.attribute_names
define_singleton_method(name) do |*values|
where(attributes.zip(values).to_h).first
end
end
end
end
respond_to?、method、public_method 會失效define_method 包裹,第二次直接命中method_missing 是 Ruby 元編程的「核武」,威力巨大但副作用強。現代 Ruby 開發原則:能用 define_method 就不用 method_missing。真正需要動態方法名稱時,遵循「檢查→快取→執行→善後」四步驟,並實作 respond_to_missing?。理解 define_method、instance_eval/class_eval、singleton_method 的區別,能精準選擇適合的元編程工具。ActiveRecord 的動態查詢是教科書級範例,值得深入研讀其原始碼。