技术开发 频道

在Ruby中对字符串和block求解

【IT168 技术文档】{$PageTitle=介绍 }
介绍
对包含代码的字符串和block求解,是我最钟爱的Ruby特性之一。Ruby提供了多种不同类型的求解方式;不过我最常用的是下面这些:eval、instance_eval和class_eval。

Module.class_eval
使用Module类的class_eval(及其别名module_eval)方法,可以在一个类的定义或者module定义的上下文中对给定字符串或block进行求解。我们常常用class_eval来向类的定义中加入方法,或是包含其他的module。

klass = Class.new klass.class_eval do include ERB::Util def encoded_hello htnl_escape "Hello World" end end klass.new.encoded_hello #=> Hello World

 不使用class_eval也可以达到上面的效果,但是要牺牲代码的可读性。

klass = Class.new klass.send :include, ERB::Util klass.send :define_method, :encoded_hello do html_escape "Hello World" end klass.send :public, :encoded_hello klass.new.encoded_hello #=> Hello World

Object.instance_eval
使用Object的instance_eval方法,可以在一个类实例的上下文中对给定字符串或block进行求解。这是个功能强大的概念:你可以先在任何上下文中创建一块代码,然后在一个单独的对象实例的上下文中对这块代码进行求解。为了设定代码执行的上下文,self变量要设置为执行代码时所在的对象实例,以使得代码可以访问对象实例的变量。

class Navigator def initialize @page_index = 0 end def next @page_index += 1 end end navigator = Navigator.new navigator.next navigator.next navigator.instance_eval "@page_index" #=> 2 navigator.instance_eval { @page_index } #=> 2

与使用class_eval的示例类似,实例变量的值可以通过其他的方式获取,不过使用instance_eval是一种非常直观的做法。

Kernel.eval
使用Kernel的eval方法可以在当前上下文中对一个字符串求解。可以选择为eval方法制定一个binding对象。如果给定了一个binding对象,求解的过程会在binding对象的上下文中执行。

hello = "hello world" puts eval("hello") #=> "hello world" proc = lambda { hello = "goodbye world"; binding } eval("hello", proc.call) #=> "goodbye world"

扩展eval的上下文
第一次使用eval,我用它来创建了attr_init这个类方法。当时我发现我总是在重复下面代码中的模式:

def some_attribute @some_attribute || = SomeClass.new end

因此我决定创建一个类方法来封装上面的行为:

class << Object def attr_init(name, klass) define_method(name) { eval "@#{name} ||= #{klass}.new" } end end

记得当时我觉得这样调用eval是非常丑陋的做法,但那会儿我想不出更好的方式来实现这样的效果;因此我把代码贴到了博客中,等待别人的指摘;他们很快就做出了回应,并给出下面的做法。一开始我并没有觉察这样做的好处,但是后来我意识到这个解法是非常出色的:它只需要调用一次eval方法,而不是在每次进行方法定义时都去重新调用eval。

class << Object def attr_init(name, klass) eval "define_method(name) { @#{name} ||= #{klass}.new }" end end

这样优化的有趣之处在于:它需要求解更多的内容, 以达到提升运行效率的目的。从那时开始,我只在必要的时候才使用eval,而且我非常注意如何以更有效率的方式来使用eval。


在不同上下文中使用instance_eval
在不同上下文中,对block或是以字符串形式出现的代码进行求解是很有价值的一种做法,也是设计领域特定语言(Domain Specific Language,DSL)时很常用的一种技术。实际上,在多种上下文环境中进行求解的能力是使用DSL的一个关键因素。请看下面的代码:
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方法并给定一个block参数,便可以生成一条SQL语句:
SqlGenerator.evaluate { multiply two times two } => "select 2 * 2"
然而,你还可以在一个calculator类的上下文中执行同样的代码来获得结果:
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
上述代码展示了如何使用instance_eval来指出block执行的作用范围。我在前面提到过,instance_eval方法在接受者的上下文中对字符串或block展开求解。例子中的接收者是SqlGenerator的一个实例和Calculator的一个实例。同时要保证使用self.new.instance_eval这样的方式来调用。如果不调用self的new方法,会将block作为类的一部分进行求解,而不是在类的实例中完成。

上述代码同样展示了开始定义DSL所需的一些步骤。创建DSL是很有挑战性的工作,但同时会带来很多好处。通过DSL来表达业务规则,所带来的好处是可以在多种上下文中执行这些业务规则。如上述示例所展示的,通过在不同上下文中执行DSL,可以从同一个业务规则产生多种不同的行为。当业务规则随着时间推移而改变时,系统中所有引用该业务规则的构成部分都会随之发生变化。而对Ruby求解方法的利用,就是成功实现这种效果的关键。

