Ruby元编程(三)

参考书籍《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方法:可以删除(包括继承而来的)所有方法。

     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方法:只能删除当前类中定义的方法。

    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)"