2025上半年,最新 AI实践都在这!20+ 应用案例,任听一场议题就值回票价 了解详情
写点什么

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

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

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

关注

评论

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

荣誉再加码!天翼云揽获SD-WAN & SASE大会两项大奖!

科技热闻

探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?

左诗右码

Go

如何设置自动化测试断言?

老张

软件测试 自动化测试 接口测试 质量保障

【JIT/极态云】技术文档--钉钉自建组织

武汉万云网络科技有限公司

低代码

ticdc 同步延迟指标与原理解析

TiDB 社区干货传送门

6.x 实践

TiDB v8.x Tiproxy组件

TiDB 社区干货传送门

8.x 实践

双 11 营销活动数量、转化率双提升,火山引擎数智平台为此都做了什么?

字节跳动数据平台

智能车间管理系统(源码+文档+部署+讲解)

深圳亥时科技

全球司库 | 科学掌握企业融资债券数据信息,需从这几个维度展开!

用友智能财务

财务 企业数智化 司库

拼多多商品评价API的获取与应用

科普小能手

拼多多 API API 接口 拼多多商品详情接口 拼多多API

CST软件如何使用Poser人体摆姿势

思茂信息

cst cst使用教程 CST软件

淘宝API接口探索:图片搜索拍立淘与商品评论的深度挖掘

代码忍者

API 接口 pinduoduo API

HyperWorks基于 Shrink Warp Mesh 的零部件网格剖分

智造软件

仿真 仿真软件 Hypermesh

DR Auto-Sync:TiDB 同城两中心自适应同步复制技术解析

TiDB 社区干货传送门

新版本/特性解读

端侧AI,风起移动智能计算

脑极体

芯片

鸿蒙安全控件之粘贴控件简介

龙儿筝

【创新视角】解锁淘宝商品详情API:让商品主图与详情图“跃然屏上”,重塑购物体验魅力!

代码忍者

API 接口 pinduoduo API

【线上发布会预约中】资源有限,性能无限:GreptimeDB Edge 赋能车端数据处理新高度

Greptime 格睿科技

数据库 汽车 性能报告

⭐️ GitHub Star 数量前十的工作流项目

NocoBase

GitHub 开源 工作流 工作流引擎 工作流自动化

【GreatSQL优化器-04】贪婪搜索算法浅析

GreatSQL

携手上海证券,共同见证市场活跃背景下交易服务新趋势

非凸科技

上海证券 非凸科技

“老爷机”训不动Lora?一台云电脑就可以让你轻松炼丹

Finovy Cloud

LoRa 云电脑

山西省等保测评公司名单【2025】

行云管家

网络安全 等保 山西

【等保小知识】信息系统怎么定义?等保测评多久一次?

行云管家

信息系统 等保 等保测评

企业采购比价:品牌采购时借用淘宝商品详情接口来采购比价

tbapi

淘宝商品详情接口

【JIT/极态云】技术文档--标准组织

武汉万云网络科技有限公司

低代码

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