写点什么

在 Ruby 中对字符串和 block 求解

  • 2007-08-24
  • 本文字数:7800 字

    阅读完需:约 26 分钟

介绍

对包含代码的字符串和 block 求解,是我最钟爱的 Ruby 特性之一。Ruby 提供了多种不同类型的求解方式;不过我最常用的是下面这些:eval、instance_eval 和 class_eval。

Module.class_eval

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

klass = Class.new<br></br> klass.class_eval do<br></br> include ERB::Util<p> def encoded_hello</p><br></br> htnl_escape "Hello World"<br></br> end<br></br> end<p> klass.new.encoded_hello #=> <strong>Hello World</strong></p>不使用 class_eval 也可以达到上面的效果,但是要牺牲代码的可读性。

klass = Class.new<br></br> klass.send :include, ERB::Util<br></br> klass.send :define_method, :encoded_hello do<br></br> html_escape "Hello World"<br></br> end<br></br> klass.send :public, :encoded_hello<p> klass.new.encoded_hello #=> <strong>Hello World</strong></p>### Object.instance_eval

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

class Navigator<br></br> def initialize<br></br> @page_index = 0<br></br> end<br></br> def next<br></br> @page_index += 1<br></br> end<br></br> end<br></br> navigator = Navigator.new<br></br> navigator.next<br></br> navigator.next<br></br> navigator.instance_eval "@page_index" #=> 2<br></br> navigator.instance_eval { @page_index } #=> 2与使用 class_eval 的示例类似,实例变量的值可以通过其他的方式获取,不过使用 instance_eval 是一种非常直观的做法。

Kernel.eval

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

hello = "hello world"<br></br> puts eval("hello") #=> "hello world"<p> proc = lambda { hello = "goodbye world"; binding }</p><br></br> eval("hello", proc.call) #=> "goodbye world"### 扩展 eval 的上下文

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

def some_attribute<br></br> @some_attribute || = SomeClass.new<br></br> end因此我决定创建一个类方法来封装上面的行为:

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

class << Object<br></br> def attr_init(name, klass)<br></br> eval "define_method(name) { @#{name} ||= #{klass}.new }"<br></br> end<br></br> end这样优化的有趣之处在于:它需要求解更多的内容, 以达到提升运行效率的目的。从那时开始,我只在必要的时候才使用 eval,而且我非常注意如何以更有效率的方式来使用 eval。

在不同上下文中使用 instance_eval

在不同上下文中,对 block 或是以字符串形式出现的代码进行求解是很有价值的一种做法,也是设计领域特定语言(Domain Specific Language,DSL)时很常用的一种技术。实际上,在多种上下文环境中进行求解的能力是使用 DSL 的一个关键因素。请看下面的代码:

class SqlGenerator<br></br> class << self<br></br> def evaluate(&script)<br></br> self.new.instance_eval(&script)<br></br> end<br></br> end<p> def multiply(arg)</p><br></br> "select #{arg}"<br></br> end<p> def two(arg=nil)</p><br></br> "2#{arg}"<br></br> end<p> def times(arg)</p><br></br> " * #{arg}"<br></br> end<br></br> end使用上面的代码,调用 SqlGenerator.evaluate 方法并给定一个 block 参数,便可以生成一条 SQL 语句:

SqlGenerator.evaluate { multiply two times two }<br></br> => "select 2 * 2"然而,你还可以在一个 calculator 类的上下文中执行同样的代码来获得结果:

class Calculator<br></br> class << self<br></br> def evaluate(&script)<br></br> self.new.instance_eval(&script)<br></br> end<br></br> end<p> def multiply(arg)</p><br></br> eval arg<br></br> end<p> def two(arg=nil)</p><br></br> "2#{arg}"<br></br> end<p> def times(arg)</p><br></br> " * #{arg}"<br></br> end<br></br> end执行结果:

Calculator.evaluate { multiply two times two }<br></br> => 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<br></br> if the $1-$2 No Limit' list is more than 15 then notify the floor to open<br></br> if the '$5-$10 Limit' list is more than 8 then notify the brush to announce<br></br> if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce第一个执行 DSL 的上下文被用来通知赌场雇员。代码如下:

