【收藏】Paul Graham:百年语言

【分享理由】在我看来,Paul Graham是一个典型的工匠,也只有工匠才会跳出编程这项工作去思考一些更加本质的问题。我为什么要学这门语言?找工作?项目中用到?可它本身是用来干什么的呢?为什么这么设计?它能实现我想做的任务吗?在各种论坛里总能找着争论哪种语言好坏的帖子,通常都不会有结果,因为大多数语言在一定的时间、空间背景下都有它存在的道理。很喜欢文中的一句话:低效的软件不是拙劣的,真正拙劣的是使程序员做不必要的工作的语言。


百年语言

Paul Graham 2003年4月

(本文出自2003年Python大会上的一篇主题讲话)

译者:Sander

很难预测人们的生活在一百年后会是什么样子,我们只能给很少的事物一个确切的预测。我们知道到那时候每个人都将驾驶气垫轿车,地方法规将对建造上百层的高楼无所制约,大部分时间都将日月无光,女人们都将精通武术(martial arts)……在这里,让我们把这幅图景的一个细节放大来看看:那时候人们用什么编程语言来写那些气垫轿车的控制软件呢?

这是一个值得思考的问题,其意义不在于我们一定要用这种语言,而是在于据此我们可以选择可能发展成那种语言的语言——如果我们够幸运的话。

我认为,语言就像物种一样,会形成进化树,没有前途的分支将枯死脱落。我们已经看到了这种事情发生:Cobol——曾几何时风光无限,现如今没有一个像样的后代,它就是在进化中被淘汰的“穴居人”语言。(译注:穴居人是石器时代的欧洲大陆的主宰,大约3万年前灭绝。穴居人又叫尼安德特人,其发声系统不发达。)

我预测Java的气数也跟这种语言差不多。有人不时发邮件跟我说:“你怎么能说Java不可能成为一种成功的语言呢?它现在已经是一种相当成功的语言了。”那么我承认这一点——如果你衡量成功的标准是关于Java的书籍(特别是个人著作)在书架上占去的空间的大小,或者是为了找工作不得不学习Java的大学生的数量的多少的话。我所说Java不可能成为一种成功的语言,意思是从物种进化的角度来看,Java将会走向穷途末路,就像Cobol一样。

这只是一个猜想,我也许会猜错。我在此的重点不是要讨论Java,而是要提出进化树的论点并引发人们来问自己:“X语言在进化树上的什么位置?”之所以提出这个问题,不仅为了避免百年后去后悔,更主要是因为跟紧语言发展的主流对于当前选择好的编程语言有积极的启发意义。

假如你生活在旧石器时代,任何时候你大概都会因为自己“处在进化树的主干上”(译注:石器时代地球上生活着智人在内的多个人种,后来其他人种都灭绝了,只有智人在竞争中生存下来,成为现代人类的祖先)而感到无比幸福,虽然还有大量的穴居人——他们也是这个世界的居民,并且克鲁马努人(译注:旧石器时代晚期在欧洲的高加索人种)不时会来袭击你,还偷走你的食物。

因此我也想知道编程语言在一百年以后会是什么样子,从而决定现在该把赌注押在哪个“树枝”上。

编程语言的进化过程又不同于物种的进化过程,因为编程语言的分支可能会汇聚。譬如Fortran这个分支,似乎正在渐渐并入到Algol的后代中。理论上讲这对于物种来说也是可能的,但是这种可能性很小,似乎从来就没有发生过。

集中化对于语言的进化更有可能,部分原因是语言进化的走向空间比较小,还有部分原因是对语言的进化来说,突变不是随机的。语言的设计者总会有意识地吸取其他语言的思想。

对于语言设计者来说,考虑一下编程语言的进化方向就特别有意义,因为他们可以据此把握好设计取向。在那种情况下,“处在进化树的主干上”就不只是选择一个好的语言了,而是从中得到启发,以对语言的设计做出正确的决策。

任何编程语言都可以分为两个部分:作为公理(axiom)的一个基本运算符(operator)集和语言的其他部分,其他部分原则上可以根据基本语素写出来。我想基本语素集是一种语言在其漫长的生存期中最重要的部分了,而其他部分可能会改变。这就好比买一幢房子,原则上你应该首先考虑房子的地理位置,其他的任何因素你都可以调整,但是你不能调整位置。

