天天看點

Rails每周一題(十六): Evaluation in Ruby

Ruby的evaluation是一個很重要的功能,它可以eval一個字元串或者一個block。在一些适宜的情況下使用它會得到一些“意外”的效果。

常用的eval

Ruby常用的evaluation有:class_eval (module_eval),instance_eval ,eval 。

這三種evaluation方法可以在不同的情況下使用:

1. class_eval

class_eval和module_eval是相同的,class_eval是module_eval的一個alias。

class_eval可以在一個mod的上下文中eval一個字元串或者一個block,常用于給一個類添加方法(執行個體方法):

class Thing
end
   
a = %q{def hello() "Hello there!" end}
Thing.module_eval(a)
puts Thing.new.hello()  #=> "Hello there!"
           

2. instance_eval

instance_eval可以在一個執行個體的上下文中eval一個字元串或者一個block:

class Klass
  def initialize
    @secret = 99
  end
end

k = Klass.new
k.instance_eval { @secret }   #=> 99
           

3. eval

eval是在目前上下文中eval一個字元串,如果指定一個binding,則在binding的上下文中eval。

def getBinding(str)
  return binding
end
str = "hello"
eval "str + ' Fred'"                      #=> "hello Fred"
eval "str + ' Fred'", getBinding("bye")   #=> "bye Fred"
           

class_eval和instance_eval的不同

首先注意到class_eval和instance_eval的不同主要在于執行上下文(context)的不同。class_eval在一個mod的上下文中執行,而instance_eval在一個執行個體的上下文中執行。

其次,他們的常用場景不同。class_eval的應用場景一般是“打開一個類”來做一些事情,比如增加方法,或者是include一個module。

String.class_eval do
  include ExtraMethods

  def another_method
    ....
  end
end
           

而instance_eval主要關注于一個執行個體。

class Paginator
  def initialize total_entires
    @total_entries = total_entires
    @page_index = 0
  end

  def next
    @page_index += 1
  end
end
paginator = Paginator.new(100)
paginator.next
paginator.instance_eval "@page_index" #=> 1
paginator.instance_eval { @page_index } #=> 1
           

當然,我們也同樣可以通過class_eval給Paginator類增加一個方法來擷取page_index。

在我們了解instance_eval和class_eval之前,很可能會誤用它們,比如通過下面的方法給Paginator增加一個執行個體方法:

Paginator.instance_eval do
  def baz
    "baz"
  end
end
           

結果:

Paginator.new.baz   #=> undefined method ‘baz’ for #<Foo:0x7dce8>
           

但我們偶然發現:

Paginator.baz   #=> "baz"
           

其實,如果我們了解ruby的對象模型,對這個結果并不會意外。我剛才已經說過:instance_eval關注于一個執行個體,它是在一個執行個體的上下文中執行的。Paginator類本身就是一個Class類的執行個體,是以Paginator.instance_eval做的就是給這個Class執行個體--Paginator--增加一個方法,也就是Paginator的類方法。

同樣,我們可以通過instance_eval給任意類的執行個體增加方法,比如:

"good".instance_eval do 
  def opposite
     "bad"
  end
end

"good".opposite #=> "bad"
           

這個opposite方法就是"good"執行個體的一個singleton method。

instance_eval和DSL

DSL的一個主要特點是可以在不同的上下文中執行,下面就是一個例子(竊取自Jay Field舉的例子):

class SqlGenerator
   class << self
     def evaluate(&script)
       self.new.instance_eval(&script)
     end
   end

   def multiply(arg)
     "select #{arg}"
   end

   def two(arg=nil)
     "2#{arg}"
   end

   def times(arg)
     " * #{arg}"
   end
 end

 SqlGenerator.evaluate { multiply two times two }
 #=> "select 2 * 2"

 class Calculator
   class << self
     def evaluate(&script)
       self.new.instance_eval(&script)
     end
   end

   def multiply(arg)
     eval arg
   end

   def two(arg=nil)
     "2#{arg}"
   end

   def times(arg)
     " * #{arg}"
   end
 end

 Calculator.evaluate { multiply two times two }
 #=> 4
  
           

簡單得介紹了一下ruby的evaluation,要了解更多,可以看下面這兩篇文章:

http://www.infoq.com/articles/eval-options-in-ruby

http://blog.jayfields.com/2007/03/ruby-instanceeval-and-classeval-method.html