D语言真相
Andrei Alexandrescu 著
译的不好,请大家指正!
06月18日 00:38
06月19日 23:45
让我们看看为什么D语言是值得认真研究的.
当然,我不能哄骗自己说服你很容易. 我们程序员是一群以奇怪的方式来组成和维持语言偏好的.
程序员下意识的反应是在书店看到某某编程语言的书籍后想:好了,我给自己30秒内能找到我不喜欢这个语言的地方.获得编程语言的专业知识是一个长期而艰苦的过程,并且总是充满拖延和不确定性.试图寻找快捷理由来避免努力是一种生存本能:风险高,投资非常危险,因此早在学习过程前就迅速作出消极决定能让我们获得巨大的安慰.
尽管如此,学习和使用一门编程语言是很有乐趣的.由于用一门语言编码是很有趣的:如果这门语言做了令人满意的工作,让人满意的原则就是(让人)带着崇高的敬意使用它.任何一个原因都会让程序员关心语言,例如语言不认真,不安全,自以为是,或者非常乏味.同时,语言不可能满足大家的需求和口味,许多人要求也是矛盾的.因此它必须认真致力于几个基本原则才能适应编程语言的广阔场景.
因此,如何对待D呢?你可能已经听说它了.
在本文中我介绍了大体的概况,这意味着没有严格介绍他们的概念和功能,我必然要使用合理的直觉.
让我们看看一些主要的D语言基本特征.请注意,许多功能或者限制附加条件让他们界限模糊了.所以,如果你读的东西不完全让你满意,请不要担心:下一句可能包含附加的解释.例如,假设你读到"D包含垃圾收集",让你毛骨悚然,心灰意冷,要敬而远之.但如果你有耐心,你会发现,D有构造函数和析构函数,你可以实现一个确定生命周期的对象.
在了解很多的事情之前,有几件事你应该知道.首先,不管你是以什么原因发现的D语言,这一次不是"和其他的一样",如果你早日选择,这实际上大大优于其他的语言.D相对沉默,但是一直在以惊人速度不断变化.许多功能已经完成,正在做的事也成为了已知,就在本文中.在写本文时,我的书<<D编程语言>>已经40%完成,并且可以在Amazon预定,Safari(http://safaribooksonline.com/Corporate/Index/)的订阅服务可以提供一些完成的章节.
这里有两个主要的版本:D1和D2.本文焦点仅在D2.D1是一个稳定版本(将不进行修改除了修正错误).D2是一个重大修改版本,为了做的一贯正确,牺牲了向后兼容,增加了一些关键功能,有关多核和范型编程.在这个过程中,语言的复杂性有所增加.这实际上是一个很好的指标,因为没有实际使用的语言都变的更小.即使语言开始声明的意图就是"小而美"的也不可避免的增长和使用.(是的,甚至是Lisp语言,备用).虽然程序员梦想小巧,简单的语言.当他们醒来,他们似乎只想要更多的建模能力.D语言的转变就是让你在不值得羡慕的立场去处理一个移动的目标.我选择写一篇文章,因为描述的功能可以工作或者不完全执行.
官方D编译器是免费提供的,在digitalmars.com包含桌面平台( Windows ,苹果机和Linux),其他还正在实现中,特别是包括.NET和一个使用LLVM作为后端.此外,还有两个重要的D基本库库,官方-Phobos和社区推动Tango. Tango设计适用于D1,正在移植到D2中, Phobos(这是令人沮丧的古怪的D1迭代实现)正在发生重大修改和补充,以充分利用D2的功能.(因此,毫不奇怪,很多门派讨论哪些基本库更好,但竞争激励让他们一样好.)
最后但绝非不重要的是,两个窗口库实现相当壮观.成熟的DWT库是一个SWT的D实现.新的发展是很受欢迎的Qt库也公布了一个D绑定(在写Alpha版本).Qt是一个伟大的(最好的,如果你听了正确的人)库,开发轻便的GUI界面应用.这两个库让D达到了GUI层面.
2.D基本法则
D能被最好的描述是高层次系统编程语言.它包含的功能是在高层次,甚至是脚本语言.象快速的编辑-运行周期,垃圾收集,内建哈希表,或者省略类型定义,但也有低层次的功能例如指针,手工内存管理(象C的malloc/free)或者半自动的(使用构造,析构和独特的scope声明)方法.通常和C/C++程序员知道和喜爱的内存管理直接联系.事实上,D能直接链接和调用C函数,不用任何解释层.整个C标准库都对D程序直接有效.然而,你很少会调用底层,因为D本身的能力,往往更强大,更安全和高效.总的来说,D提供了强类型声明,方法和效率并不一定矛盾.除了更高级别的主题,我们会尽快讨论,没有提及细节的描述,那么D的说明将是不完整的.所有变量都会被初始化,除非你初始化变量为void.数组和关联数组直观好看.迭代清晰,NaN真正在用.重载规则可以理解,内置支持文档和单元测试.D也是多模式的:也就是它包含了面向对象编程,范型编程,函数编程和过程编程功能,多种风格可以无缝封装在小的包中. 下面易于理解的章节介绍D的一些通用部分.
你好,初级例子
让我们看看,没有烦人的语法,所以干脆痛快:
import std.stdio; void main() { writeln("Hello, world!"); }
语法象多数人了解的语法一样-合理.对语言表面(语法)关心多了,我们理解应该是没有多大的差别.但令一方面,我们也不能阻止人们注意语法.(我记得从黑客帝国中的红衣女孩到现在),对于我们大多数人来说,D有很多相类似的语法,因为它是C风格的语法.象C++,Java,C#中的语法风格一样.(我假设你熟悉其中的一个,所以我不需要解释.D有意料中的功能,如整数,浮点数,数组,迭代和递归.
在谈到其他语言,请允许写一个初级例子的C和C++版本: "Hello World".经典的C版本,在K&R的第二版中,看起来像这样:
#include <stdio.h> main() { printf("hello, world\n"); }
等价的C++版本例子是(热情的补充)
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
}
许多流行的比较,是用各种语言写的第一个例子的代码长度和需要了解信息的数量.讨论的正确性会让我们采用不同的途径.即会发生什么原因使写入标志输出的贺词失败?当然,C语言回来的错误,因为它没有检查printf的返回值.说实话,其实是有点糟糕,虽然我的系统上无标志编译和运行,C版本的helloworld返回的值在某些操作系统不可以预测,因为在main函数的结尾不可预测.(在我的机器,它会返回13,这让我有些害怕.然后我意识到为什么了:"hello, world\n"有13个字符,printf返回了打印的字符数量,因此吧13存放在EAX寄存器.幸运的是退出代码不理会寄存器;因此操作系统最终是13.).原来结果是C89和C99的标准中给出的程序不正确.在一些搜索中,网络上看来正确的常用C语言打招呼是:
#include < stdio.h> int main() { printf("hello, world\n"); return 0; }
这是正确的.因为它取代了无法预料的返回值,不论打印是否成功,都会返回成功(给操作系统)
在C++程序中,如果你忘记返回值,C++会保证从main返回0,而且还忽略了错误.在程序开始时
#include <stdio.h> int main() { return printf("hello, world\n") < 0; }
和
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
return std::cout.bad();
}
进一步调查显示,其他语言典型的"hello,world",如Java (由于空间限制代码省略) ,J# (一种完全语言-我的意思是完全的Java无关),或Perl ,所有例子同样成功.您几乎认为这是一个阴谋,但幸运的是,喜欢的Python和C#的会抛出一个异常.
为D版本会怎么办?当然,它不需要任何改变: writeln会在失败时抛出一个异常.异常的主要原因的信息会打印到标准错误流(如果可能的话),并以失败退出代码,总之,正确的事情是自动完成的.我不会采取这种初级例子.如果没有,原因有两个:第一,想象街头数百万程序员对他们的"Hello, world"程序是冒牌的来哭喊是很有趣的.(图片上口号"hello,world! Exit Code 13"巧合吗?或者,"您好,沉睡的人,醒醒吧!"等等).第二,例子不是孤立的,而是说明了一个普遍模式,D不仅试图让你做正确的事,只要有可能,它会系统地让正确的阻力到最小的方向.和原来的一样,这些常常会超过一个人的想象.(在你修改我的代码前,你应该认为"void main()"是一个合法的D语法.在C++新闻组中,他们需要找到另外一个喜欢的工具而切换到D中,语言学家会让新手用int main()来替换void main().)
够了,我打算讨论的语法和语义结束了.回到语法上来,有一个和C++,C#,Java有明显不同的是D用 T!(X, Y, Z) 代替了 T<X, Y, Z>(T!(X)或者T!X 来代替T<X>)来表示参数化类型.有几个好的理由.使用大于号和小于号,在进行"<"左移,">"右移或者">>"的算术操作时会冲突.C++有巨大的代码解析的问题.导致特殊规则的牺牲和任意的歧义.更不用说世界上很少有人知道object.template fun<arg>()的语法.如果你的一个C++学术研究人有了超人级别的自信,问问他们这些语法在做什么,你会看到超人克星工作了.java和C#也采用了大于小于号,但明智的不运行算术表达式作为参数,从而把这个选择留给了将来.D扩展了传统的一元运算符"!"并使用经典双括号搭配,(我相信)你总是会正确的使用它们.
编辑模式
D的编辑单元,是一个保护的模块化文件.文件的包就是一个目录.这不言而喻的精致.没有任何接口,程序源代码真正的感觉好多了,象在一个出色的数据库中.这种方法采用"数据库"发展了很长一段时间了.和版本控制,备份,操作系统级别的安全保护,日志完美的集成在一起.而且也使开发门槛降低:你需要的只是一个编辑器和编译器.目前专门支持的工具还很少,但你可以找到的如Emacs的D模式,Vim的支持,Eclipse的插件Descent,Linux的调试工具ZeroBugs,还有全功能的IDE Poseidon.
生成的代码是一个典型的二步循环编译和链接,但这种情况大大快于大多数类似的环境,有两个原因,不,是三个.
第一,语言的语法允许高度优化的词法分析和分析的步骤分开进行。
二,您可以轻松地指示编译器不产生许多对象,不象其他编译器做的一样,构造的一切存储到内存,并只有一个线性提交到磁盘
三,Walter Bright,D的创建者和最初实现者,是一个根深蒂固的优化专家
低延迟的意思就是你可以将D用作一个魔鬼的解释器(全部语法支持).
D有一个真正的模块系统,支持独立的编辑,并自动从源代码生成使用模块的摘要(即"头文件"),因此您不必担心单独维护多余文件,除非你真的想维护的话,在这种情况下,您也可以维护.是的,停止对咬文嚼字纠缠吧.
3.内存模型和多核
我们知道 D 语言能够直接调用 C 函数,看起来好像 D 语言直接构建在 C 语言内存模型上似的。如果我们毫不在乎多核——大规模并行架构所带来的处理能力,或许这还是个好消息,就这样用好了。但如今多核时代已经来临,而 C 语言的那套处理方式则已极端过时和漏洞百出。其他的过程式和面向对象语言仅仅为此做了点点的改善。再就是函数式编程语言的情况,它依赖于不变性优雅的回避了很多并行相关的问题。
作为一个相对新生的语言,D 语言处在一个相当有利的形势。当它进入多线程世界时,可以综合考量、取舍很多东东,而且 D 语言的内存模型在某一方面和很多其他语言完全不同。你可以思考一下,经典的线程是不是如下这样工作:你调用了一个原语来启动一个新线程,然后新线程能够立即看到和访问程序中的任何数据。作为一个可选项,且按照依赖操作系统的晦暗模糊来说,线程也可以获得所谓的线程私有数据来为己所用。总之呢,内存默认是被所有线程所共享的。这种方式昔日已经引起人间腥风血雨无数,直到今天仍然在继续造就新的人间地狱。当初之所以留下这么多人间恨事,主要是因为并发更新的笨拙本质:它很难被正确追踪和同步,以至于不能自始至终的良好保持数据。但是人们仍然趋之若鹜,因为共享内存的观念非常接近于底层硬件的真实模型,而且使用得当的话,效率也非常高。现在是脱离这人间地狱的时候了。今天借助于内存容量的快速增长,需要共享的机会有所减少。关于现代硬件的事实是,处理器之间通讯通过深层分级存储器制度。而这其中每个核心很大一部分保持私有。因此共享内存方式也很难再发挥作用,它快速成为程序变慢的方式之一。因为存储器物理移动的次数和距离也快速增加了。
当传统命令式语言和这些问题缠斗的时候,函数式语言从数学中汲取智慧,另辟其径:命令式傻小子们,我们根本不关心硬件模型,而只关心纯粹的数学模型。因为数学大部分不会变化,而且时间恒定,因此成为并行计算的理想候选人。(想象一下,那些第一批从数学家转程序员的家伙听到并行计算的时候,肯定会直拍自己的前额:“等一下,等一下...”译注:我对 Andrei 老大讲笑话的水平真是不敢恭维)在函数式编程圈子里都知道这样的一个计算模型天生喜爱无序、并行执行,但是它的潜能直到近来才被挖掘出来。今天,越来越清楚地显示函数式的,mutation-free的编程方式至少会成为并行程序设计的一部分。
那么 D 的定位呢。D 对于并行有个基本的必要概念:
内存默认线程私有,按需共享。
在 D 中,所有的内存都是默认线程私有。即使是那些丑陋的全局变量也会被分配给每个线程。如果你想要共享,就需要在对象前面加上 shared 修饰符,这也就是说,该对象可以立即被其他线程可见。更为重要的是,D 语言类型系统了解共享数据,且限制它能够做什么以便确保同步机制能自始至终地被正确使用。这个模型可以很好的避免那些默认线程共享编程语言所带来的诸多同步棘手问题。在那些语言中,类型系统根本无法知道哪一个数据需要共享,哪一个不需要。所以它只能信任程序员,让他们来适当的标明共享数据。但是这样,就会产生很多麻烦,各种各样的场景都需要特别规则说明,比如非共享数据,已标明的共享数据,未标明但实际是共享数据,还有混合情况。Oh,yeah...
多核支持目前是非常活跃的一个研发领域,真正实际的良好模型还没有发现。D 语言把线程私有内存模型作为基础设施,只是为以下这些基础设施做些方便之处:纯函数、lock-free 原语,旧有的基于锁机制的良好编程机制、消息队列(计划中)等等。更高级的特性,比如所有权类型也在讨论中。
4.不可改变性,安全,面向对象功能
到目前为止一切良好,但不变性和函数式编程风格的代码所发生的一切都是纯数学演示, D承认关键作用,函数式编程和不可变性构成并行程序(而不是仅仅并行,对于这个问题) ,所以它为永远都不会改变数据定义了一个immutable(关键字).与此同时D还认识到,变化往往是达到目标的最佳手段,更不用说我们许多人熟悉的风格了。 D的答案是相当有趣,因为它将可变数据和不可改变的数据包含在一个整体中了。
为什么不可变数据非常棒?因为在交叉线程中共享不可变数据永远不需要同步。不同步比同步速度快多了。诀窍在于,确保只读要真正意义上只读,否则一切保障都土崩瓦解。为支持并行编程的这一重要方面,D提供了一个无以伦比的混入功能支持非常重要的设计。为用immutable修饰的数据提供了强有力的静态保证,你必须正确输入程序不能改变数据。此外,不变性是深度的-如果你是不可变的,并声明为不可变数据,您将始终是不可变的(为什么呢?如果不那样,你认为你分开了不变的数据可以共享不变量,但最终无意中变成了可变数据共享,在这种情况下,我们又会回到复杂的规则中了,我们希望避免回到从前)。程序中整个相互关联对象的子图可以轻松的用immutable画出。类型系统知道它们,并允许线程自由共享,也可以在单线程代码中优化它们,以便更有优势的访问。
D是第一个提出默认的私有内存模型的语言?别客气。一个系统集成了默认私有线程,还有可变和不可变数据在内存中,这让D必须区别它们。诱惑是内部的更多细节,但让我们离开,为新的一天,继续概况。
极致的安全特性
作为一个系统级的编程语言,D允许极端高效,同时也有危险的构造:允许非托管的指针,手工内存管理。非常小心的设计也可能被破坏。
然而,D也有很简单的机制让一个模块“安全”,一致的编辑风格可以强制内存安全。成功编写代码在语言的的一个子集-被亲切的称为“SafeD”,不用担心软件的可移植性,你就用你听到的编程经验就行,或者你不需要单元测试。SafeD仅仅关注消除可能的内存泄漏。安全的模块(或者使用了安全编辑模式)利用了额外的语义检查,禁止在编译时所以的危险语言功能,如忘记释放的指针,或者删除堆栈变量的地址。
在SafeD中你就不会有内存泄漏。安全模块构成了应用的绝大部分,然而“系统”模块却很少(是安全模块),要在代码回审时受到不断的注意。大量的应用程序可以完全用SafeD写成。但是一些事情象内存分配您必须让您的双手油腻(译为费尽心力更好吧)。伟大的是,你不需要用不同的语言完成你的应用的几个部分。
在本文写作之时,SafeD还没有完成,还在积极的开发中。
不言自明:没有更多的斧头了
D是多模式的,这是一个需要技巧的说法,它没有心怀叵测。D已经获得备忘。任何事情不一定是一个对象,一个函数,一个列表,一个哈希表,或者牙齿仙女(译注:美国儿童都知道的典故,牙齿仙女可以有无边的法力)。这取决于你怎么做到这一点。用D编程会感觉到解放:因为你在想解决问题的时候不需要花费时间想法适应你选择的锤子(斧子)。现在,真相大白,你的责任就是:花费时间搞清楚什么样的设计最适合给出的问题。
不愿意停留在唯一的方法上,D沿袭了C++开始时的传统和优势,D为多种编程风格的转变提供了支持,更好的集成各种模式,并大大降低了下述任何语言转变的阻力。这对入门者也是很有利的。显然D更多感谢C++,并或多或少的吸收了其他语言:例如Java,Haskell,Eiffel,JavaScript,Python,和Lisp。(其实大多数语言借鉴于Lisp语言,有些只是不会承认这一点)(译注:Lisp的确很伟大,它完全由列表组成,有难以置信的强大功能和灵活性,也算是函数式编程的鼻祖,可惜我不会)
D兼收并蓄其他语言一个很好的例子就是资源管理。有些语言赌定你需要的管理的所有资源都需要垃圾收集。C++程序员知道RAII的优点,有些人说任何事都需要资源管理。每个小组都缺乏详尽的协作使用各种工具的经验,从而导致了滑稽的辩论:各方甚至不理解对方的论点。事实上,两个方法都有不足,因此D打破了一家之言。
面向对象编程
在D中你可以使用结构也可以使用类。它们同享很多优点,但却有不同的章程:结构是值类型,而类是为了动态多态性并通过参考访问。这些让人困惑,切片相关的错误,使用注释的方式“//不,不要继承!”不存在。在你设计一个类型的时候,你可以预先决定它是单态的值还是多态的参考。C++著名的允许顶一个歧义性类型,但是它们很少用,也易于出错,让人反感,足以为了简单设计而避免使用它们。
D提供的面向对象与Java和C#的相似:单一实现继承,多个接口继承。这使得Java和C#的代码非常容易迁移到D的实现中。D决定放弃语言级支持多重继承,但是伴随而来的是酸葡萄理论“多重继承是邪恶的:有什么护身符可以帮助我?”相反,D只是承认困难,使用多重继承工作更有效率,是一种有效的方式。使用多重继承的大部分好处在于控制成本,D允许类型使用多个子类型,如下:
class WidgetBase { ... } class Gadget { ... } class Widget : WidgetBase, Interface1, Interface2 { Gadget getGadget() { ... } alias getGadget this; // Widget subtypes Gadget! }
这个引入的别名和this一样工作。什么时候你在一个Widget中要获得一个Gadget时,编译器就会调用getGadget 得到它。调用完全是透明的,如果不那样,它就不是子类型了,使用它将会让人沮丧。(如果你觉得这是一个讽刺,那么可能就是)。此外,getGadget已酌情完成任务-例如它可能会返回一个子对象,这或一个全新的对象。如果需要,你仍然可以做一些拦截方法,这听起来像很多样板的编码。但在这里就是D预先的反射和代码生成的能力(见下文)。基本思想就是,D子类型可以通过alias this来实现,你甚至可以成为int的子类型,如果你觉得喜欢它。
从面向对象的经验中,D集成了已被证明好的技术,例如一个显示的重载关键字避免意外重载,signal和slots,一种技术因为版权不能提到的,因此我们称为契约编程。
5.函数式和范型编程
函数式编程
如何快速顶一个函数式风格的斐波那契函数?
uint fib(uint int n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
我承认这是愉快的设想。其中一个设想就是,最后我回去,会以某种方式消除函数的执行,没有计算机科学教授不断教它。(下一个有冒泡排序和快速排序在空间上的函数O(n log n),但是fib有很大余地胜过他们,另外,杀死希特勒或斯大林是冒险的,因为它难以评估后果,而消除fib是好的。)fib需要指数的时间来完成,因此,执行只是无知的复杂性和花计算费用。一个是逗人的马虎借口,一个象越野车的驾驶。你知道什么是坏的指数?fib(10)和fib(20)在我的机器上用的时间可以忽略不计,但是fib(50)却要执行19分半。十有八九,计算fib(1000)会比人类活的还长,这给了我安慰,因为我们是值得的,如果我们继续教授拙劣的程序。
那么什么样的斐波那契函数实现是“Green”函数?
uint fib(uint n) { uint iter(uint i, uint fib_1, uint fib_2) { return i == n ? fib_2 : iter(i + 1, fib_1 + fib_2, fib_1); } return iter(0, 1, 0); }
这个修订版可以用很少的时间执行fib(50)。这个实现现在耗费O(n)时间,尾部调用优化(D实现)抵消了空间(时间)复杂度。问题是新的版本看起来不美。修订版本必须维护两个状态变量伪装成函数参数。因此我们不妨删除并写一个直接的循环,避免iter函数带来的费解。
uint fib(uint n) { uint fib_1 = 1, fib_2 = 0; foreach (i; 0 .. n) { auto t = fib_1; fib_1 += fib_2; fib_2 = t; } return fib_2; }
待续。。