我认为好的公理的选择很重要,但是公理要尽量少,这一点同样重要。数学家们对于这一点感受应该更深刻:公理越少越好。我认为也确实如此。

最近,人们仔细核查起编程语言的核心,看看是不是有什么“公理”是可以除去的,这已经成为一项有益的实践。我发现在我长期的职业生涯里,自己经常像个笨蛋一样,用垃圾堆积着垃圾(译注:原文cruft breeds cruft,随着软件的发展,以及经历了修改bug和更新的若干周期,它的部分代码已不再使用但仍然保留在源码中。这种代码称为cruft。 cruf可能是一两行无用代码或整个源文件模块。由于很难识别cruft,去除cruft 往往很困难。)并且我发现同样的事情在软件里随时随地都在发生。

我有一个预感,软件进化树的主干会贯穿于某些编程语言中,这些语言有着最小、最干净的“核”。一种语言越能用它自己来写自己,就越好。

当然,在提出一百年后编程语言会是什么样子的问题的时候,我做了一个很大的假设。一百年后我们还写程序吗?我们不是只需要告诉计算机我们希望他们做什么就可以了吗?到现在为止,这方面还没有大的进展。我想此后的一百年里,人们还是要通过现在这样手工编写的程序来告诉计算机去做什么。或许有的问题我们现在需要写程序来解决,而一百年后这些问题不必再写程序来解决,但是我想我们还是要面对很多我们今天编程所面临的同样的问题。

谁要说他能预测某一技术在一百年后将是什么样子,我们都会觉得他在吹牛。但是不要忘记我们已经有了五十年的经验,当我们反思过去的五十年里语言的进化是多么缓慢的时候,再来展望一下一百年后的情况就是一件可以理解的事。

语言进化缓慢,是因为它们并不是技术,语言只是符号。程序只是告诉计算机你要解决的问题的形式化的描述。编程语言的进化的速度并不像搬运或传递,倒更像数学符号的进化速度——数学符号也在进化,但是如你所见,却不像它所支持的技术一样有巨大而飞快的变化。

无论一百年后计算机是什么材料做的,可以很肯定地预测它将比现在运行更快。如果摩尔定律(Moor’s Law)继续有效的话,它的速度将是现在的7,379亿亿(quintillion)(73,786,976,294,838,206,464)倍,这是难以想象的。不可否认,对于速度的预测摩尔定律很可能失效,任何事物如果每18个月就增长一倍的话,长到最后就很可能违背某些基本的极限。但是这不妨碍我们去相信计算机会比现在快得多,即使它最后只比现在快那么小小的一百万倍,也会从本质上改变编程语言的基本规则。到那时候,那些当前被认为是运行速度缓慢、不能生成高效率生成码的语言就会得到更多的空间。

固然有一些应用还将追求速度。因为我们用计算机解决的一些问题本身是由计算机引起的,比如你要处理的视频图像的速率依赖于另一台产生视频图像的速率。另外还存在一些可以无限吃掉机时的问题,例如图像描述、加密、仿真等。

如果一些应用逐渐降低对效率的要求,而另一些应用继续要求占用最新的硬件能提供的所有速度。那么更快的计算机就意味着语言必须覆盖一个更广泛的效率范围。我们已经看到了这种事的发生,一些用新近流行的语言来实现的程序如果用几十年前的标准来衡量的话,那对机时的“浪费”是惊人的。

这不只是发生在编程语言上的一个现象,而是一个普遍的历史趋势。当技术更新换代了以后,后一代都能做前一代会认为是浪费的事情。三十年前的人肯定会觉得我们随心所欲地打长途电话是件令人惊讶的事,一百年前的人们一定会更惊讶于从波士顿经过孟菲斯到达纽约的包裹一天就能送到。

我现在就可以告诉你,一百年后当我们有了更快的硬件以后那些新增的处理能力都做了些什么?它们都将被“浪费”掉!

我从计算机能力还很珍贵的时候开始学习编程。我还记得那时候从我的Basic程序中节省出所有能节省的空间以便装入一个4K大小的TRS-80,在这个无止境的重复上我花费了很大的精力,把机器的能力发挥到极限,最终还是受不了这种低效。但是现在看来,我那时拼命节约机器资源的直觉是错误的——就如同一个从小受过贫穷的煎熬的人,连去看医生这样很重要的事情也不舍得花钱。

