Ruby元编程

1,481 阅读4分钟

1 什么是元编程

DSL(Domain Specific Language)特定领域语言,可以看作一种设计模式。如:Rspec单元测试,在使用的过程中提供给我们一些配置、语法、关键字等等,所提供的这些就可以看作是一种DSL。高度抽象的基于一个语言而生成的一种语言。

作用:

  1. 避免代码的重复
  2. 帮我们完成类似DSL的功能,设计出一套软件或者模块,最终暴露出来一些非常简单的接口。

2 元编程的使用场景

3 动态方法

3.1 动态调用

class MyClass
  def my_method(my_arg)
    my_arg + 1
  end
end

myclass = MyClass.new
myclass.my_method(2)

myclass.send(:my_method, 2) #第一个参数方法名,可以是symbol或字符串;后面的会全部传递给方法名

send中,想要调用的方法名变成参数,这样就可以在代码运行的最后一刻决定调用哪个方法。

3.2 define_method

在运行时定义方法的技术成为动态方法。

class User < ActiveRecord::Base
  # def is_pending?
  #   self.status == 'pending'
  # end
  STATUS = %w[pending activated suspended]

  STATUS.each do |status|
    define_method "is_#{status}?" do
      self.status == status
    end
  end
end

3.3 method_missing

接收者并没有对应的方法称为幽灵方法。

简单的例子:

module B
  def hello
    p 'hello'
  end
  
  def method_missing name
    p name
  end
end

class A
  include B
  def hi
    p 'hi'
  end
end
a = A.new
a.hi
a.hello
a.nandudu
class A
end
# 要实现:
A.title = 'nandudu'
A.title # => nandudu

常规思路:

class A 
  @@attributes = {}
  
  def self.title
    @@attributes[:title]
  end
  
  def self.title= value
    @@attributes[:title] = value
  end
end

使用method_missing:

class A
  @@attribute = {}
  class << self
    def method_missing method_name, *params
      method_name = method_name.to_s
      if method_name =~ /=$/
        @@attributes[method_name.sub('=', '')] = params.first
      else
        @@attributes[method_name]
      end
    end
  end
end

3.3 alias_method && alias

使用方法别名:

  • alias_method方法中,第一个参数是方法的新名字,第二个参数是方法的原始名字,可以用符号或字符串表示。
  • Ruby还提供了一个alias关键字,可以代替Module#alis_method,在顶级作用域修改方法时使用
class String
  alias_method :real_length, :length
  def length
    real_length > 5 ? 'long' : 'short'
  end
end
"war and peace".length #=>"long"
"war and peace".real_length #=> 13

上面的代码重定义了 String#length 方法,但是别名方法引用的还是原始方法。这说明重定义方法时并不是真正修改了这个方法

4 作用域相关

4.1 instance_eval

首先要明白instance_eval是所有类的实例方法,打开的是当前实例作用域。所以

class A
end

a = A.new
a.instance_eval do 
  def hello
    'hello'
  end
end

puts a.hello
#=>hello

其次因为class类本身也是Class类的一个实例,所以instance其实也可以用在类上,所以也可以定义类方法

class A
end

A.instance_eval do 
  def class_method
    'hello'
  end
end

puts A.class_method
#=>hello

4.2 class_eval

首先class_eval是只有类才能调用的,class_eval会重新打开当前类作用域.

class A
end


A.class_eval do 
  def instance_methods
    puts '343'
  end
  def self.hi
    puts 'hi'
    puts self
  end
end

a = A.new
puts a.instance_methods
puts A.hi
#=>343
#=>hi
#=>A

可以看出在一个类上调用了class_eval,就可以在其中定义该类的实例方法。

所以

  • instance_eval必须由实例调用,可以用来定义单例方法
  • class_eval必须由类调用,可以用来定义实例方法

5 eval 与 binding

evel

eval(string [, binding [, filename [,lineno]]]) → obj
# Evaluates the Ruby expression(s) in string. If binding is given, which must be a Binding object, the evaluation is performed in its context. If the optional filename and lineno parameters are present, they will be used when reporting syntax errors.

eval 把字符串 expr 当作 Ruby 程序来运行并返回其结果。乍看之下,eval 方法同在字符串中嵌入 #{} 的作用一样:

str = "hello"
eval "str + ' Fred'" #=> "hello Fred"
"#{str} Fred" #=> "hello Fred"

但是,有些时候,结果却并非想象的那样,考虑下面的例子:

exp = gets().chomp()
puts(eval(exp)) #=> 8
puts("#{exp}") #=> 2*4
puts("#{eval(exp)}") #=> 8

键入 2*4,发现两个方法给出的结果并不相同。这是因为,通过 gets() 方法接收到的是一个字符串,#{} 将它当成字符串处理,而不是一个表达式,但是 eval(exp) 将它作为一个表达式处理。

eval 会默认在当前上下文环境中执行。我们可以通过eval方法的第二个参数指明 eval 所运行代码的上下文环境,这个参数可以是 Binding 类对象或 Proc 类对象。

Binding

Binding 类的实例对象,可以封装代码在某一环境运行时的上下文,可以供以后使用。变量、方法、self 的值以及任何迭代器代码块都会被保留在该实例对象中。我们可以通过 Kernel#binding 方法来创建一个 Binding 类的实例对象。

def foo
  a = 1
  binding
end

eval("p a", foo) # => 1
class Demo
  def initialize(n)
    @secret = n
  end
  def get_binding
    binding
  end
end

k1 = Demo.new(99)
b1 = k1.get_binding
k2 = Demo.new(-3)
b2 = k2.get_binding

eval("@secret", b1)   #=> 99
eval("@secret", b2)   #=> -3
eval("@secret")       #=> nil

严格意义上,没有说我们怎么写就叫做元编程,只是针对Ruby的一些高级的或者动态的特性的一个概括,只要是利用了这些高级的特性,就可以成为元编程。