这个国庆假期,我读完了《D程序设计语言》 一书。里面读到了很多有趣的东西,挑一点写出来和大家分享一下。
字符串,数组和关联数组(hash 表)是最重要的三种数据结构,我们几乎可以利用它们模拟出任何更复杂的结构。Lua 就是这么干的,只不过 Lua 把数组和关联数组合并成一个 table 类型了。D 在语言层面对这三种数据结构支持的很好,概念定义非常清晰。这一篇只谈数组和字符串,不涉及 hash 表的部分。
数组可以看成是存放同一类型数据的连续内存。
在 C 语言中,数组和指针虽然是不同的类型,但编译器生成的代码却是相同的,可以说实质上,数组即指针。但将数组隐含有长度信息,即内存的范围。有些数组是固定大小的,在编译器就知道其范围;有些数组需要动态扩展大小,其范围是运行期确定,并可以改变的。无论如何,对数组的随机访问,缺乏边界检查的代码都隐藏着风险。
D 语言是一门期望有高安全性的同时又重视运行性能的语言。它在平衡这个问题上的解决方案很有趣。程序员可以指定一段代码是安全的,还是系统级的,还是是接口安全的。根据不同的标注来插入边界检查代码。在 debug 版中,即使是系统级代码,也会插入类似 assert 的契约检查。
由于 D 语言以 GC 为内存管理核心(且要求所有数据都是位置无关,可移动的),所以管理数组切片 Slice 就变得很简单。不同的Slice 引用同一块内存,不用担心数据生命期问题。扩展数组也可以根据需要重新分配内存,或是在原地扩展。
提到数组扩展,不得不谈一下 D 语言中结构的 postblit 。D 语言中,所有的 class 都是引用语义的,而 struct 是值语义的。C++ 中花了很多年想解决的一个性能问题就是源于 vector 扩展时,数据如何从旧的位置移动新位置的问题。在 stl 的 sgi 实现中,为 POD 结构增加的特化模板来提高复制效率;在 C++11 中又从语言层面增加了右值引用来实现移动语义,来解决反复析构构造对象带来的性能浪费。
而 D 语言中没有那些晦涩的移动构造,拷贝构造概念;它只有 postblit 。也就是数据都应该默认按位复制(blit),然后在 blit 后,再用 postblit 方法去修改新的副本。这种不动源对象,而只在副本上修改的移动钩子技术概念更简单清晰。而且编译器可以自行推导什么时候调用 postblit 才是必要的。这个技术不仅仅用来解决数组的扩展问题,也可以很好的搞定 C++ 中返回值优化问题。
对于固定大小的数组,D (2.0) 是按值类型处理的(动态数组则是引用类型),不同长度的数组是不同的类型,但它们都可以隐式转换(映射)成动态数组。比较短的固定数组做值传递的时候更方便高效,也符合其它基础类型的特征。长数组可以通过 ref 修饰按引用传递。