某些浪费固然是可耻的,譬如SUVs(译注:Sport Utility Vehicle,运动型多功能轿车)就被证明是一种拙劣的产品,即使它载油量很大且不会产生污染。SUV之所以拙劣,是因为它为了解决一个拙劣的问题——怎么让一辆小型货车看上去更威猛。不过不是所有的浪费都是坏的。我们有证据来支持这一点,打长途电话的时候你不会繁琐地一分钟一分钟地数时间,如果有足够的资源,无论是打长途还是短途,你可能会觉得都是一样的。

有好的浪费,也有不好的浪费。我对好的浪费感兴趣,就是那种花更多的开销,但是能获得更简单的设计。我们如何从浪费更新、更快的硬件的机时中获取好处呢?

在这个计算机处理能力很弱的时代,对速度的渴求在我们心中早已根深蒂固,我们应该有意识地克服这种想法。在语言设计中,我们应该有意识地寻找一切机会,用执行效率来换取哪怕很小的使用便利性。

大多数数据结构都是由于速度的原因而存在。譬如,当今很多语言都既有字符串又有列表(list)。从语义上讲,字符串是列表的一个子集——其元素为字符,因此你为什么需要一种单独的数据类型呢?真的不需要。字符串的存在,只是因为效率的原因。但是这种用扰乱语言语义的手法来使程序运行更快的做法是没有说服力的。语言中包含字符串类型就是一种不成熟的优化。

如果我们把语言的核心看成是一组公理集,那么只简单地为了效率的好处而往这个公理集里面添加公理,却不能增加语言的表达力的话,这种添加就是丑陋的。效率是很重要,但是我不认为那种获取效率的方法是正确的。

我认为解决问题的正确方法是将程序的内涵与它的实现细节分离。不要既有列表又有字符串,只要有列表就够了,如果有必要,可以通过某种方式给编译器以建议,允许编译器把字符串按照相邻字节来存放。

既然速度在程序的绝大部分地方都无关紧要,那么你通常就不必为这种小事操心了,这一点随着计算机速度越来越快,将越来越正确。

人们很少注意到程序的实现也应当使程序变得更有弹性。一个程序往往在编写的过程中会发生需求变化,这是不可避免的,也是应当受到欢迎的。

“essay”(译注:企图;小品文)一词源于法语的一个动词“essayer”,意思是“尝试”,也指为了“试图”推演出某一结论而写的东西。软件也跟essay一样,作者往往并不确知哪些才是他们真正要表达的东西,我认为一些最好的程序也是essay。

Lisp(译注:一种表处理语言,用于处理包含有表格的数据的编程语言,被广泛地运用于人工智能研究)黑客(hacker,译注:黑客指掌握尖端电脑技术的人,而不是人们常说的网络入侵者,下同)们已经了解到弹性数据结构的价值,我们倾向于在程序的第一个版本中用列表(list)来处理一切数据。这种最初的版本是如此惊人地低效,因为它有意地避免去想它到底要做什么的细节,就像——至少对我来说——是吃牛排的时候有意避免去想它来自哪里一样。

一百年后程序员将需要什么语言?最可能是那种只需要最少的精力就写出 “非常低效”的“第一版程序”就搞定问题的语言。至少,我们目前可以如此描述这种语言。而用他们的话说,他们需要一种容易编程的语言。

低效的软件不是拙劣的,真正拙劣的是使程序员做不必要的工作的语言。浪费机器时间不是低效,浪费程序员的时间才是真正的低效。这个道理随着计算机越来越快,也将会越来越明白。

我想去掉字符串(string)这种数据结构已经指日可待了。我们在Arc(译注:Lisp的一种方言)中就已经是这么做的,而且成功了。一些用正则表达式描述起来相当拙劣的操作,在Arc中用递归函数就很容易描述了。

像这种扁平的数据结构还能存在多久?我审慎而全面地思考了各种可能性,结果连我都大吃一惊。譬如,我们将会放弃数组吗?毕竟,数组只是一种以整数向量为键值的哈希表(hash table),而我们会连哈希表都用列表来取代吗?

