mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
del
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<audio id="audio" title="08 | 语言的模型:如何打破单一语言局限,让设计更好地落地?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bb/cd/bb92f2e08fc2d2d6301fd8426e36c9cd.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
经过前面几讲,我们已经学习了如何去了解一个现有软件的设计。从这一讲开始,我们就进入到新的模块,讨论如何设计一个软件。做设计之前,我们要先知道手边有哪些工具。所以在这个模块开启之初,我们先来讨论程序设计语言。
|
||||
|
||||
或许你会觉得,程序设计语言有啥好讨论的?哪个程序员没有一门看家的程序设计语言呢?不知道你是否遇到过这样的问题:
|
||||
|
||||
- 面向对象用来组织程序是好,但我用的是C语言;
|
||||
- 我用的是C++,函数式编程的好,跟我有什么关系;
|
||||
- 动态语言那些特性很好,可惜我用的是Java;
|
||||
- ……
|
||||
|
||||
如果你这么想,说明你被自己的看家本事给局限住了,这种思维方式会让你即便学到了更多的好东西,也只能无可奈何。
|
||||
|
||||
其实,程序设计语言之间没有那么泾渭分明的界限,程序员唯有多学习几门语言,才能打破语言的局限,让设计更好地落地。你可以根据项目特点选择合适的语言,也可以将其它语言一些优秀的地方借鉴过来。Andrew Hunt和David Thomas在《程序员修炼之道》(The Pragmatic Programmer)中给程序员们提了一项重要的建议:**每年至少学习一门新语言。**
|
||||
|
||||
可是,语言那么多,我要一个一个都学过去吗?学语言到底在学什么呢?
|
||||
|
||||
其实,程序设计语言本身也是一个软件,它也包含模型、接口和实现。而**我们学习程序设计语言主要是为了学习程序设计语言提供的编程模型**,比如:不同的程序组织方式,不同的控制结构等等。因为不同的编程模型会带给你不同的思考方式。
|
||||
|
||||
既然要学习编程模型,我们就要先知道编程模型设计的来龙去脉,所以,今天我先带你领略一下程序设计语言的发展历程。
|
||||
|
||||
## 程序设计语言发展简史
|
||||
|
||||
我们今天接触到的程序设计语言都是图灵完备的。这里的“图灵完备”指的是语言指定的数据操作规则能够实现图灵机的全部功能(图灵机的概念是由阿兰·图灵提出的,图灵机为计算机能够解决的问题划定了一个边界)。所以,图灵机是所有程序设计语言最底层的模型,程序设计语言都是在这个基础上生长出来的,包括众所周知的计算机基础:用0和1编码。
|
||||
|
||||
我们今天的计算机能够识别的都是0和1,但真正用0和1直接写代码的人少之又少,因为实在太麻烦了。所以,早在计算机诞生之初,就产生了**汇编语言**,它可以将那些0101的操作符变成更容易记住的ADD、MOV之类的指令。
|
||||
|
||||
相比于01串,汇编虽然进步了一些,但人们很快就发现,用汇编写程序也是非常痛苦的事情,因为只有对计算机了如指掌,才能写好汇编。更可怕的是,即便你熟练掌握了一种计算机的汇编语言,换成另外一种计算机,你也必须从头学过。
|
||||
|
||||
这时,就轮到**高级程序设计语言**登场了。
|
||||
|
||||
第一门被广泛使用的高级程序设计语言是Fortran,它为程序设计语言的发展奠定了基础。比如,一些基本控制结构出现了,数据开始拥有了类型(类型就是一种对内存数据的解释方式)。虽然这些东西在今天看来非常简单,但和那个年代使用的汇编相比,简直是一个巨大的飞跃。
|
||||
|
||||
Fortran对于计算机的发展起到了巨大的推动作用,人们也逐渐认识到高级程序设计语言对于开发效率的提高。接下来,人们开发了各种高级程序设计语言,不断地探索怎样写好程序。
|
||||
|
||||
早期程序设计语言探索的集大成者就是**C语言**,它提供了对于计算机而言最为恰当的抽象,屏蔽了计算机硬件的诸多细节。时至今日,C语言依然受众广泛。
|
||||
|
||||
随着高级程序设计语言的发展,门槛逐步降低,人们可以开发的程序规模也逐渐膨胀。这时候,**如何组织程序**成了新的挑战。有一种语言搭着C语言的便车将面向对象的程序设计风格带入了主流视野,这就是C++。很长一段时间内,C++成为了行业中最主流的选项,既兼容C语言,又提供了很好的程序组织方式。
|
||||
|
||||
虽然各种高级程序设计语言已经屏蔽了很多细节,但有一个问题始终没有得到很好的解决,也由此引发了更多的问题,这就是**内存管理**。其实,人们早就在尝试各种屏蔽内存管理的方式,但因为早期计算机硬件性能有限,所以没有任何一种方式能够成为行业主流。
|
||||
|
||||
后来,计算机硬件的能力得到了大幅度提升,这让那些在故纸堆里的技术又焕发了新的活力。这个阶段的胜利者是Java,一方面,它支持面向对象编程;另一方面,它还有垃圾回收机制——一种内存管理的方式。
|
||||
|
||||
Java的路其实也很坎坷,因为它早期在个人电脑上的尝试并不算成功。后来选择了企业级开发的赛道,才有机会展现自己的优势。因为企业级服务器本身性能优于个人电脑,对Java有更高的容忍度,它才得到了机会,不断进行自身的优化。
|
||||
|
||||
当硬件不再是程序设计语言的发展障碍之后,程序设计语言又该如何发展呢?
|
||||
|
||||
从前面的历程不难看出,程序设计语言的发展就是一个“逐步远离计算机硬件,向着待解决的问题靠近”的过程。所以,程序设计语言接下来的发展方向就是**探索怎么更好地解决问题**了。
|
||||
|
||||
前面说的这些只是程序设计语言发展的主流路径,其实还有一条不那么主流的路径也一直在发展,就是**函数式编程的程序设计语言**,这方面的代表就是LISP。
|
||||
|
||||
在这条路上,刚开始,很多人都是偏学术风格的,他们更关心的是解决方案是否优雅,也就是说,如何解决问题,如何一层层构建抽象。他们也探索更多的可能,垃圾回收机制就是从这里来的。但同样受限于当时硬件的性能,这条路上的探索在很长一段时间之内都只是一个小众游戏。
|
||||
|
||||
当硬件的性能不再成为阻碍,如何解决问题开始变得越来越重要时,函数式编程终于和程序设计语言发展的主流汇合了。促进函数式编程引起广泛重视也还有一个硬件因素:**多核**。
|
||||
|
||||
多核的出现,本身是IT行业应对CPU发展进入瓶颈期的一个解决方案,但它却打破了很多程序员只习惯于利用一个CPU写程序的传统方式。
|
||||
|
||||
为了利用多核的优势,人们探索了各种方案,今天看到的各种并发模型、异步模型等解决方案都从那时开始得到了蓬勃的发展。函数式编程在这个方面的探索就是利用自己声明式的表达方式屏蔽了硬件差异。让人们注意到函数式编程的价值的就是著名的MapReduce。
|
||||
|
||||
函数式编程的兴起,让那些在函数式编程社区的探索随之兴起,比如,声明式编程、DSL、元编程等等。一些后出现的程序设计语言开始将面向对象和函数式编程二者融合起来,比如Scala。而像Java和C++这些“老战士”则逐渐地将函数式编程的支持加入到语言之中。
|
||||
|
||||
相比于这些“正规军”,还有一股力量也逐渐从边缘走上了舞台,这就是**动态语言**,代表语言有 Perl、Python、Ruby、PHP等等。以前,人们更喜欢用“脚本语言”称呼这类程序设计语言,这个名字表明,它就是为了简单地解决一些特定的问题而出现的。所以,在人们心目中,它们显得并不那么正式。但它们简单、轻巧的特性有效地降低了入门的门槛,也赢得了一大批拥趸。
|
||||
|
||||
**语言的发展就是一个互相学习和借鉴的过程**。以前,动态语言的弱项在于不适用于规模比较大的工程,而近些年来,随着动态语言用户的增多,配套的工具也逐渐多了起来,动态语言项目的规模也逐渐增大。而在主航道的程序设计语言,也纷纷向动态语言学习,努力地简化代码编写的难度,比如,Java和C++都开始支持类型推演(Type Inference),目的就是让程序员少敲几个字符。
|
||||
|
||||
至此,我简单地带你回顾了一下程序设计语言的发展历程,梳理了程序设计语言的发展脉络。从中不难看出,如果把程序设计语言当作一个软件,它的发展历程就是一个逐渐添加新模型的过程,而其发展的结果就是如今的开发门槛越来越低,能够开发的程序规模越来越大。
|
||||
|
||||
## 一切语法都是语法糖
|
||||
|
||||
现在,你已经能更好地理解我们在前面提出的说法,**学习程序设计语言其实就是要学习语言提供的编程模型。**
|
||||
|
||||
以我学过的一些程序设计语言为例:
|
||||
|
||||
- C语言提供了对汇编指令直接的封装。
|
||||
- C++先是提供了面向对象,后来又提供了泛型编程。
|
||||
- Java把内存管理从开发者面前去掉了,后来引入的Annotation可以进行声明式编程。
|
||||
- Ruby提供了动态类型,以及由Ruby on Rails引导出的DSL风格。
|
||||
- Scala和Clojure提供了函数式编程。
|
||||
- Rust提供了新的内存管理方式,而Libra提供的Move语言则把它进一步抽象成了资源的概念。
|
||||
|
||||
既然学习新的程序设计语言是为了学习新的编程模型,反过来也可以说,**不提供新编程模型的语言是不值得刻意学习的**。如果你已经学会了一两门程序设计语言,学习一门新的语言其实并不困难,因为每种语言提供的新模型是有限的,基本的元素是类似的,无非是用了不同的关键字。
|
||||
|
||||
所以,学习新语言,只是在做增量的学习,思维负担并没有那么沉重。一旦对于程序设计语言的模型有了新的认识,你就能理解一件事:**一切语法都是语法糖**。
|
||||
|
||||
语法糖(Syntactic sugar)是英国计算机科学家彼得·兰丁发明的一个术语,指的是那些为了方便程序员使用的语法,它对语言的功能没有影响。
|
||||
|
||||
懂得了语法糖的道理,要想更好地理解程序设计语言,一种好的做法就是打开语法糖,了解一下语法是怎么实现的:
|
||||
|
||||
- 类型是一种对内存的解释方式。
|
||||
- class/struct是把有相关性的数据存放到一起的一种数据组织方式。
|
||||
- Groovy、Scala、Kotlin、Clojure等JVM上的新语言,提供了一种不同于Java的封装JVM的方式。
|
||||
- ……
|
||||
|
||||
通过前面的介绍,你也看到了,语言的发展并非一蹴而就,而是一个渐进式的发展历程。一些新的尝试总会在一些不起眼的地方冒出来,而且语言之间也在相互借鉴。
|
||||
|
||||
如果你能每年学习一门新语言,起初,你可以了解不同的编程模型。当你的积累足够多了,学习语言就是在跟踪程序设计语言的最新发展了。
|
||||
|
||||
**当你手里有了足够多的“武器”时,你就可以打开思路,运用不同的方式解决问题了**,甚至把其它语言的好东西,借鉴到自己使用的语言中。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们谈到了程序设计语言。学习不同的程序设计语言可以帮助我们更好地落地设计,也可以让我们向不同的语言借鉴优秀的方面。
|
||||
|
||||
我们简要地了解了程序设计语言的发展历史,从最开始的对机器模型的封装,到今天不断降低的开发门槛,程序设计语言的演化从未停止。我们也看到各种不同的编程风格在经历了最初各自独立的发展之后,开始慢慢融合。
|
||||
|
||||
对程序设计语言发展的了解,可以帮助我们理解一件事:**一切语法都是语法糖**。新的语法通常是在既有的结构上不断添加出来的,为的是简化代码的编写。
|
||||
|
||||
《程序员修炼之道》鼓励程序员们每年至少学习一门新语言,主要是为了让我们去学习新的编程模型,而不提供新编程模型的语言不值得刻意去学习。
|
||||
|
||||
不过,这就需要你对程序设计语言有着更深的理解。下一讲,我们来看程序设计语言的接口,看看更具体的语言演化是如何发生的。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**每年至少学习一门能够提供新编程模型的程序设计语言。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e9/ad/e9b1ccd6cdb75768cf8070fa60e2ecad.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你最近打算学习哪门新的程序设计语言呢?为什么?欢迎在留言区分享你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
135
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/09 | 语言的接口:语法和程序库,软件设计的发力点.md
Normal file
135
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/09 | 语言的接口:语法和程序库,软件设计的发力点.md
Normal file
@@ -0,0 +1,135 @@
|
||||
<audio id="audio" title="09 | 语言的接口:语法和程序库,软件设计的发力点" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3d/ba/3d91fcf5ebaef4df21e69fa99be6e3ba.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
在上一讲中,我们学习了程序设计语言模型的演变过程。学习不同的程序设计语言,实质上就是学习不同的编程模型。谈完了模型,接下来,就该说说接口了。
|
||||
|
||||
这一讲,我们就来谈谈程序设计语言的接口,一说起程序设计语言的接口,你的直观印象肯定是程序设计语言的语法,那是一个你已经很熟悉的话题了,但程序设计语言还有一个你可能都不曾留意的接口:程序库。
|
||||
|
||||
如果你已经能够完成基本功能的开发,想让自己在编程水平上再进一步,成为一个更好的程序员,你就可以从封装程序库开始练习。因为想封装出一个好的程序库所需的能力,就是软件设计所需的能力。封装程序库,可以成为你软件设计进阶的发力点。
|
||||
|
||||
## 消除重复的程序库
|
||||
|
||||
我们写程序的时候,只要规模稍微大一点,你就会发现同样的模式经常出现,差别无非是几个地方的参数不一样,这就是重复。
|
||||
|
||||
最开始的重复是指令级别的重复,程序员们会把同样的指令序列放到一起,通过传入不同的参数进行识别。你发现了吗?我说的就是函数。函数已经成了今天的主流编程方式,是几乎所有的程序设计语言都有的基础设施,人们甚至忘了它的由来。
|
||||
|
||||
写程序的一项主要日常工作就是定义各种函数。一旦你定义了大量的函数,就会发现有很多函数不仅仅在某个项目中是适用的,而且在很多项目中都是适用的。这时,作为一个“懒惰”的程序员,我们就会把这些在多个项目中使用的部分抽取出来,组成一个模块,这就是程序库的来源。
|
||||
|
||||
所以,程序库就是为了消除重复而出现的。而消除重复,也是软件设计的初衷。
|
||||
|
||||
程序库(Library)是程序员最熟悉的一项内容。学习一门新语言,首先是学习语法(Syntax),然后学习程序库(Library),之后再学习运行时(Runtime),这样,你就具备一门语言的基础了。再往后,你需要了解的就是各种惯用法(Idiom),以及如何运用到实际的工作中。
|
||||
|
||||
有一些程序库实在是太常用了,它们就会随着语言一起发布,成为标准库。比如,程序员熟知的第一个程序“Hello, world”的做法来自《C程序设计语言》,其中用到的printf就是来自C的标准库。再比如,Java程序员无人不知的JDK,里面包含了大量的程序库,一个Java程序员如果不能说出几个容器,简直就不好意思和人打招呼。
|
||||
|
||||
当然,如果在实际工作中只使用标准库,有些代码写起来还是非常麻烦的。因为标准库提供的能力通常是很基础的。这时,我们就需要利用更多的第三方程序库,它们给提供了更丰富的选项,去完善标准库做得不够的地方。
|
||||
|
||||
也就是说,它们会在标准库的基础上,再做一次封装,提供一个新的编程模型,或是新的接口,甚至修正一些标准库的bug,让开发变得更简单。只要是人气足够的语言,在这个方面做得都非常好,它们会提供大量的第三方库。
|
||||
|
||||
正是因为第三方库的兴起,**怎样管理第三方库**就成了一个问题。今天,这已经成了一个标准的问题,也有了标准的解决方案,那就是**包管理器**。很多语言都有了自己的包管理器,像Java的Maven、Node的NPM、Ruby的RubyGems等等,而像Rust这样年轻的语言,包管理器甚至变成了随语言的发行包一起发布的一项内容。
|
||||
|
||||
## 语言设计就是程序库设计
|
||||
|
||||
### new
|
||||
|
||||
虽然程序库受限于特定的程序语言,但其表达的思想却不受语言限制。我给你举个例子,在软件开发中,我们最常做的一个操作就是初始化。如果采用C这样比较早期的语言,通常的做法就是,分配一块内存,然后给这块内存赋值,如下图所示:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/65/37/659ef6ccbf186611e5ebe07618d5f237.jpeg" alt="">
|
||||
|
||||
类似的代码会反复出现,成为一个固定的模式。
|
||||
|
||||
而一些新的语言索性就将二者合二为一,成了一个固定的语法,这就是很多人熟悉的new。无论是C++程序员,还是Java程序员,对new一定不陌生。
|
||||
|
||||
当我们调用new时会发生什么呢?首先,它会申请一块内存,然后调用对应类的构造函数。类也好,构造函数也罢,这些内容都是C语言没有的,但这个初始化的模式与C语言是如出一辙的。
|
||||
|
||||
只不过,在C语言里面,这个操作通常是通过程序库实现的,而到了C++和Java中,它成了语法。一旦变成了语法,它就成了语言的一部分,成为了一个特定的模型。
|
||||
|
||||
对于使用者而言,这个模型就是一个接口,只要接口的行为不变,我的代码就不用变。
|
||||
|
||||
但对于接口另一面的实现者而言,它就可以做一些特定的工作了。比如,插入不同的内存分配算法,这就是C++的allocator所做的事情;再比如,把内存完全管控起来,这就是Java做的事情。没错,Java之所以能够让程序员忽略内存管理,new功不可没。
|
||||
|
||||
一个经过验证的模式最终变成了语言的一部分,而它的起点只是一个常见的用法:一个程序库。
|
||||
|
||||
### synchronized
|
||||
|
||||
我再用Java中的synchronized给你举个例子。我们都知道,并发编程是程序员的一门必修课。学习并发编程,一方面要学习各种概念,比如“锁”;另一方面还要学习不同语言相应的程序库。因为这些概念太常用了。所以,Java干脆把它变成了一个语法,也就是synchronized。
|
||||
|
||||
成为语法固然是一个巨大的进步,但是在一些场景下,语法反而会显得僵化。这时候,又轮到程序库登场了。我还以前面所说的new和synchronized为例给你讲解一下。
|
||||
|
||||
## 程序库设计就是语言设计
|
||||
|
||||
### new
|
||||
|
||||
new虽然帮我们解决了一些问题,但与new配合使用的构造函数往往有一个致命伤,那就是它只有一个名字,也就是类的名字。
|
||||
|
||||
当我们需要表达多种不同的构造逻辑时,各村就出现了各村的高招。我曾经见过有人利用重载(overload)来解决问题的,不同的构造逻辑用不同类型的参数。比如,一个用HashMap,一个用TreeMap。作为一个新加入项目的程序员,你很难想到这是两种不同的构造逻辑,它们与这里不同类型的数据结构其实一点关系都没有。
|
||||
|
||||
一个更好的解决方案是利用**工厂模式**解决这个问题,也就是说,用一个名字更能表意的函数,代替构造函数作为构造入口。
|
||||
|
||||
还是以Java为例,ArrayList是Java程序员很熟悉的一个数据结构。如果我要创建一个包含两个元素的ArrayList,同时,还要创建一个初始容量为10的ArrayList。用JDK原生的做法,我可以这么做:
|
||||
|
||||
```
|
||||
// 创建有两个元素的数组
|
||||
ArrayList<String> listWithElements = new ArrayList();
|
||||
listWithElements.add("foo");
|
||||
listWithElements.add("bar");
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
// 创建一个初始容量为10的数组。
|
||||
ArrayList<String> listWithCapacity = new ArrayList(10);
|
||||
|
||||
```
|
||||
|
||||
这种做法可行,但代码表意并不清晰。那有没有更好的做法呢?Google的Guava程序库,对于同样的场景,给出了一个不同的做法:
|
||||
|
||||
```
|
||||
ArrayList<String> listWithElements = newArrayList("foo", "bar");
|
||||
ArrayList<String> listWithCapacity = newArrayListWithCapacity(10);
|
||||
|
||||
```
|
||||
|
||||
显然,从语义上来说,这种做法更清晰。Java领域的行业名著《Effective Java》(第三版)的第一个条款是“用静态工厂方法代替构造函数”,讨论的就是这种做法。
|
||||
|
||||
### synchronized
|
||||
|
||||
再来看synchronized。它虽然解决了一部分并发的问题,但是,这个解决问题的方式粒度太大了。程序员只要稍微深入一些,就会感到synchronized的掣肘。于是,Java的开发者在Java 5中开始了新一轮的编程模型探索,这次探索的成果就是后来的并发编程库。这也为面试官们提供了新的素材。
|
||||
|
||||
你看到了吧,解决同一个问题,它可以用一个语法,也可以用一个程序库,二者之间是等价的。
|
||||
|
||||
Andrew Koenig和Barbara Moo写过一本书《C++沉思录》,书里记录了C++早期开发者在设计各种C++特性时的思考,这是一本编程思想之作。当年读这本书时,有两章的标题让我陷入了沉思,分别是**“语言设计就是程序库设计”和“程序库设计就是语言设计”**。
|
||||
|
||||
通过今天的学习,这两句话对你来说就不难理解了。因为语法和程序库是在解决同一个问题,二者之间是相互促进的关系。通常是先有程序库,再有语法;如果语法不够好,新的程序库就会出现,新一轮的编程模型就开始孵化。
|
||||
|
||||
一切有生命力的语言都会不断改善自己的语法,某些好用的程序库就可以转正成为语法。比如,Java引入Lambda,支持函数式编程;C++引入类型推演,简化了代码编写。
|
||||
|
||||
同样地,程序库的发展也在推动着语言的不断进步,有一部分语法就是为了让程序库表现得更好而存在的。比如说:
|
||||
|
||||
- C里面的宏,虽然很多人用它来定义常量,但只有编写程序库才能让它更好地发挥出自身的价值;
|
||||
- Java中的Annotation,很多人都在用,但用它做过设计的人却很少,因为它的使用场景是以程序库居多;
|
||||
- Scala中的隐式转换,如果你没有设计过DSL,很可能根本就不知道它有什么具体的作用。
|
||||
|
||||
至此,你已经能够理解程序设计语言的接口不只包含语法,还有程序库。而且,学习一种程序设计语言提供的模型时,不仅仅要看语法本身有什么,还要了解有语言特性的一些程序库。
|
||||
|
||||
所以,对于程序员而言,想要自己的编程水平上一个台阶,学习编写程序库是一个很好的路径。一方面,我们可以锻炼自己从日常工作中寻找重复;另一方面,我们可以更好地理解程序设计语言提供的能力。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们的学习主题是程序库,程序库最初只是为了消除重复。后来,逐渐有了标准库,然后有了大量的第三方库,进而发展出包管理器。
|
||||
|
||||
如果通用性足够好,一些经过大量实践验证过的程序库甚至会变成语言的语法,而一些语法解决得不够好的地方,又会出现新的程序库去探索新的解决方案。所以,**语言设计就是程序库设计,程序库设计就是语言设计**。二者相互促进,不断发展。
|
||||
|
||||
当你开始学习如何编写程序库,你对软件设计的理解就会踏上一个新的台阶。
|
||||
|
||||
我们说过,学习不同程序设计语言一个重要的原因是为了相互借鉴。理解了模型和接口,你就知道该借鉴什么,但具体如何借鉴呢?我们还需要了解这些模型是如何实现的,所以下一讲,我们就来谈谈程序设计语言的实现。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**提升软件设计能力,可以从编写程序库入手。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/d0/910d852b08bc45f94acb879cf3080cd0.jpeg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
今天我们讲到了程序库和语法之间的互相促进,最后,我想请你分享一下,你还能找出哪些语法和程序库互相促进的例子呢?欢迎在留言区分享你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
89
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/10 | 语言的实现:运行时,软件设计的地基.md
Normal file
89
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/10 | 语言的实现:运行时,软件设计的地基.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<audio id="audio" title="10 | 语言的实现:运行时,软件设计的地基" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/39/3d/395ce527f5e6b8eff09c195adc9da53d.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
通过前两讲的学习,相信你已经对程序设计语言有了全新的认识。我们知道了,在学习不同的程序设计语言时,可以相互借鉴优秀的设计。但是要借鉴,除了模型和接口,还应该有实现。所以,这一讲,我们就来谈谈程序设计语言的实现。
|
||||
|
||||
程序设计语言的实现就是支撑程序运行的部分:运行时,也有人称之为运行时系统,或运行时环境,它主要是为了实现程序设计语言的执行模型。
|
||||
|
||||
相比于语法和程序库,我们在学习语言的过程中,对运行时的关注较少。因为不理解语言的实现依然不影响我们写程序,那我们为什么还要学习运行时呢?
|
||||
|
||||
因为**运行时,是我们做软件设计的地基。**你可能会问,软件设计的地基不应该是程序设计语言本身吗?并不是,一些比较基础的设计,仅仅了解到语言这个层面是不够的。
|
||||
|
||||
我用个例子来进行说明,我曾经参与过一个开源项目:在JVM上运行Ruby。这种行为肯定不是 Java语言支持的,为了让Ruby能够运行在JVM上,我们将Ruby的代码编译成了Java的字节码,而字节码就属于运行时的一部分。
|
||||
|
||||
你看,**做设计真正的地基,并不是程序设计语言,而是运行时,有了对于运行时的理解,我们甚至可以做出语言本身不支持的设计**。而且理解了运行时,我们可以成为一个更好的程序员,真正做到对自己编写的代码了如指掌。
|
||||
|
||||
不过,运行时的知识很长一段时间内都不是显学,我初学编程时,这方面的资料并不多。不过,近些年来,这方面明显得到了改善,各种程序设计语言运行时的资料开始多了起来。尤其在Java社区,JVM相关的知识已经成为很多程序员面试的重要组成部分。没错,JVM就是一种运行时。
|
||||
|
||||
接下来,我们就以JVM为例,谈谈怎样了解运行时。
|
||||
|
||||
## 程序如何运行
|
||||
|
||||
首先,我们要澄清一点,对于大部分普通程序员来说,学习运行时并不是为了成为运行时的开发者,我们只是为了更好地理解自己写的程序是如何运行的。
|
||||
|
||||
运行时的相关知识很多,而**“程序如何运行”**本身就是一条主线,将各种知识贯穿起来。程序能够运行,前提条件是,它是一个可执行的文件,我们就从这里开始。
|
||||
|
||||
一般来说,可执行的程序都是有一个可执行文件的结构,对应到JVM上,就是类文件的结构。然后,可执行程序想要执行,需要有一个加载器将它加载到内存里,这就是JVM类加载器的工作。
|
||||
|
||||
加载是一个过程,那么加载的结果是什么呢?就是按照程序运行的需求,将加载过来的程序放到对应的位置上,这就需要了解JVM的内存布局,比如,程序动态申请的内存都在堆上,为了支持方法调用要有栈,还要有区域存放我们加载过来的程序,诸如方法区等等。
|
||||
|
||||
到这里,程序完成了加载,做好了运行的准备,但这只是静态的内容。接下来,你就需要了解程序在运行过程中的表现。一般来说,执行过程就是设置好程序计数器(Program Counter,PC),然后开始按照指令一条一条地开始执行。所以,重点是要知道这些指令到底做了什么。
|
||||
|
||||
在Java中,我们都知道,程序会编译成字节码,对于Java来说,字节码就相当于汇编,其中的指令就是Java程序执行的基础。所以,突破口就在于**了解指令是如何执行的**。
|
||||
|
||||
其实,大部分JVM指令理解起来都很简单,尤其在你了解内存布局之后。比如,加法指令就是在栈上取出两个数,相加之后,再放回栈里面。
|
||||
|
||||
我要提一个看上去很简单的指令,它是一根拴着牛的绳子,这就是new,没错,就是创建对象的指令。那头牛就是内存管理机制,也就是很多人熟悉的GC,这是一个很大的主题,如果展开来看的话,也是一个庞杂的知识体系。
|
||||
|
||||
有了对指令的理解,就对Java程序执行有了基本的理解。剩下的就可以根据自己的需要,打开一些被语法和程序库隐藏起来的细节。比如,synchronized是怎样实现的,顺着这条线,我们可以走到内存模型(Java Memory Model,JMM)。
|
||||
|
||||
当然,这里的内容并不是为了与你详细讨论JVM的实现,无论是哪个知识点真正展开后,实际上都还会有很多的细节。
|
||||
|
||||
这里只是以JVM为例进行讲解,学习其他语言的运行时也是类似的,带着“程序如何运行”这个问题去理解就好了。只不过,每种语言的执行模型是不同的,需要了解的内容也是有所差异的。比如,理解C的运行时,你需要知道更多计算机硬件本身的特性,而理解一些动态语言的运行时,则需要我们对语法树的结构有一定认识。
|
||||
|
||||
有了对运行时的理解,我们就可以把一些好的语言模型借鉴到自己的语言中,比如,使用C语言编程时,我们可以实现多态,做法就是自己实现一个虚拟表,这就是面向对象语言实现多态的一种方案。
|
||||
|
||||
**运行时的编程接口**
|
||||
|
||||
我们前面说过,做软件设计的地基是运行时,那怎样把我们的设计构建在运行时之上呢?这就要依赖于运行时给我们提供的接口了。所以,我们学习运行时,除了要理解运行时本身的机制之外,还要掌握运行时提供的编程接口。
|
||||
|
||||
在Java中,最简单的运行时接口就是运行时类型识别的能力,也就是很多人熟悉的getClass。通过这个接口,我们可以获取到类的信息,一些程序库的实现就会利用类自身声明的信息。比如,之前说过,有些程序库会利用Annotation进行声明式编程,这样的程序库往往会在运行的过程中,以getClass为入口进行一系列操作将Annotation取出来,然后做相应的处理。
|
||||
|
||||
当然,这样的接口还有很多,一部分是以标准库的方式提供的,比如,动态代理。通过阅读JDK的文档,我们很容易学会怎么去运用这些能力。还有一部分接口是以规范的方式提供的,需要你对JVM有着更好的理解才能运用自如,比如,字节码。
|
||||
|
||||
前面我们说了,通过了解指令的执行方式,可以帮助我们更好地理解运行时的机制。有了这些理解,再来看字节码,理解的门槛就大幅度地降低了。
|
||||
|
||||
如果站在字节码的角度思考问题,我们甚至可以创造出一些Java语言层面没有提供的能力,比如,有的程序库给Java语言扩展AOP(Aspect-oriented programming,面向切面编程)的能力。这样一来,你写程序的极限就不再是语言本身了,而是变成了字节码的能力极限。
|
||||
|
||||
给你举个例子,比如,Java 7发布的时候,字节码定义了InvokeDynamic这个新指令,当时语言层面上并没有提供任何的语法。如果你需要,就可以自己编写字节码使用这个新指令,像JRuby、Jython、Groovy 等一些基于JVM的语言,开发者就可以利用这个指令改善自己的运行时实现。当然InvokeDynamic的诞生,本身就是为了在JVM上更好地支持动态语言。
|
||||
|
||||
好消息是,操控字节码这件事的门槛也在逐渐降低。最开始,操作字节码是一件非常神秘的事情,在许多程序员看来,那是只有 SUN 工程师才能做的事情(那时候,Java还属于 SUN)。
|
||||
|
||||
后来,出现了一个叫[ASM](https://asm.ow2.io/)的程序库,把字节码拉入了凡间,越来越多的程序员开始拥有操作字节码的能力。不过,使用ASM,依然要对类文件的结构有所理解,用起来还是比较麻烦。后来又出现了各种基于ASM的改进,现在我个人用得比较多的是[ByteBuddy](https://bytebuddy.net/)。
|
||||
|
||||
有了对于字节码的了解,在Java这种静态的语言上,就可以做出动态语言的一些效果。比如,Java语言的一些Mock框架,为什么可以只声明接口就能够执行,因为背后常常是动态生成了一个类。
|
||||
|
||||
一些动态语言为了支持自己的动态特性,也提供了一些运行时的接口给开发者。比如,Ruby里面很著名的method_missing,很多框架用它实现了一些效果,即便你未定义方法也能够执行的。你也许想到了,我们提到过的Ruby on Rails中各种find_by方法就可以用它来实现。
|
||||
|
||||
method_missing其实就是一个回调方法,当运行时在进行方法查找时,如果找不到对应方法时就调用语言层面的这个方法。所以,你看出来了,这就是运行时和语言互相配合的产物。如果你能够对于方法查找的机制有更具体的了解,使用起来就可以更加地得心应手,就能实现出一些非常好的设计。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们讨论了程序设计语言的实现:运行时。**对于运行时的理解,我们甚至可以做出语言本身不支持的设计。所以,做设计真正的地基,并不是程序设计语言,而是运行时。**
|
||||
|
||||
理解运行时,可以将**“程序如何运行”**作为主线,将相关的知识贯穿起来。我们从了解可执行文件的结构开始,然后了解程序加载机制,知道加载器的运作和内存布局。然后,程序开始运行,我们要知道程序的运行机制,了解字节码,形成一个整体认识。最后,还可以根据需要展开各种细节,做深入的了解。
|
||||
|
||||
有一些语言的运行时还提供了一些语言层面的编程接口,程序员们可以与运行时进行交互,甚至拥有超过语言本身的能力。这些接口有的是以程序库的方式提供,有的则是以规范的方式提供。如果你是一个程序库的开发者,这些接口可以帮助你写出更优雅的程序。
|
||||
|
||||
关于程序设计语言的介绍,我用了三讲分别从模型、接口和实现等不同的角度给你做了介绍。目的无非就是一个,想做好设计,不仅仅要有设计的理念,更要有实际落地的方式。下一讲,我们来讲一个你可以在项目中自己设计的语言:DSL。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**把运行时当作自己设计的地基,不受限于语言。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/d9/0ac9b789f401a370ff91b4bd495417d9.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你知道哪些程序库的哪些特性是利用运行时交互的接口实现的?欢迎在留言区分享你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
121
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/11 | DSL:你也可以设计一门自己的语言.md
Normal file
121
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/11 | DSL:你也可以设计一门自己的语言.md
Normal file
@@ -0,0 +1,121 @@
|
||||
<audio id="audio" title="11 | DSL:你也可以设计一门自己的语言" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/d0/ed9d00486f582a72c093adf55b37d7d0.mp3"></audio>
|
||||
|
||||
你好!我是郑晔。
|
||||
|
||||
在前面,我们花了三讲的篇幅探讨程序设计语言,一方面是为了增进我们对程序设计语言的理解,另一方面,也希望从中学习到软件设计方面做得好的地方。除了借鉴一些语言特性之外,我们还能怎样应用程序语言,来帮我们做设计呢?
|
||||
|
||||
讲到程序设计语言模型时,我说过,程序设计语言的发展趋势,就是离计算机本身越来越远,而离要解决的问题越来越近。但通用程序设计语言无论怎样逼近要解决的问题,它都不可能走得离问题特别近,因为通用程序设计语言不可能知道具体的问题是什么。
|
||||
|
||||
这给具体的问题留下了一个空间,**如果我们能把设计做到极致,它就能成为一门语言**,填补这个空间。注意,我这里用的并不是比喻,而是真的成为一门语言,一门解决一个特定问题的语言。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/fd/c6/fd861a726b8a85d1d22ca56b78515cc6.jpg" alt="">
|
||||
|
||||
这种语言就是领域特定语言(Domain Specific Language,简称 DSL),它是一种用于某个特定领域的程序设计语言。这种特定于某个领域是相对于通用语言而言的,通用语言可以横跨各个领域,我们熟悉的大多数程序设计语言都是通用语言。
|
||||
|
||||
我在[第8讲](https://time.geekbang.org/column/article/245868)说过,它们都是图灵完备的,但DSL不必做到图灵完备,它只要做到满足特定领域的业务需求,就足以缩短问题和解决方案之间的距离,降低理解的门槛。
|
||||
|
||||
虽然大多数程序员并不会真正地实现一个通用程序设计语言,但实现一个DSL,我们还是有机会的。这一讲我们就来谈谈DSL,看看我们可以怎样设计自己的语言。
|
||||
|
||||
## 领域特定语言
|
||||
|
||||
不过,一说起设计一门语言,很多人直觉上会有畏惧心理。但实际上,你可能已经在各种场合接触过一些不同的DSL了。程序员最熟悉的一种DSL就是正则表达式了,没错,也许已经习惯使用正则表达式的你都不知道,但它确实就是一种DSL,一种用于文本处理这个特定领域的DSL。
|
||||
|
||||
如果你觉得正则表达式有点复杂,还有一种更简单的DSL,就是配置文件。你可能真的不把配置文件当作一种DSL,但它确实是在实现某个特定领域的需求,而且可以根据你的需求对软件的行为进行定制。
|
||||
|
||||
一个典型的例子是Ngnix。无论你是用它单独做Web服务器也好,做反向代理也罢,抑或是做负载均衡,只要通过Ngnix的配置文件,你都能实现。配合OpenResty,你甚至可以完成一些业务功能。
|
||||
|
||||
这么一说,你是不是觉得DSL的门槛不像听上去那么高了。
|
||||
|
||||
经过前面几讲的学习,你应该知道了,语法只是一种接口。很多人说到设计DSL,脑子里实际想的也只是设计一种语法。所以,从软件设计的角度看,DSL最终呈现出来的语法只是一种接口,但最重要的是它包裹的模型。
|
||||
|
||||
Martin Fowler在他的《领域特定语言》这本书中,将这个模型称为语义模型(Semantic Model)。不过,在我看来,Martin Fowler起这个名字是站在语言开发的角度,毕竟语义这个词,只有学过编译原理的人才好理解。所以,这里真正的重点是模型。
|
||||
|
||||
想要实现一个DSL,可以这么说,DSL的语法本身都是次要的,模型才是第一位的。当你有了模型之后,所谓的构建DSL,就相当于设计一个接口,将模型的能力暴露出来。
|
||||
|
||||
当把DSL理解成接口,我们接受DSL的心理负担就小了很多。你可以想一想,它和你熟悉的REST API其实没有什么本质的不同。
|
||||
|
||||
既然是接口,形式就可以有很多种,我们经常能接触到的DSL主要有两种:外部DSL和内部 DSL。Martin Fowler在他的书中还提到了语言工作台(Language Workbench),不过,这种做法在实际工作中用到的不多,我们暂且忽略。
|
||||
|
||||
外部DSL和内部DSL的区别就在于,DSL采用的是不是宿主语言(Host Language)。你可以这么理解,假设你的模型主要是用Java写的,如果DSL用的就是Java语言,它就是内部DSL,如果DSL用的不是Java,比如,你自己设计了一种语法,那它就是外部DSL。
|
||||
|
||||
把概念说清楚了,一些问题便迎刃而解了。这也可以解释为什么DSL让有些人畏惧了,原因就是说起 DSL,这些人想到的就是自己设计语法的外部的 DSL。其实,即便是外部DSL,也不一定要设计一门语法,我们甚至可以借助已有的语法来完成。比如,很多程序员熟悉的一种语法:XML。
|
||||
|
||||
如果你是一个Java程序员,XML就再熟悉不过了。从Ant到Maven,从Servlet到Spring,曾经的XML几乎是无处不在的。如果你有兴趣,可以去找一些使用Ant做构建工具的项目,项目规模稍微大一点,其XML配置文件的复杂程度就不亚于普通的源代码。
|
||||
|
||||
因为它本质上就是一种用于构建领域的DSL,只不过,它的语法是XML而已。正是因为这种DSL越来越复杂,后来,一种新的趋势渐渐兴起,就是用全功能语言(也就是真正的程序设计语言)做DSL,这是后来像Gradle这种构建工具逐渐流行的原因,它们只是用内部DSL替换了外部DSL。
|
||||
|
||||
从复杂度而言,自己设计一种外部DSL语法,大于利用一种现有语法做外部DSL,二者之间的差别在于谁来开发解析器。而外部DSL的复杂度要大于内部DSL,因为内部DSL连解析的过程都省略了。从实用性的角度,更好地挖掘内部DSL的潜力对我们的实际工作助益更多。
|
||||
|
||||
## 代码的表达性
|
||||
|
||||
你或许会有一个疑问,内部DSL听上去就是一个程序库啊!你这个理解是没错的。我们前面说过,语言设计就是程序库设计,程序库设计就是语言设计。当一个程序库只能用在某个特定领域时,它就是一个内部DSL,这个内部DSL的语法就是这个程序库的用法。
|
||||
|
||||
我先用一个例子让你感受一下内部DSL,它来自Martin Fowler的《领域特定语言》。我们要创建一个Computer的实例,如果用普通风格的代码写出来,应该是这个样子:
|
||||
|
||||
```
|
||||
Processor p = new Processor(2, 2500, Processor.Type.i386); Disk d1 = new Disk(150, Disk.UNKNOWN_SPEED, null);
|
||||
Disk d2 = new Disk(75, 7200, Disk.Interface.SATA);
|
||||
return new Computer(p, d1, d2);
|
||||
|
||||
```
|
||||
|
||||
而用内部 DSL 写出来,则是这种风格:
|
||||
|
||||
```
|
||||
computer()
|
||||
.processor()
|
||||
.cores(2)
|
||||
.speed(2500)
|
||||
.i386()
|
||||
.disk()
|
||||
.size(150)
|
||||
.disk()
|
||||
.size(75)
|
||||
.speed(7200)
|
||||
.sata()
|
||||
.end();
|
||||
|
||||
```
|
||||
|
||||
如果这是一段普通的Java代码,我们看到一连串的方法调用,一定会说,这段代码糟糕至极!但在这个场景下,和前面的代码相比,这段代码省去了好多变量,反而是清晰了。这其中的差别在哪里呢?
|
||||
|
||||
之所以我们会觉得这种一连串的方法调用可以接受,一个重要的原因是,这段代码并不是在做动作,而是在进行声明。做动作是在说明怎么做(How),而声明的代码则是在说做什么(What)。
|
||||
|
||||
二者的抽象级别是不同的,“怎么做”是一种实现,而“做什么”则体现着意图。**将意图与实现分离开来**,是内部DSL与普通的程序代码一个重要的区别,同样,这也是一个好设计的考虑因素。
|
||||
|
||||
Martin Fowler在讨论DSL定义时,提到了DSL的4个关键元素:
|
||||
|
||||
- 计算机程序设计语言(Computer programming language);
|
||||
- 语言性(Language nature);
|
||||
- 受限的表达性(Limited expressiveness);
|
||||
- 针对领域(Domain focus)。
|
||||
|
||||
其中,语言性强调的就是DSL要有连贯的表达能力。也就是说,你设计自己的DSL时,重点是要体现出意图。抛开是否要实现一个DSL不说,的确,**程序员在写代码时应该关注代码的表达能力**,而这也恰恰是很多程序员忽略的,同时也是优秀程序员与普通程序员拉开差距的地方。
|
||||
|
||||
普通程序员的关注点只在于功能如何实现,而优秀的程序员会懂得将不同层次的代码分离开来,将意图和实现分离开来,而实现可以替换。
|
||||
|
||||
说到这里,你就不难理解学习内部DSL的价值了,退一步说,你不一定真的要自己设计一个内部DSL,但学会将意图与实现分离开,这件事对日常写代码也是有极大价值的。
|
||||
|
||||
有了这个意识,你就可以很好地理解程序设计语言的一个重要发展趋势:声明式编程。现在一些程序设计语言的语法就是为了方便进行声明式编程,典型的例子就是Java的Annotation。正是它的出现,Spring原来基于XML的外部DSL就逐步转向了今天常用的内部DSL了,也就是很多人熟悉的Java Config。
|
||||
|
||||
你会发现,虽然我在这说的是写代码,但分离意图和实现其实也是一个重要的设计原则,是的,**想写好代码,一定要懂得设计**。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天,我们讨论了领域特定语言,这是针对某个特定领域的程序设计语言。DSL在软件开发领域中得到了广泛的应用。要实现一个DSL,首先要构建好模型。
|
||||
|
||||
常见的DSL主要是外部 DSL和内部DSL。二者的主要区别在于,DSL采用的是不是宿主语言。相对于外部 DSL,内部DSL的开发成本更低,与我们的日常工作结合得更加紧密。
|
||||
|
||||
内部DSL体现更多的是表达能力,相对于传统的代码编写方法而言,这种做法很好地将作者的意图体现了出来。即便我们不去设计一个内部DSL,这种写代码的方式也会对我们代码质量的提高大有助益。
|
||||
|
||||
关于语言,已经讲了四讲,我们先告一段落。下一讲,我们要来讨论编程范式,也就是做设计的时候,我们可以利用的元素有哪些。
|
||||
|
||||
如果今天的内容你只能记住一件事,那请记住:**好的设计要迈向DSL,我们可以从编写有表达性的代码起步**。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/75/33/7516280b19aacd35a2235e54d14b0833.jpg" alt="">
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你还能举出哪些DSL的例子呢?欢迎在留言区分享你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
119
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/加餐 | 再八卦几门语言!.md
Normal file
119
极客时间专栏/geek/软件设计之美/设计一个软件—程序设计语言/加餐 | 再八卦几门语言!.md
Normal file
@@ -0,0 +1,119 @@
|
||||
<audio id="audio" title="加餐 | 再八卦几门语言!" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/62/59/62f575fe54ca640e584888569358c959.mp3"></audio>
|
||||
|
||||
你好,我是郑晔!
|
||||
|
||||
软件设计是一个比较烧脑的话题,对于一些同学来说,学起来还是有一些辛苦的。所以,我准备了这次加餐,让大家在前面高密度地狂奔了一段时间之后,稍微休息一下。
|
||||
|
||||
我在第8讲中讲了程序设计语言的发展,有同学觉得不过瘾,想了解其他语言的发展过程。那好,我们就来谈谈几门比较吸引眼球的程序设计语言。
|
||||
|
||||
## C#
|
||||
|
||||
当年Java开始起势的时候,微软还处于自己的巅峰,它当然不想错过Java这么有前景的东西。但是,微软从来就不会老老实实按照标准做事,所以,你会看到微软手中的Basic已经很不像Basic了,微软的C++也有着自己的扩展。
|
||||
|
||||
于是,微软也想做出一个自己的Java,J++就出现了。但是,这不是一个正常的Java,引发了SUN的不满,将微软告上法庭。最终,双方庭外和解,微软不再祸害Java,J++停止更新。
|
||||
|
||||
但有一点不得不承认,微软在Windows上的JVM性能是当时最好的,因为操刀J++的是Anders Hejlsberg,他是全世界最顶级的程序员。微软为了不与Java开启的受控(Managed)代码浪潮擦肩而过,于是,转身又推出了C#和.NET。
|
||||
|
||||
C#的初版本简直和Java一模一样,一个Java程序员几乎不用培训就可以成为一个C#程序员。所以,从语言的角度来说,最初的C#并没有对行业做出什么贡献。
|
||||
|
||||
不过,既然有Anders Hejlsberg在背后,事情当然不会就这么简单收尾。C#在语言特性上开始一路狂奔,一个更强大的C#崭露头角。像Lambda、类型推演这些特性早早就落户C#了。
|
||||
|
||||
然而,C#时运不济,它的上升期遇到了微软的下降期。越来越多的公司选择了Java,越来越多的程序员拥抱了Java,而语言模型上表现优秀的C#则遭遇了冷落。
|
||||
|
||||
当年Java号称“一个语言,多个平台”,而.NET则是“一个平台,多个语言”。结果呢,.NET的“一个平台”并不足以吸引更多的公司和程序员的投入,除了微软自己,其他在上面开发语言的尝试通常都是浅尝辄止。而JVM虽然目标不是为了多语言,但丝毫不妨碍很多人在上面开发新语言,比如,Groovy、Scala、Clojure等等。
|
||||
|
||||
最终,**JVM成了“多个语言,多个平台”**。随着微软的逐步开放,.NET也开始迈向了多平台,**C#也成了一门跨平台的语言**,遗憾的是,为时已晚,Java 已经成就了一番霸业。
|
||||
|
||||
如果出于学习的目的,C#绝对是值得一学的程序设计语言,毕竟微软在语言设计上还是很有一套的。Java语言的进化是非常缓慢的,尤其是SUN的衰退又耽误了很多年。所以,从语言特性上来看,说C#领先Java十年并不夸张。
|
||||
|
||||
## JavaScript
|
||||
|
||||
JavaScript从诞生之日起就扮演着一个不受待见的角色,Brendan Eich发明JavaScript完全是为了应付工作,因为他当时供职的Netscape需要让网页上的元素动起来。
|
||||
|
||||
雷锋和雷锋塔有什么关系?Java和JavaScript有什么关系?这是一个经常被人提起的段子,但实际上,JavaScript和Java真的有关系,关系就是蹭热度。当时的Java给世界描绘了一个美好的未来,让无数人心潮澎湃,JavaScript就想借一下Java的东风。
|
||||
|
||||
JavaScript仅仅用10天就设计出来,所以,在它的实现中,包含了各种奇怪的问题。不过,它还是体现出了BrendanEich的功底,比如,JavaScript提供了对各种编程范式的支持。其实,他真正想做的是一门函数式编程的语言,但向现实妥协的结果就是,借助了C风格的语法,函数式编程的底子却留在了JavaScript里。
|
||||
|
||||
虽然在今天看来,在浏览器上JavaScript一枝独秀,但当年它也是有竞争对手的,那个年代无处不在的微软,出手做了一个VBScript。但是,如同微软错过了互联网时代一样,与Windows结合更加紧密的VBScript也在这场竞争中败下阵来。
|
||||
|
||||
当年,与JavaScript联系在一起的,更多的是像走马灯之类的页面特效。让JavaScript真正第一次得到重视是Ajax这门技术。Ajax的出现,让页面的元素可以与远程的服务器进行交互,JavaScript开始由一个小玩具变成了一门值得研究的技术,前端的表现力得到了大幅度的提升。
|
||||
|
||||
但很长一段时间里,JavaScript一直都不是一门正式的语言,对于很多人来说,它只是要做前端时顺便学习的语言。这种现象一直持续到Node.js的诞生。
|
||||
|
||||
Node.js其实是一个集成商,它之所以能有良好的表现要归功于V8这个JavaScript引擎。而V8的出现则要归因于Google对于网络应用前景的格局。
|
||||
|
||||
想当年的浏览器大战,Netscape和IE拼得你死我活,最终IE凭借Windows的优势成了赢家,Netscape也退出了历史舞台。然而,胜利后的微软认为天下已平,竟然解散了IE的团队,导致了程序员们要在很长时间内忍受IE这个既不标准又慢的浏览器。
|
||||
|
||||
这就给后来居上者留下了空间。最有名的两个后来者,一个是Netscape的转世Firefox,另外一个就是Google出品的Chrome。
|
||||
|
||||
**Chrome认为未来的页面一定要有更强的表现力,所以,一个高效强大的浏览器是必需的**。既然慢是个大问题,Chrome就着力解决慢这个问题,甚至不惜开发了一个新的JavaScript引擎,也就是V8,它的重点就是解决JavaScript执行慢的问题。可等微软看懂Google的操作,幡然悔悟,重新投入浏览器的开发之时,大势已去。Chrome成了新的霸主。
|
||||
|
||||
Chrome有一点做得很好,V8一开始就是一个独立的JavaScript引擎。所以Node.js才可以很方便地把它借鉴过去。除了V8的性能优势,Node.js还引入了异步IO的模型,这刚好与JavaScript事件驱动的特点相吻合。
|
||||
|
||||
Node.js刚一登场便赢得了满堂喝彩。因为,人们认识到,JavaScript原来不只能在浏览器中运行,也可以跑在服务器端。很快,**NPM这个包管理器登场,降低了众多开发者参与的门槛,JavaScript迎来了属于自己的爆发**,各种各样的程序库让人眼花缭乱。
|
||||
|
||||
前端开发也由少数人的爱好,成为了一个专属的职位,像React、Angular、Vue等框架的出现,更是让前端开发有了工程的味道,而不再是小打小闹了。
|
||||
|
||||
一旦JavaScript突破了浏览器的限制,给人们的想象空间就大了许多。除了服务器端,有人想把JavaScript用在嵌入式开发中,有人想把它用在手机开发中。**JavaScript成了一门全平台覆盖的语言**,大有一统天下的架势。
|
||||
|
||||
不过,JavaScript作为一门语言,其问题之多也是由来已久的。虽然JavaScript本身也在不断进化,但沉重的历史包袱让很多人都想开发出新的语言去替代它。所以,在JavaScript社区中,很多人把它看成了一种Web上的汇编语言,把新的语言编译成JavaScript,这样,就可以在浏览器上运行了。从早先的CoffeeScript到现在的TypeScript,甚至新一代的JavaScript标准都是以这种方式进行开发的。
|
||||
|
||||
当然还有人有更高的追求,他们认为仅仅在语言层面屏蔽JavaScript是不够的。**WebAssembly就是想成为Web上真正的汇编**,真正取JavaScript而代之,事实上,它也得到了很多人的支持。不过,这种努力至今仍在继续中,还有很长的路要走。
|
||||
|
||||
JavaScript就是这样,从一出生就不受待见,到今天,很多人仍想把它干掉。但这并不妨碍它在软件开发的历史中写下浓墨重彩的一笔。
|
||||
|
||||
## Go和Rust
|
||||
|
||||
在系统编程方面,C语言是当之无愧的霸主,然而,C语言已经快50岁了。在计算机这个快速变化的行业里,50年长得令人发指。在这50年中,C从被人质疑发展到如日中天,再到应用开发的地位逐步被取代。如今,它只在系统编程有着无可替代的作用。事实上,人们也一直想着替代它。
|
||||
|
||||
C的强项是对于计算机模型的适度抽象,弱项却是在程序的组织上。因为在C诞生那个年代,程序的规模还不算太大。然而C的成功却让程序的规模越来越大,大到超出了C语言的能力范畴。于是,有人想着把面向对象加到C语言里,扩大程序的组织规模。这方面的尝试,我们都熟悉的是C++。
|
||||
|
||||
不过,C++只风光了一段时间,就被Java盖了过去。C++本身有一段时间变成了语言特性的试验田,泛型编程,尤其是模板元编程的出现,一度让人怀疑人生。它成了高手极度喜爱,普通人一脸懵硬着头皮写的程序语言。
|
||||
|
||||
但更重要的是,C++背负了C语言所有的历史负担。所以,很多C的问题在C++里面依然存在,比如,内存管理。虽然C++有各种补丁方案,但你必须对C++极其了解,才能写好C++,然而,这个要求对于一个工程化的语言来说,实在是太高了。
|
||||
|
||||
所以,无论是C还是C++,都是在执行性能上无可挑剔,在代码编写上一地鸡毛,人们还是需要一门更有开发效率的系统编程语言。
|
||||
|
||||
时间来到新千年,又有人出手想代替C语言,这回出手的人物背景强大,他就是Ken Thompson,C语言的亲爹。2009年,如日中天的Google推出了Go语言,再加上Ken Thompson和Rob Pike这样早期的Unix先驱站在它背后,Go语言的前景给人无限的遐想。
|
||||
|
||||
Go语言的语法设计是简单的,基本上,你花一个晚上就可以把Go语言完整地学习一遍。**它在接口设计和并发上的处理方式都给人眼前一亮的感觉**。人们热切地期盼着它成为下一个系统编程语言的霸主。
|
||||
|
||||
但事实并没有像人们想象地那样发生,除了初生之时引起了一片欢呼,Go语言很长一段时间都在低位徘徊。比较有趣的是,中国有很多开发者对于Go的喜爱程度极高,一度让Go语言在中国的热度远远超过了全球的平均水平。之所以Go没有很快赢得人们的关注,因为它关注的系统编程领域并没有太多的机会留给它,人们嘴上喊着热爱,手里还依然用C写着代码。
|
||||
|
||||
不过,机会总是留给做好准备的人,语言也不例外。随着Docker这套虚拟化软件登上历史舞台,Go语言终于有了用武之地。人们开始意识到,**原来云计算领域还有一些基础设施要写,用C的话,不好维护;用Java的话,浪费资源;Go恰如其分地解决了大部分问题**。
|
||||
|
||||
一批新生代的基础设施纷纷出炉,除了Docker之外,还有帮助人们实现容器部署的Kubernetes,也就是k8s,还有辅助Service Mesh的istio等等。
|
||||
|
||||
虽然在云计算基础设施中,Go赢得了一席之地,这属于开辟了一片蓝海。在传统系统编程的红海中,Go语言其实并没有做出什么特别的成绩,对于实时性和性能要求极高的领域,Go语言有一个拿不出手的弱项,也就是它的GC。
|
||||
|
||||
自动的内存管理固然是简化程序员工作的一项重要手段,但对于系统编程这个领域而言,GC显然还没有表现得能够赢得大家的信任,而且,在可见的未来,也不会有明显的起色。
|
||||
|
||||
所以,在系统编程领域替代C的征程上,大家都还有机会。这条赛道上目前最有力的竞争者是Rust。
|
||||
|
||||
Rust出自Mozilla,这是浏览器Firefox背后的公司,它原本是Mozilla员工Graydon Hoare的个人项目,后来得到了公司赞助,由一个练手的项目成为了一个正式的项目。
|
||||
|
||||
Rust对初学者并不友好,对于习惯“少废话、先动手”的程序员而言,Rust的初体验可能一点都不好,按照习惯方式写出来的代码很可能是无法编译的。比如,Rust的“变”量缺省是不变的,再有,想写好Rust程序,先要了解所有权的概念。不过,也恰恰是因为这些限制,让Rust写出来的程序犯下低级错误的概率大大降低了。
|
||||
|
||||
**如果你理解系统编程面临的问题,以及现代软件开发的趋势,你会发现,Rust提供的选项很好地规避了许多问题**。比如,之所以要用不变性,是因为它可以规避掉很多因为“变”带来的问题,这是函数式编程给软件开发贡献的一个重要思路。再比如,所有权的概念也是为了防止一块内存不同的人去改,造成各种问题,同时,也给内存管理提供了新的思路。
|
||||
|
||||
内存不能让程序员管,这已经成了共识,但主流的GC方案又不能满足系统编程的需要,Rust则给出了第三种方案,把内存当作一种资源,申请下来就初始化好,出了生命周期就销毁掉。之所以能够做到这点,还是要拜Rust强大的编译器所赐,因为所有权的存在,编译器可以很好地分析出内存到底该什么时候释放。
|
||||
|
||||
Rust成为系统编程语言的有力竞争者还有一个原因,它背靠着LLVM。LLVM是一套编译器的基础设施,它的出现是因为传统的工具链GCC太过沉重。LLVM把编译器的前端和后端分离开来,语言开发者只要关注前端,设计好各种语言特性,就可以利用LLVM的后端进步的优势,比如,不断优化带来的性能提升。对系统编程语言来说,一个重点就是可移植性。
|
||||
|
||||
系统编程一个重要的战场就是各种嵌入式设备,而绝大多数设备都只支持C/C++语言。一个重要的原因就是谁来移植编译器,C/C++的后端常常是厂商提供支持的,而其他语言则多半无人理睬。现在有了LLVM的基础设施,一个芯片厂商只要支持了LLVM的后端,用LLVM前端开发出的语言也就都得到了支持。这对于新兴语言来说,绝对是一个巨大的好消息。
|
||||
|
||||
Rust在语言层面表现出来的安全特性,帮它赢得了像微软、亚马逊这样大厂的注意;占用资源少的内存管理方式,让一些人开始尝试使用它编写Linux驱动;更多的移植可能,也让它成为了嵌入式开发的一种考虑。在这场C语言替代者的竞争中,Rust值得期待!
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天的内容主要是为了让大家放松一下,所以,我们也不做内容上的总结了。
|
||||
|
||||
每个程序员除了学习当下要用到的知识之外,一般都会对自己的未来做一些技术储备,其中,判断技术趋势就是我们在投资未来时的一个重要参考。
|
||||
|
||||
如何才能更好地判断未来技术发展趋势呢?就是去知道一些技术的发展历史。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后,我想请你分享一下,你看好哪门语言未来的发展?为什么?欢迎在留言区分享你的想法。
|
||||
|
||||
感谢阅读,如果你觉得这一讲的内容对你有帮助的话,也欢迎把它分享给你的朋友。
|
||||
Reference in New Issue
Block a user