参考书籍《Ruby 元编程(第二版)》
Ruby 版本:书上使用的是 2.x,自己使用的 3.1.2

# 动态调用方法

使用 [Object#send] 可以进行动态调用方法。send 方法的第一个参数是要调用的方法名字(可以是字符串或符号),剩下的参数和代码块会直接传递给调用的方法,这样可以在代码运行的最后一刻决定调用那个方法,这个技巧被称为动态派发(Dynamic Dispatch)

示例代码
class MyClass
  def my_method(my_arg)
    my_arg * 2
  end
end
obj = MyClass.new
# 常规调用方式
p obj.my_method(3) #=> 6
# 使用 send 方法进行动态调用
p obj.send(:my_method, 3) #=> 6

# 动态定义方法

使用 Module#define_method 方法可以随时定义一个方法,只需要提供一个方法名和充当方法主体的块

示例代码
class MyClass2 
  # 动态定义了 my_method 方法
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end
obj2 = MyClass2.new
p obj2.my_method(2) #=> 6

# 幽灵方法

在 Ruby 中,编译器并不检查方法的调用行为,这意味着可以带调用一个并不存在的方法。
当在对象上调用方法时,如果该方法在祖先链上找不到,那么就会调用 method_missing 方法,它是 BasicObject 的一个私有实例方法,默认行为是抛出 NoMethodError 错误
我们可以通过重写 method_missing 方法,来拦截那些实际不存在的方法,达到实现幽灵方法(Ghost Method)的效果。

示例代码
class Lawyer
  def something; end
end
nick = Lawyer.new
nick.talk_simple # 运行时抛出异常: NoMethodError
# 重写 method_missing 方法
class Lawyer2 
  def method_missing(method, *args)
    puts "Your call: #{method} (#{args.join(', ')})"
    puts "(You also passed it a block)" if block_given?
  end
end
bob = Lawyer2.new
bob.talk_simple('a', 'b') do 
  # a block
end
#=> 输出结果如下:
#=> "Your call: talk_simple (a, b)"
#=> "(You also passed it a block)"

# respond_to? 方法

使用 respond_to? 方法可以询问一个对象是否有对应的方法。
当询问对象是否存在一个幽灵方法时,会再次调用 respond_to_missing? 方法,将其结果作为返回值返回,这个方法是一个钩子方法(Hook Method),默认实现是返回 false,当使用 [method_missing] 处理幽灵方法时,一般就需要对其进行重写

示例代码
class Demo
  def method_missing(name, *args)
    # 只处理名称为 hello 的幽灵方法
    super if name.to_s != 'hello'
    p "method name: #{name}, args: #{args}"
  end
  # 实际存在的一个方法
  def test
  end
end
a = Demo.new
p "respond_to: #{a.respond_to?('test')}"   #=> true
# 在调用 respond_to? 方法时,如果方法是一个幽灵方法,它会调用 respond_to_missing? 方法确定返回值
p "respond_to: #{a.respond_to?('hello')}"  #=> false
# 重写 respond_to_missing? 方法之后
class Demo
  def method_missing(name, *args)
    # 只处理名称为 hello 的幽灵方法
    super if name.to_s != 'hello'
    p "method name: #{name}, args: #{args}"
  end
  # 实际存在的一个方法
  def test
  end
  
  # 重写 respond_to_missing? 方法
  # 只对名为 hello 的幽灵方法返回 true
  def respond_to_missing?(method, include_private=false)
    method == :hello || super
  end
end
a = Demo.new
p "respond_to: #{a.respond_to?('test')}"   #=> true
# 当询问的幽灵方法名为 hello 时,返回 true
p "respond_to: #{a.respond_to?('hello')}"  #=> true
# 否则,返回 false
p "respond_to: #{a.respond_to?('hello2')}"  #=> false

# 白板类

白板类是对幽灵方法的完善,如果幽灵方法和一个真实方法同名,那么幽灵方法会被忽略。
如果不需要继承来的方法(真实方法),可以通过删除它来解决问题,这种拥有极少数方法的类称为白板类(Blank Slate)。

  • 实现方式一:

    1. 通过继承 BasicObject 类,因为其只有少数几个实例方法。
    2. 通过自定义白班类实现,这样可以确保实例方法的名称不会和自己要使用的幽灵方法重名。
  • 实现方式二:

    示例代码
    class Test
        def method_missing(method)
            case method
            when :display
                p 'Display method (Ghost Method)'
            end
        end
    end
    t = Test.new
    t.display #=> nil
    # 上面返回 nil 是因为 Test 类自动继承自 Object 类,而 Object 中已经有一个 display 方法了,它回返回 nil
    Object.methods.grep(/^display/) #=> [:display]

    1、使用 Module#undef_method 方法:可以删除(包括继承而来的)所有方法

    使用undef_method删除方法
    class BlankSlate
         def self.hide(name)
             # 删除了继承自 Object 的 display 方法
             undef_method name if name == :display
         end
         instance_methods.each { |m| hide(m) } 
     end
    class Test < BlankSlate
         def method_missing(method)
             case method
             when :display
                 p 'Display method (Ghost Method)'
             end
         end
     end
     t = Test.new
     t.display #=> "Display method (Ghost Method)"

    2、使用 Module#remove_method 方法:只能删除当前类中定义的方法

    使用remove_method删除方法
    class Test
        def self.hide(name)
            # 删除了当前类 Test 中的实例方法 current
            remove_method(name) if name == :current
        end
        def method_missing(method)
            case method
            when :current 
                p 'Current method (Ghost Method)'
            end
        end
        def current 
            p "current method..."
        end
        instance_methods.each { |m| hide(m) }
    end
    t = Test.new
    t.current #=> "Current method (Ghost Method)"
更新于 阅读次数