关于赌场中扑克牌桌的示例

Ruby提供的不同的求解方法,让我们可以很方便的在不同上下文中执行代码。举例来说,假设你为一个赌场工作,分派给你的任务是设计一个系统。当需要开一张新的扑克牌桌,或是需要知道等多久才能开新牌桌时,这个系统负责通知扑克牌室的工作人员。新开牌桌的业务规则,根据牌桌上的赌注大小和等待列表中的人数多少而不同。例如,对于一个赌注不封顶的牌局来说,牌桌边等待的人数多一些也无妨,因为人们更有可能在一手牌中输光他们所有的钱;如果贸然开新的牌桌,由于没有足够的玩家,该牌桌可能很快就要关闭。规则在DSL中可能以下面的方式表示:
if the '$5-$10 Limit' list is more than 12 then notify the floor to open if the $1-$2 No Limit' list is more than 15 then notify the floor to open if the '$5-$10 Limit' list is more than 8 then notify the brush to announce if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce
第一个执行DSL的上下文被用来通知赌场雇员。代码如下:
class ContextOne < DslContext bubble :than, :is, :list, :the, :to def more(value) '> ' + value.to_s end def method_missing(sym, *args) @stakes = sym eval "List.size_for(sym) #{args.first}" end def floor(value) __position(value, :floor) end def brush(value) __position(value, :brush) end def open __action(:open) end def announce __action(:announce) end def __action(to) { :action => to } end def __position(value, title) value[:position] = title value end def notify(value) [@stakes, value] end end

ContextOne通过下面的代码执行。

script = <<-eos if the '$5-$10 Limit' list is more than 12 then notify the floor to open if the '$1-$2 No Limit' list is more than 15 then notify the floor to open if the '$5-$10 Limit' list is more than 8 then notify the brush to announce if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce eos
class Broadcast def self.notify(stakes, options) puts DslContext.sym_to_stakes(stakes) options.each_pair do |name, value| puts " #{name} #{value}" end end end ContextOne.execute(script) do |notification| Broadcast.notify(*notification) end

ContextOne继承自DslContext。DslContext的定义如下。

class DslContext def self.execute(text) rules = polish_text(text) rules.each do |rule| result = self.new.instance_eval(rule) yield result if block_given? end end def self.bubble(*methods) methods.each do |method| define_method(method) { |args| args } end end def self.polish_text(text) rules = text.split("\n") rules.collect do |rule| rule.gsub!(/'.+'/,extract_stakes(rule)) rule << " end" end end def self.extract_stakes(rule) stakes = rule.scan(/'.+'/).first stakes.delete!("'").gsub!(%q{$},'dollar').gsub!('-','dash').gsub!(' ','space') end def self.sym_to_stakes(sym) sym.to_s.gsub!('dollar',%q{$}).gsub!('dash','-').gsub!('space',' ') end end<

ContextOne的method_missing方法中使用了List类,List类代码如下。
class List def self.size_for(stakes) 20 end end
ContextOne使用DSL检查每张牌桌的List大小,并在必要的时候发送通知。当然,这只是演示代码,List对象也只不过是stub,以验证ContextOne和DslContext所有的功能都没有问题。这里要重点注意:方法的执行被委托给了instance_eval,这样才能在ContextOne的上下文中对代码进行求解。

同样的脚本,可以在第二个上下文中执行;这个上下文返回当前正在散播的不同类型的赌博游戏。
class ContextTwo < DslContext bubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush def announce @stakes end alias open announce def method_missing(sym, *args) @stakes = sym end end
正像我们看到的,添加新的上下文是非常方便的。由于DslContext的execute方法调用instance_eval方法,上面的代码可以如下的方式执行。
ContextTwo.execute(script) do |stakes| puts ContextTwo.sym_to_stakes(stakes) end
为了使我们的示例更加完整,我们创建另外一个例子,显示所有接收通知的位置。
class ContextThree < DslContext bubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open,:open def announce; end def open; end def brush(value) :brush end def floor(value) :floor end def method_missing(sym, *args) true end end
同样的,这个上下文也继承自使用了instance_eval的DslContext,因此,只要运行下面的代码来执行即可。
ContextThree.execute(script) do |positions| puts positions end
在多个上下文中对DSL进行求解的能力,模糊了代码和数据之间的界线。可以对脚本‘代码’进行求解来生成报表(比如关于系统中已联系雇员的报表)。在展示需要多久才会新开扑克牌桌这样的上下文中,也可以对脚本进行求解(比如,业务规则说明需要15个人才能新开一张牌桌,系统知道在等待列表中有10个人,因此显示“5 more people needed before the game can start”)。使用instance_eval,我们可以在系统需要的任何上下文中,对同样的代码进行求解。
0
相关文章