class ContextOne < DslContext<p> bubble :than, :is, :list, :the, :to</p><p> def more(value)</p><br></br> '> ' + value.to_s<br></br> end<p> def method_missing(sym, *args)</p><br></br> @stakes = sym<br></br> eval "List.size_for(sym) #{args.first}"<br></br> end<p> def floor(value)</p><br></br> __position(value, :floor)<br></br> end<p> def brush(value)</p><br></br> __position(value, :brush)<br></br> end<p> def open</p><br></br> __action(:open)<br></br> end<p> def announce</p><br></br> __action(:announce)<br></br> end<br></br> def __action(to)<br></br> { :action => to }<br></br> end<p> def __position(value, title)</p><br></br> value[:position] = title<br></br> value<br></br> end<p> def notify(value)</p><br></br> [@stakes, value]<br></br> end<p> end</p>ContextOne 通过下面的代码执行。

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

class DslContext<br></br> def self.execute(text)<br></br> rules = polish_text(text)<br></br> rules.each do |rule|<br></br> result = self.new.instance_eval(rule)<br></br> yield result if block_given?<br></br> end<br></br> end<p> def self.bubble(*methods)</p><br></br> methods.each do |method|<br></br> define_method(method) { |args| args }<br></br> end<br></br> end<p> def self.polish_text(text)</p><br></br> rules = text.split("\n")<br></br> rules.collect do |rule|<br></br> rule.gsub!(/'.+'/,extract_stakes(rule))<br></br> rule << " end"<br></br> end<br></br> end<p> def self.extract_stakes(rule)</p><br></br> stakes = rule.scan(/'.+'/).first<br></br> stakes.delete!("'").gsub!(%q{$},'dollar').gsub!('-','dash').gsub!(' ','space')<br></br> end<p> def self.sym_to_stakes(sym)</p><br></br> sym.to_s.gsub!('dollar',%q{$}).gsub!('dash','-').gsub!('space',' ')<br></br> end<br></br> end<ContextOne 的 method_missing 方法中使用了 List 类,List 类代码如下。

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

同样的脚本,可以在第二个上下文中执行;这个上下文返回当前正在散播的不同类型的赌博游戏。

class ContextTwo < DslContext<p> bubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush</p><p> def announce</p><br></br> @stakes<br></br> end<p> alias open announce</p><p> def method_missing(sym, *args)</p><br></br> @stakes = sym<br></br> end<p> end</p>正像我们看到的,添加新的上下文是非常方便的。由于 DslContext 的 execute 方法调用 instance_eval 方法,上面的代码可以如下的方式执行。

ContextTwo.execute(script) do |stakes|<br></br> puts ContextTwo.sym_to_stakes(stakes)<br></br> end为了使我们的示例更加完整,我们创建另外一个例子,显示所有接收通知的位置。

class ContextThree < DslContext<p> bubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open, :open</p><p> def announce; end</p><br></br> def open; end<p> def brush(value)</p><br></br> :brush<br></br> end<p> def floor(value)</p><br></br> :floor<br></br> end<p> def method_missing(sym, *args)</p><br></br> true<br></br> end<p> end</p>同样的,这个上下文也继承自使用了 instance_eval 的 DslContext,因此,只要运行下面的代码来执行即可。

ContextThree.execute(script) do |positions|<br></br> puts positions<br></br> end在多个上下文中对 DSL 进行求解的能力,模糊了代码和数据之间的界线。可以对脚本‘代码’进行求解来生成报表(比如关于系统中已联系雇员的报表)。在展示需要多久才会新开扑克牌桌这样的上下文中,也可以对脚本进行求解(比如,业务规则说明需要 15 个人才能新开一张牌桌,系统知道在等待列表中有 10 个人,因此显示“5 more people needed before the game can start”)。使用 instance_eval,我们可以在系统需要的任何上下文中,对同样的代码进行求解。

同样具有魔法的 eval

上述代码展示的是:如何在不同的作用范围中,使用 instance_eval 对 block 进行求解。不过,eval 方法同样可以在不同的上下文中进行求解操作。下面我来展示如何在 block 的作用范围中对 ruby 代码构成的字符串进行求解。

先让我们从一个简单的例子开始,不过让我们先回顾一下如何根据 block 的 binding 使用 eval。我们需要一个能够帮我们创建 block 的类。

class ProcFactory<br></br> def create<br></br> Proc.new {}<br></br> end<br></br> end在示例中,ProcFactory 类有一个方法:create;它的功能只是简单地创建并返回了一个 proc 对象。尽管这看起来似乎没什么特别之处,但我们可以在 proc 对象的作用范围中,使用它对任何包含 ruby 代码的字符串进行求解。这样,我们不需要直接引用某个对象,便可以在这个对象的上下文中求解 ruby 代码。

