使用C++构建最简单的动态类型系统

来源:互联网 发布:摇粒绒和法兰绒 知乎 编辑:程序博客网 时间:2024/06/03 17:26

为了说明动态类型系统和静态类型系统之间的差异和联系,这里简单叙述一下如何使用静态类型系统来构建动态类型系统。这个任务类似于为一门新的语言设计类型系统。所以如果你发现这个动态类型系统类似于你使用过的任何OO系统,请原谅我。应为所有的动态类型系统都是类似的,而且其背后的机理完全相同,只是实现上的差异而已。

 

先列出OO的核心技术,这样在我们设计的时候可以时刻对比我们是不是在这样做。

* 使用类来表达概念

* 使用消息在类之间传递信息

 

我们动态类型系统的期望目标很简单,那就是可以动态添加成员和方法。有了这个,我们就可以实现几乎所有的动态系统的特性了,如不特定消息发送,避免虚函数调用的开销,更简单的函数覆盖实现等等。

 

动态的类型系统,那就意味着运行时我们有一切的类型信息。所有我们需要把类型信息以合适的方式存储起来。在C++中,基础的类型就有十几种,更不要说用户创建的更多类型了。怎么办呢?简单想一下,就明白,我们只要一简单的字符串表达类型即可。不是使用字符串本身的类型,而是使用字符串对象的内容,不同的内容表示不同的类型即可。于是,我们可以说,对象Foo有如下的成员,其类型为“double”,其名字为weight,其值为94.5。可以看到,我们需要三个不同维度的东西来表达一个成员。C++中之需要两个就可以了,类型信息是静态的,编译期会处理。所以,这里的核心是:把类型信息表达为数据。

 

另一方面,C++成员的名字的硬编码的,在执行的时候,这些名字已经被地址替代了。这不是我们想要的,所以,我们要进一步把成员的名字也转变为数据存储起来。这里,名字可以直接映射为字符串。

 

最后就是对应的值。C++中,不同类型的值在地址中有完全不同的表达。在动态系统中如何表达呢?对比上面的例子,可以看到,我们完全可以把值的不同表达转化为字串表达出来。

 

于是,我们可以说,对象Foo的一个成员其类型为“double”,其名字为“weight”,其值为“94.5”。它当然可能有多个成员,所以在C++中,我们可以把它的成员表达为:

struct Obj {

  std::map<std::string, std::pair<std::string, std::string>> members_;

};

其中第一个string是指成员名,第二个是类型,第三个是值。

 

一个类总是要实现某种型。这对应于一个消息的处理,就是一个针对特定消息的执行过程,那就是函数。一个消息附带的参数千差万别,如果我们想要一致地处理可能的消息,就要把消息以一种一致的方式表达出来。一个消息所包含的信息有:

* 消息名字。必须的

* 消息参数。必须的,空参数是一种特殊情况

 

于是可以简单吧消息定义为:

struct Msg {

  std::string name_;

  std::vector<obj> params_;

};

注意,因为消息的参数本身也是动态的,我们需要而且已经使用了上述的对象定义。

 

而一个消息传递过程则涉及:

* 消息发送者。可选的

* 消息接收者。必须的

* 消息本身。必须的

 

于是,我们可以先假设消息可以通过如下的当时传递

extern void send_to(std::string receiver, Msg msg, std::string sender = "");

参数的顺序是无所谓的,把sender放在最后只是为了让这个参数为可选。

 

现在回到讨论如何处理一个消息。第一个问题是,动态系统中,如何找到对象接收者的?我们必须有一个结构用来存储整个执行器的所有活动对象。那就意味要有一个对象管理器,它管理者所有的全局对象。可以暂定义如下:

struct ObjManager {

  std::map<std::string, Obj> objs_;

};

 

这样,我们就可以找到合适的对象了。如果该对象不存在,那就是运行时错误了。因为动态类型系统没有编译期来保证类型的正确性,程序员必须确保他要引用的对象确实存在而且状态正确。

 

第二个问题是,如何在对象中找到匹配的消息处理函数?由于函数也要求是动态的,对象自己必须维护一个可处理的消息的列表。消息的处理原型可以是std::string chomp_msg(Msg msg);。所以对象定义可以扩展为:

typedef std::string (*pf_chomp_msg)(Msg);

struct Obj {

  std::map<std::string, std::pair<std::string, std::string>> members_;

  std::map<std::string, pf_chomp_msg> functions_;

};

 

这就基本齐了。加上必要的辅助构造,就是一个基本可用的动态类型系统了。注意,这里没有说是OO动态类型系统。因为OO技术可以在类型系统之上搭建。我们看一下:

* 没有型的概念,型直接绑定到对象实现;

* 没有类的概念,对象直接表征了类的实现;

* 因为没有型,没有接口继承的概念。

* 实现继承则表现为

* 覆盖和重载的区别消失了,都表现为把消息名字映射到新的消息处理函数。

* 没有专门的信息隐藏支持

* 不特定的消息处理

 

动态类型系统不是没有缺点的。一个最明显的问题就是成员名字和函数名字在每一个对象中都要存储。如果一个类有多个实例,那么这个开销是非常大的。可以使用类似FlyWeight这样的设计模式来部分地改善这个问题。一个典型的例子就是Chrome V8实现中对JavaScript对象存储的优化。

 

很多时候,当不存在与静态类型语言交互的需求时,保存类型信息是没有多少用处的,有值就可以了。所以这个简单的模型还可以精简。

 


结论:通过这个简单的例子可以看到,动态类型系统个静态类型系统的差别其实并不大,主要在于在什么时候以及地方存储类型信息。编译期存储时,你获得的好处是编译期可以帮你检验类型完整性;运行时存储时,你获得的好处是更加灵活,一般也要更慢一点。使用什么系统完全取决于具体的应用需要。

 

另一方面,OO完全是凌驾于类型系统之上的。只要实现了OO的核心技术,再加上必要的工具支持(继承,覆盖等),就可以宣称是OO系统了。能不能把这些区别开,是有效学习理解这些技术的关键。

 

注:上述伪码未经运行验证。

 

原创粉丝点击