NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

使用单实例类来处理对象元信息

  • 2008-01-21
  • 本文字数:3114 字

    阅读完需:约 10 分钟

假设你有大量的对象 —— 一个对象图 ——它们是一些操作或者 API 调用的结果。任务:分析数据并将分析结果作为对象图的元数据。

这么说过于抽象?请考虑一个编辑器是如何工作的:解析器产生一个代表代码的树状结构的解析树(或者抽象语法树,AST)。然后:许多算法遍历解析树,处理数据:收集在符号表中的符号,做类型推论,使用类型做数据类型检查,等等。

但是请稍等:最后两步处理有问题:类型推论代码在哪里存贮它产生的数据为类型检查者使用?最方便的方式是将数据存储到需要数据的地方 —— 例如,如果推论出(AST 中的)一个表达式节点返回类型 Foo,那么最好就在这个节点中存储该信息。

为了说明我们的解决方案,我们会看一些 AST 方面的工具 —— 不是一个编译器,而是与编译器有类似需求的工具。在 Ruby 中, ParseTree 类库以 AST 的形式返回 Ruby 源代码。例子:

[:vcall, :obj, :say_hello, [:array, [:lit, 42], [:lvar, :foo]]]这是一些以 ParseTree AST 方式展现的 Ruby 代码。由于此处是一篇文章而不是一个虚拟机,以对象和组织为一个图的对象引用的方式来说明问题有点困难 —— 所以我们使用 ParseTree 的 s-expr 形式来表示树。S-exprs 是嵌套的列表,每个列表代表树中的一个节点,列表的第一个元素表明了节点的类型。在上例中,该节点代表对一个虚拟方法(vcall)的调用。参数包括方法接收者(即被调用的方法中的“self”对象),方法名称和方法的参数。

我们讨论的工具可以是一个类型推论器,静态分析工具或者自动重构工具。对于这类工具的工作需求之一是类型推论,也就是,决定变量或者表达式的类型。例如,这个 AST 子句代表了一个对方法 to_s 的调用:

[:vcall, :obj, :to_s]从上面的句子中可以收集到什么信息,可以用这些信息做什么?例如,返回值的类型是难以决定的 —— 但是既然它是 to_s 方法,它会返回代表一个对象的字符串 —— 那就让我们认为它返回”String”类型。这不是必然为真的 —— 这是一个猜测。对于诸如代码补全之类的工具,这个推测已经足够好了 —— 100%的精确在此情况下是不可能的。在某些情况下,可以收集到更多信息,分析器可以确定更精确的信息。

分析器处理解析树,用分析产生的元数据来注解 AST。为了保持代码的模块化,将分析器与元数据的消费者分离是个好主意。元数据的消费者可能是遍历 AST 并使用元数据做某些事的代码。例如,Ruby 编辑器可以高亮一个覆盖了其超类中方法的方法。

解决方案

让我们长话短说:这里是一个 ParseTree 节点的注解方案:

node = [:vcall, :obj, :to_s] <br></br>def node.set_metadata(key, value)<br></br>  @_metadata ||= Hash.new<br></br>  @_metadata[key] = value<br></br>end <br></br>def node.metadata<br></br> @_metadata ||= {}<br></br>end<br></br>node.set_metadata(:type, :String)这段代码是做什么的?

秘密武器:单实例类

在 Ruby 中每个对象都是一个类的实例。不象许多其他 OOP 语言,Ruby 允许你改变一个对象的类。不要把改变对象的类与打开类(open class)相混淆。在 Ruby 中,修改一个类是可能的 —— 甚至在运行时。单实例类同样是可以修改的 —— 只不过它们的改变仅仅影响一个对象的类。看起来差别似乎很小 —— 但是它对于限制修改类对该类对象带来的影响有很大的好处。另一方面,打开类对类定义的修改影响了所有的代码和所有相应对象。打开类是有用的,但是也可能会和其他人的代码搅和在一起,而且如果有太多的代码在公共类上作打开动作,可能会与已有方法发生名称冲突。在 ParseTree 节点这个案例中 —— 这些节点是普通的 Ruby 数组 —— 意味着仅仅真正被使用的数组对象才受到影响 —— 而不是在堆中的所有的数组都受到了影响。

再换个角度来看:使用单实例类,对于类的改变是局部化的,仅对产生和使用单实例类的代码有影响 —— 这些变化对于外界永远是不可见的。而另一方面,打开类是全局性的改变:一个类名称是一个全局的变量,例如“String”指向代表字符串的类对象。就像作用域较小的变量(本地变量,成员变量)应该比作用域较大的全局变量更优先使用,在单实例类中作出的变化也应该优先于打开类。

当然,选择哪个方案(单实例或者打开类)取决于具体的情况。对于打开类,类的改变发生一次 —— 在改变之后,该类的所有对象都拥有了增加的方法。对于单实例类,类的改变(即创建单实例类,改变对象的类指针指向单实例类)在每次改变的时候都发生。

如下所示,语法是简单的:

def object_variable.method_name()<br></br> # code<br></br>end指向对象的变量前缀于方法名。一种更灵活的更模块化的方式是使用混入(Mixin)来做。混入允许将处理某些方面的方法集合在一个模块中然后将这些方法一次性的混入到类中。例如:

复制代码
module Metadata
def set_metadata(key, val)
@_metadata[key] = val
end
def metadata
@_metadata ||= {}
end
end
x = [:vcall, :obj, :to_s]
x.extend Metadata
x.set_metadata(:type, :String)

混入允许将定义在一个模块中的方法混入一个类中 —— 在这里,混入的是节点对象的单实例类。再次重申:现在只有这个对象具有这些混入的方法。