proc = ProcFactory.new.create<br></br> eval "self.class", proc.binding #=> ProcFactory什么时候会用到这样的功能呢?我最近在开发表示 SQL 的 DSL 时用到了它。我开始使用类似下面代码的语法:

Select[:column1, :column2].from[:table1, :table2].where do<br></br> equal table1.id, table2.table1_id<br></br> end上述代码被求解时,跟在 from 后面的 [] 实例方法将所有的表名保存在一个数组中。接下来,当执行 where 方法时,传递给 where 的 block 会执行。此时,method_missing 方法会被调用两次,第一次针对:table1,第二次针对:table2。在 method_missing 的调用中,对之前提到过的、用 [] 方法创建的表名数组进行检查,以查看标识符参数(:table1 和:table2)是否为合法的表名。如果表名在数组中,我们返回一个知道如何应对字段名称的对象;如果表名非法,我们会调用 super 并抛出 NameError。

应对一般的简单查询,上面的做法不存在问题;但如果涉及到子查询的话,就另当别论了。前述实现对下面示例中的代码是无效的。

Delete.from[:table1].where do<br></br> exists(Select[:column2].from[:table2].where do<br></br> equal table1.column1, table2.column2<br></br> end)<br></br> end不过我们可以使用 eval 与指定的 binding 一起,让上面的代码正常工作。此处的技巧是:将表名数组从外部的 block 隐式地传递到内部的 block 中。用显式方式传递会让 DSL 看起来很丑陋。

在 Select 类的 where 方法中,我们使用 block 的 binding 对象来得到 Delete 实例的 tables 集合。我们能够这样做,在于 Delete 实例的 where 方法被作为上下文(亦即 block 的 binding)传递给了 select 实例的 where 方法。binding 对象(或上下文)是 block 被创建时的作用范围。下面的代码是对 where 方法的完整实现。

def where(&block)<br></br> @text += " where "<br></br> tables.concat(eval("respond_to?(:tables) ? tables : []", <br></br>block.binding)).inspect<br></br> instance_eval &block<br></br> end我们把 eval 所在的语句拆开看看它都干了什么。它做的第一件事情是:

eval "respond_to?(:tables) ? tables : []", block.binding它的作用是“在 block 的作用范围中对语句进行求解”。在当前例子中,block 的作用范围是:

Delete.from[:table1].where do .. end这个范围是一个 Delete 类的实例,Delete 类中确实有 tables 方法,其作用是暴露表名数组(tables#=>[:table1])。因此,语句被求解后会返回表名数组。剩余的语句就可以看作:

tables.concat([:table1])此句只是将所有的表名加入到 tables 数组中,并且可以被内部的 block 访问。有了这一行代码的处理,我们就可以让子查询产生正确的结果了。

delete from table1 where exists (select column2 from table2 where table1.column1 = table2.column2)下面的代码可以产生上述结果,并且能够作为参考,以了解如何与 binding 一起使用 eval。

class Delete<br></br> def self.from<br></br> Delete.new<br></br> end<p> def [](*args)</p><br></br> @text = "delete from "<br></br> @text += args.join ","<br></br> @tables = args<br></br> self<br></br> end<p> attr_reader :tables</p><p> def where(&block)</p><br></br> @text += " where "<br></br> instance_eval &block<br></br> end<p> def exists(statement)</p><br></br> @text += "exists "<br></br> @text += statement<br></br> end<br></br> end<p> class Select</p><br></br> def self.[](*args)<br></br> self.new(*args)<br></br> end<p> def initialize(*columns)</p><br></br> @text = "select "<br></br> @text += columns.join ","<br></br> end<p> def from</p><br></br> @text += " from "<br></br> self<br></br> end<br></br> def [](*args)<br></br> @text += args.join ","<br></br> @tables = args<br></br> self<br></br> end<p> def tables</p><br></br> @tables<br></br> end<p> def where(&block)</p><br></br> @text += " where "<br></br> tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect<br></br> instance_eval &block<br></br> end<p> def method_missing(sym, *args)</p><br></br> super unless @tables.include? sym<br></br> klass = Class.new<br></br> klass.class_eval do<br></br> def initialize(table)<br></br> @table = table<br></br> end<p> def method_missing(sym, *args)</p><br></br> @table.to_s + "." + sym.to_s<br></br> end<br></br> end<br></br> klass.new(sym)<br></br> end<p> def equal(*args)</p><br></br> @text += args.join "="<br></br> end<br></br> end### 结语

正如我们所看到的那样,使用 Ruby 提供的多种求解方法,我们可以创建简练、可读的代码;这些求解方法同时提供了创建诸如领域特定语言之类强大工具的能力。

关于作者

Jay Fields 是 ThoughtWorks 的一位开发人员。他总是在寻找令人兴奋的新技术,并愿意马上采用这些技术。他最近一段时间的工作中心放在领域特定语言(DSL)上面,所交付的应用为特定业务领域专家使用 DSL 撰写应用业务规则提供了强大的支持。

查看英文原文: Evaluation Options in Ruby - - - - - -

译者简介:郑柯,目前就职于一家医药电子商务公司,从事医用耗材电子商务平台的开发与维护。有志于在中国的软件开发业界推广 Agile 的理念和方法论,笃信以人为本,关注 Ruby,关注敏捷,关注人。

2007-08-24 04:231639
用户头像

发布了 479 篇内容, 共 157.9 次阅读, 收获喜欢 49 次。

关注

评论

发布
暂无评论
发现更多内容

火山引擎“数据飞轮”助力教育行业持续优化产品

Geek_2d6073

测试一波回归模型的误差

EquatorCoco

人工智能 数据 回归 挖掘

解锁财务信任,掌握企业业务合作中的倾听艺术

智达方通

业财融合 全面预算管理 财务管理 经营管理

打破购物局限!了解闲鱼商品详情关键词搜索电商API接口,挖掘不一样的购物乐趣!

联讯数据

如何做代币分析:以 TRX 币为例

Footprint Analytics

加密货币 Token 代币

软件测试学习笔记丨Docker网络模式与Docker-compose介绍

测试人

软件测试 测试开发

讲讲鸿蒙开源与技术细节

Geek_2305a8

金三银四,聊一聊测试跳槽/面试的必备技能

霍格沃兹测试开发学社

SD-WAN技术简化企业网络架构的关键优势

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

敏捷开发最佳实践:团队维度实践案例——打造敏捷“绿洲”

爱吃小舅的鱼

敏捷 敏捷开发

SATX合约代币矩阵公排系统开发详情

l8l259l3365

为什么说第三代指标平台的本质是做 “轻” 数仓?

Aloudata

ETL 指标平台

AI与人类联手,智能排序人类决策:RLHF标注工具打造协同标注新纪元,重塑AI训练体验

汀丶人工智能

大模型 智能标注 RLHF

强大好用的shell:什么是shell?

小魏写代码

智能护航:人工智能引领软件测试新革命

测吧(北京)科技有限公司

测试

都说了别用BeanUtils.copyProperties,这不翻车了吧

不在线第一只蜗牛

Java 数据库 后端 Java后端

SD-WAN与MPLS哪一个是最优选择?

Ogcloud

SD-WAN MPLS SD-WAN组网 SD-WAN服务商 SDWAN

SD-WAN在银行的应用:降低维护成本、提升网络安全

Ogcloud

SD-WAN 企业网络 SD-WAN组网 SD-WAN服务商 SDWAN

苹果上架App被拒绝的原因

遇见您的私人法律顾问:智能法律大模型,智能解答您的法律困惑

汀丶人工智能

人工智能 智能问答 法律大模型

吴恩达AI系列第一课:教你如何利用AI创建一个披萨店客服

cloud studio AI应用

Cloud Cloud Studio 人工智能、

面试官:说一下红锁RedLock的实现原理?

王磊

Java 面试

为多渠道销售集成商品API接口的正式步骤指南

Noah

优雅使用前端枚举Enum,符合国标的那种!

不在线第一只蜗牛

前端 开发 前端框架 Enum

unsubscribe:Angular 项目中常见场景以及是否需要 unsubscribe

OpenTiny社区

前端 angular

deepin Meetup成都站来了!一起聊聊deepin-IDE 2.0,还有礼品可以拿!

nn-30

flutter Linux 操作系统 linux开发 deepin

人工智能引领软件测试新的巨大变革

霍格沃兹测试开发学社

华为云携十大系统性创新亮相巴塞罗那 打造最适合AI的基础设施

华为云开发者联盟

云计算 AI 华为云 华为云开发者联盟

英特尔亮相MWC 2024,助力企业通过现代化以实现盈利

E科讯

文心一言 VS 讯飞星火 VS chatgpt (203)-- 算法导论15.3 2题

福大大架构师每日一题

福大大架构师每日一题

在Ruby中对字符串和block求解_Ruby_Jay Fields_InfoQ精选文章