有的预测甚至比这个更骇人听闻。譬如McCarthy在1960就描述过的Lisp语言中连数字(number)都没有。逻辑上讲,你并不一定需要一个关于数字的单独的符号,因为你可以用列表来表示数字,整数n可以用一个n个元素的列表来表示,你可以用这种方式进行数学计算。只不过这样不堪其低效。

现实中没有人被建议用列表来实现数字操作,事实上McCarthy在1960年的论文在那时也根本没有指望被实现。那只是一项理论探讨,只是尝试给图灵机(Turing Machine)创造一种优雅的替代。出人意料的是,有人却根据那篇文章做出了一个可工作的Lisp解释器。不过数字不是用列表来表示,而是跟其他语言里一样,用二进制的方式来表示的。

编程语言会发展到去掉数字这种基本数据类型的程度吗?我这么问不是信口开河地制造耸人听闻的未来问题。情况就如同无坚不摧的矛遇到了无所不克的盾——在这里是无比低效的代码遇到了无比丰富的硬件资源。我认为这完全有可能,因为未来还相当长。如果某种做法可以减少语言的核心公理的数目,那么它随着时光飞逝会越来越值得“下注”。如果某种想法在一百年后依然是荒唐的,在一千年后或许未必荒唐。

必须声明,我并不是建议所有的数值计算统统用列表来实现。我只是建议语言在没有为应用而增加任何额外的符号之前的核心部分这样来定义。实际上任何需要数学计算的程序大多会用二进制来表示数字,但是这只是一个优化,并在语言的核心语义范围中。

另外应用程序和硬件之间软件的多层次性也消耗了很多计算机机时。这也是我们已经看到的发展趋势:许多软件只是被编译成字节码,运行在字节码解释器上。Bill Woods曾经告诉我,每一层解释器大概最起码要消耗10%的速度,这些额外的开销换来的是你程序的弹性。

Arc的最初版本就是这种软件分多层以获取弹性的极端的例子。那是一个建立在Common Lisp上的经典的“元循环”(metacircular)解释器,与McCarthy最初的Lisp论文中的eval函数(eval function)有一定的共同性。整个Arc只有200余行代码,所以非常易于理解和修改。而我们所用的Common Lisp,即CLisp本身又运行在一个字节码解释器上。因此这里就存在两层解释器,其中一层(上面的那一层)是惊人的低效,但是Arc仍然能用。虽然我承认它用起来很勉强,但是毕竟能用。

在应用程序方面,把软件分层是一项了不得的技术。自底向上编程意味着一层一层地写程序,每一层作为“语言”供上一层使用。这种方式有利于生成小而灵活的程序,也是取得可复用性这座“圣杯”(holy grail)的最佳途径——“语言”定义上的可复用性。在你写某种类型的应用程序的时候,越是能把你的应用下压到“语言”中,你的软件的可复用性也就越高。

不知何故可复用性的思想在1980年代被牵扯到面向对象编程中,并且看来还没有要把它解放出来的反对性的迹象。然而虽然某些面向对象软件是可复用的,但是使程序可复用的并不是面向对象,而是它的自底向上特性。想想库函数:它们可复用使因为它们是“语言”,无论它们是不是采用了面向对象的方法。

顺便说一句,我并不是预测面向对象编程的灭亡。虽然我认为它不能给好的程序员更多的帮助,但是在某些专门的领域,一些大型组织还是离不开它。面向对象编程提供了一种可行的方式去获得意大利面条式的代码,它允许你把一系列代码碎片拼合成程序。大型组织通常愿意用这种方式开发软件,我认为一百年后情况也还是如此。

在我们谈论未来的时候,最好也谈谈并行计算,因为那时候这种想法将成为现实。也就是说,并行计算看来一定会实现,无论你说那是什么时候。

未来能赶上并行计算吗?人们近20年来都在说并行计算即将实现,可是它到如今也还没有对编程产生实质影响。真的没有影响吗?芯片设计者们不得不考虑它,试图写在多CPU电脑上的系统软件的人们也不得不考虑它。

真正的问题在于,并行化会在抽象的道路上走多远?一百年后它会影响到应用软件的程序员吗?还是只是编译器编写者们才考虑它,而一般应用软件的源代码里看不见其踪影?

