50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

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

  • 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:561638
用户头像

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

关注

评论

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

从逻辑到硬件:如何转换PCB布局?

华秋PCB

工具 电路 PCB PCB布局 PCB设计

ByteHouse技术白皮书正式发布,云数仓核心技术能力首次全面解读(内附下载链接)

字节跳动数据平台

数据仓库 云原生 白皮书 数据存储 企业号 4 月 PK 榜

云原生月报丨阿里云云原生月度动态(202303)

阿里巴巴云原生

阿里云 云原生 月报

“阿里味”GitHub上新软件架构设计与业务架构融合手册

Java 架构 架构设计

LeetCode题解:136. 只出现一次的数字,哈希表,JavaScript,详细注释

Lee Chen

JavaScript LeetCode

基于 Flink ML 搭建的智能运维算法服务及应用

Apache Flink

大数据 flink 实时计算

深度学习基础入门篇[二]:机器学习常用评估指标:AUC、mAP、IS、FID、Perplexity、BLEU、ROUGE等详解 1.基础指

汀丶人工智能

人工智能 机器学习 深度学习 算法评价指标

2023年成都.NET线下技术沙龙来了!大咖分享,报名从速

MASA技术团队

.net dapr MASA

2023 年金三银四最新版 Java 面试八股文教程,涵盖 25 大专题:Java 基础 +spring 全家桶 + 大数据 + 网络 + 设计模式 + 算法

三十而立

音视频处理MCP:视频版权保护

百度开发者中心

音视频 智能视频 视频版权保护

新浪顶级架构师保驾护航!国内首本大型分布式架构笔记浴火新生

Java 架构 分布式

Serverless冷启动:如何让函数计算更快更强?

华为云开发者联盟

云原生 后端 华为云 华为云开发者联盟 企业号 4 月 PK 榜

国产数字化升级工具强势来袭,瓴羊Quick BI工具免费试用

对不起该用户已成仙‖

从零学习SDK(2)SDK的基本概念和组成部分

MobTech袤博科技

生成式AI已形成全球性“AI再造业务”趋势

百度开发者中心

#人工智能 文心一言 文心一格

远程调试为何要亲历现场,也许也可以这样解决

石臻臻的杂货铺

远程调试

目前led显示屏厂家存在的问题

Dylan

制造 行业 LED显示屏

【干货】验证码的常见类型总结

宙哈哈

php html 验证码 短信验证码

阿里工作10年,我总结出了这份1071页Spring全家桶核心笔记

三十而立

从Spring的AOP看Synchronized锁失效和事务失效的情况

Flink SQL 在美团实时数仓中的增强与实践

Apache Flink

大数据 flink 实时计算

深入探索Go语言的unsafe包,揭秘它的黑科技和应用场景!

王中阳Go

golang 高效工作 面试题 黑科技 Go 语言

6步带你用Spring Boot开发出商城高并发秒杀系统

华为云开发者联盟

高并发 开发 华为云 华为云开发者联盟 企业号 4 月 PK 榜

Reactor线程模型的演进和局部无锁化

物联网核心套件IoTCore:设备状态数据存储到时序数据库TSDB

百度开发者中心

物联网

音视频处理MCP:视频添加字幕

百度开发者中心

视频 音视频开发 智能视频

系统天气再现bug 网友:墨迹天气赶紧上!

极客天地

一键快速切换工具:One Switch 1.29中文版

真大的脸盆

Mac Mac 软件 切换工具 一键切换

硬核!互联网资深大佬手码高并发编程速成笔记(2023版)限时开源

三十而立

Java IT java面试

一文快速了解火山引擎A/B测试平台

字节跳动数据平台

大数据 AB testing实战 A/B 测试 企业号 4 月 PK 榜

Apache Paimon 在同程旅行的探索实践

Apache Flink

大数据 flink 实时计算

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