另一个例子 —— 除去死代码

如果静态分析器代码的例子过于抽象,那么让我们尝试另外一个工具:一个死代码移除器。“死代码”是实际上没有做任何事情的代码,它们被除去后不会影响程序的行为。例如这样的代码:

for x in [1,2,3] do<br></br>end根据我们的注解概念,我们可以找出这类代码并象这样来注解:

node.metadata(:dead_code, :true)但是将代码标记为死的仅仅是第一步 —— 现在我们需要除去它们。在这里我们会容易看到将代码分析和具体动作分离的意义。一个 IDE 也许仅仅希望高亮某些代码为死代码,但是 IDE 也可能提供了一种简单的方式(一种快速修复)来除去代码。

如何除去这段死代码?当然 ,可以将节点从 AST 中取出然后使用 Ruby2Ruby 来生成 Ruby 源代码。但是,这不是一个非常优美的解决方案:Ruby2Ruby 将 ParseTree AST 转换为 Ruby 源代码,但是它会丢失很多有用的信息,例如格式(空格)和注释。在 IDE 或者重构 / 清理工具中丢失这些信息是不可接受的。

元数据来救场了:节点可以用注解来记录来源的位置 —— 例如:在起初的源代码中的什么地方发现了这个特定的节点。有了这个信息就容易了:清理代码只需要扫描代码,发现所有标记为 dead_code 的节点并根据元数据中的源代码位置的字符偏移量将这些节点在源文件中删除。(当然 一旦这些节点被删除了,剩下的节点的偏移量会变化。这个问题可以通过简单地跟踪被删除的字符数量,并将此数量从实际的偏移量中减去来解决。这样,就不必重新解析和分析代码了)。

结论

这篇文章中的例子着重于语言方面的工具 —— 但是文章的思想是适用于所有的对象集合的。在任何对象图需要被注解的地方,也许是在独立开发的解析器的多个处理步骤中,将注解与节点一起保存是很方便的。如果对象图的类不是在开发者的控制之下而且在其他地方被广泛使用(例如,在 ParseTree 中的数组类),单实例类是个好的选择。对于其他情况,打开类也许是一个更好的方案。

本文所提到的特殊工具的实现:ParseTree 在 Ruby 1.8. x、Rubinius 和 JRuby ( jparsetree ) 中都有实现。JRuby 中的 ParseTree 版本增加了每个节点的来源位置信息,所以更容易修改源文件。愿你也能想出一些工具的创意并实现它们 —— 用 Ruby 是很容易的。

查看英文原文 Using singleton classes for object metadata


译者简介: 曹云飞,西安交通大学计算机软件硕士。现就职于 Ethos ,热衷于计算机理论与应用技术的钻研,软件架构与敏捷开发,目前从事 consumer product 方面的工作。参与 InfoQ 中文站内容建设,请邮件至 china-editorial@infoq.com

2008-01-21 02:56966
用户头像

发布了 47 篇内容, 共 99724 次阅读, 收获喜欢 3 次。

关注

评论

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

中国为什么加快推进数字人民币

CECBC

数字货币

硬核!八张图搞懂 Flink 端到端精准一次处理语义 Exactly-once(深入原理,建议收藏)

五分钟学大数据

大数据 flink

精美的淘客项目完全开源啦,确定不来围观吗

Silently9527

Vue mybatis springboot uniapp

产品训练营第三次作业

Geek_79e983

区块链在数字版权领域的应用发展报告(2020)

CECBC

版权保护

京东支付SDK重构设计与实现

京东数科风险算法与技术

管理笔记[4]:组织管理的目标就是实现1+1>2

L3C老司机

「产品经理训练营」作业03:利益相关方识别

狷介

产品经理训练营

【LeetCode】子集问题debug模式查看数据变化

Albert

算法 LeetCode 2月春节不断更

批判性思维自修课(六)

石君

28天写作 批判性思维

Spring - IOC

小马哥

Java 架构师 spring 5 七日更

科技,亲吻这个特别的春节

脑极体

区块链--另一场改变社会组织方式的工业革命

CECBC

区块链

不会开发的你也能管理好企业漏洞,开源免费工具:洞察(insight II)

BigYoung

安全 安全漏洞 28天写作 2月春节不断更

28天瞎写的第二百三十五天:客服得有多专业?

树上

28天写作

架构师训练营第十周作业

zamkai

PanoVideoCall 的 Electron Demo 开源了

拍乐云Pano

html Mac windows Electron js

夕四今晚加班到2点30,而王二还不打算走《打工人的那些事》

谙忆

分页方式,看这一篇就够了。

大伟

分页

一文看完TCP/IP协议基础知识点

为啥写的mybatis插件没用?一场mybatis插件加载机制的探索之旅

altantisor

spring Boot Starter mybatis源码

架构训练营大作业(一)

一期一会

看完字节大佬的算法刷题宝典,我直接手撕了500道算法算法题

程序员 面试 算法

静默安装Oracle也没那么恐怖

MySQL从删库到跑路

oracle

翻译:《实用的Python编程》README

codists

Python

Java并发包源码学习系列:阻塞队列实现之SynchronousQueue源码解析

Java 编程

学习计算机视觉

IT蜗壳-Tango

OpenCV 计算机视觉 2月春节不断更

「抖音同款播放器」上市:有效解决卡顿、黑屏和模糊

字节跳动技术团队

架构训练营大作业(二)

一期一会

《学会写作》学习笔记之如何拟标题

JiangX

28天写作

翻译:《实用的Python编程》00_Setup

codists

Python

使用单实例类来处理对象元信息_Ruby_Werner Schuster_InfoQ精选文章