很有可能的倒是大多数并行化的机会都会被浪费。这是我所作的关于我们所获得大多数额外的计算能力将会被浪费的预测的一个特例。我认为,随着将来硬件处理速度的巨大提升,并行化将不会很常用,除非你确实需要它。这表明我们一百年后的并行化(除了特定的应用以外)都不会是大规模的并行化。我认为对普通程序员来说,更可能是生成一些并行运行的子进程。

这就像在程序生命的晚期你改变一个数据结构的精确实现一样,你只是在试图优化它。“第一版程序”通常会忽略并行计算带来的好书,就像忽略数据的精确表述带来的好处一样。

因此,除了某些特定的应用软件以外,一百年内并行化将不会普遍在程序里使用。如果用了,将会是一种不成熟的优化。

 

一百年后将还存在多少种编程语言?最近似乎有大量新的编程语言出现。部分原因是更快的硬件允许程序员们在速度和便利性之间根据应用做出不同的折中。如果语言的增多是一个真实的趋势,那么一百年后我们拥有的硬件只会增长这种趋势。

不过也许一百年后只剩下几种广泛使用的语言。我这样说,部分原因是我的乐观:如果你真的干得好,你可能设计出某种语言,它既适合写出 “慢而便利”的“第一版程序”,也可以在需要的时候通过给编译器一些适当的优化建议,让它产生非常快的生成码。因为我的乐观,我还可以预测不管“可接受”的效率与“最高效”的效率之间的 “鸿沟”有多宽,一百年后程序员都将拥有合适的语言跨越它们。

随着“鸿沟”的变宽,性能评测器(profiler)将会越来越重要。现在软件性能评测方面关注的太少了,许多人都还相信编写出能产生快的生成码的编译器是让程序运行更快的途径。随着“可接受性能”与“最优性能”之间的“鸿沟”的变宽,人们也越来越清楚获得更快的应用软件的正确途径是要有一个从“可接受”到“最优”的指引,就是性能评测器。

当我说未来只会剩余几种语言的时候,并不包括那些特定领域的“小语言”。我认为这些嵌入式语言是伟大的,我也希望它们能够越来越多。但是我希望它们写出来的东西能够像薄薄的外皮一样,让用户能够看到下面更通用的语言。

谁来设计未来的语言呢?过去十年里一个最振奋人心的趋势是Perl、Python、Ruby等开放源码语言兴起,黑客们接过了语言设计的任务。迄今为止结果虽然凌乱,但还算可嘉。例如,Perl里面就有一些极好的新颖的想法。虽然也有许多还很不足的地方,但是大家都在执着地努力。如果保持现在的转变速度,天知道一百年后Perl会进化成一个什么样的语言。

一般来讲可实践者也是传授者(我认识一些最优秀的黑客都是教授),但是有许多领域传授者却不是实践者,所谓“学术”一向以堂皇的职业等级欺骗视听。在任何学术领域都有一些主题是被认可的而另外一些主题却不是,不幸的是被认可的主题与不被认可的主题之间的区别往往在于它在研究论文中的描述听起来多么有难度,而不是取得一个好的结果的意义有多么重要。像文艺作品就是一个明显的例子,研究文艺作品的人很少说对作者哪怕有一丁点用处的话。

虽然在科学里情况要好一些,但是你可以做的事情很多,可以产生好的语言的事情却很少,之间的重叠少得可怜。(Olin Shivers曾经痛批过这一点。)例如,数据类型的研究一直以来都是研究论文的不竭的题材源泉,但是对于静态类型将要排除宏类型(我认为,如果没有宏,将没有哪种语言值得使用)的事却不闻不问。

看当前语言发展趋势,语言设计正在趋于被当作开源项目来开发而不是当作研究课题来做,而且开发者也正在趋于编写应用程序的程序员而不是编译器的编写者。这是一个好的趋势,我希望它继续下去。

不像物理学——几乎不可能预测它一百年后的样子,我认为原则上是可能现在设计一种语言符合一百年后的用户需要的。

可能的设计出那种语言的方式是仅凭你的意愿写下程序,不用考虑是否有编译器可以解释它,也不用考虑是否有硬件能运行它。当你写程序的时候你可以假想有无限的资源(我想我们现在应该可以想象得到一百年后有无限资源的情景)。

人们会凭意愿写出什么样的程序呢?应该是所需的最少的劳动。要彻底的最少:在你的思维还没有被你现在习惯了的编程的语言影响的“前提”下所想到的所需的最少的劳动。不过你已经习惯了的编程语言的影响是如此的普遍深刻,以致于你不得不做出巨大的努力来克服它。你可以尽量想象一下我们这些懒鬼怎么去用最少的努力表达一个程序。事实上,因为我们所有的想法是如此受到我们的思维所用的语言的限制,所以程序的简洁一点的表达都让我们非常吃惊。你需要做的是发现和觉醒,不是自然而然地陷入其中。

一个有用的诀窍是用程序的长度来作为你编程劳动的多少的近似值——当然不是按字符来计算的长度,而是按独立语法元素(基本上就是是解析树的大小)来计算的长度。要说最短的程序就标志是你最少编程劳动也不完全准确,但是它作为一个简洁性的指标是寥胜于无的。那么,语言设计的法则就变成了:看看一个程序并问一问,是否还有别的方法可以把程序写得更短?

实际上,用不可想象的百年语言写程序会根据你接近它的内核的程度不同其结果也有所不同。你可以现在就写出排序程序,但是很难预测一百年后将需要什么样的代码库(library),大概会出现许多新领域的代码库。譬如,如果到时候SETI@home(译注:Search for Extra Terrestrial Intelligence at home,是由美国加州大学伯克利分校建立的一项旨在利用连入Internet的成千上万台计算机的闲置能力搜寻地外文明的巨大试验)还有效,我们就将需要与外星人(alien)通信的代码库。当然前提是他们也足够发达,也能用XML来沟通。

极端一点,我认为你今天就有可能设计出那种核心语言。有人可能会说,实际上它已经早在1958年(译注:J.McCarthy于1958年提出了Lisp的想法,并于同年跟他的学生们一起进行最初的实现工作。)就被设计好了。

假设我们今天就可以用所谓百年语言,我们会用它来编程吗?为了回答这个问题,让我们回顾一下从前,如果我们当前所用的编程语言在1960年就可用,那时的人们会用它们吗?

从各个方面来讲,答案都是否定的。当今的语言是建立在1960年还不存在的一些基础上的。例如,像Python这种语言中每行的缩进是很有意义的规定,但是这种规定在那时候的打印终端上是行不通的。这样的问题暂且不说,试想一下,那时候的程序都是写在纸上的,1960年代的程序员们会喜欢用我们今天所用的语言来写程序么?

我认为还是会的。虽然对于那些已经把史前古老的语言根深蒂固地融入到他们对计算机程序的认识中的缺乏想象力的人来说,确实很难。(天哪!没有指针方法那怎么操纵数据?没有goto怎么实现流程图?)但是我认为最聪明的程序员们将很自然地使用我们当今所用的大多数语言——如果他们当时有这些语言的话。

如果我们现在就拥有了百年语言,起码它会产生伟大的伪代码。用它来写程序又会怎么样呢?既然百年语言需要生成快的生成码以适应某些应用,那么大概它应该也能生成足够可接受的高效的生成码来适应我们现在的硬件。只是我们或许不得不比一百年后的用户要给出更多的优化建议,但那也是划算的。

我们现在有了两个观念,如果你把他们结合起来,会产生有趣的可能性:(1)原则上讲,百年语言是可以在今天被设计出来的;(2)这种语言如果今天已经存在,那么用它来编程应当是不错的。当你看到像这样明摆着的两个观念,就不难想到:为什么不现在就试着用百年语言来编程呢?

如果你是做语言设计工作的,我认为你最好有这种目标,并且把它牢记于心。当你学驾驶的时候,他们教给你的基本原则之一就是不要只是让你的引擎盖对准道路上的行道线,而是要把目光瞄准远处。哪怕你只是在意下一个10英尺会发生的事,也应该这样。我想我们在编程语言方面可以也应该这样做。

Notes

我相信Lisp语言中的Machine Lisp是第一个将“声明(动态变量除外)只是优化建议,而不会改变正确程序的意义”这一原理具体化的语言,而Common Lisp首次将其明确地阐释了出来。

感谢Trevor Blackwell,Robert Morris 和Dan Giffin,他们阅读了本文的初稿,感谢Guido van Rossum,Jeremy Hylton和Python社团的其他全体成员,是他们邀请我在Python大会上讲话。

阅读(1008)

【收藏】Paul Graham:百年语言》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>