为什么选择Kotlin?
我们将概述编程语言的历史发展,以便您了解Kotlin的定位以及为什么您可能想要学习它。对于初学者来说,本节介绍的一些主题可能现在看起来过于复杂。您可以随时跳过这一节,在阅读更多本书后再回来阅读。
程序必须为人类阅读而编写,而仅仅偶然为了机器执行。— Harold Abelson,《计算机程序的结构与解释》
编程语言设计是一条从满足机器需求到满足程序员需求的进化路径。
编程语言由语言设计师发明,并作为一种或多种用于使用该语言的工具的程序进行实现。实施者通常是语言设计师,至少在最初是这样。
早期的语言关注硬件限制。随着计算机变得更加强大,新的语言转向更复杂的编程,并强调可靠性。这些语言可以根据编程心理学选择功能。
每种编程语言都是一系列的实验。从历史上看,编程语言设计是一连串猜测和假设的历程,试图使程序员更具生产力。其中一些实验失败,一些稍微成功,一些非常成功。
我们从每种新语言的实验中学到了很多。有些语言解决的问题后来被发现是次要的而不是必要的,或者环境发生了变化(处理器速度更快、内存更便宜、对编程和语言的理解更深入),那个问题变得不那么重要甚至不重要。如果这些思想过时了而语言没有发展,那么它就会被淡出使用。
最初的程序员直接使用表示处理器机器指令的数字进行工作。这种方法产生了许多错误,并创建了汇编语言以用助记符操作码(程序员更容易记住和阅读的单词)替换数字,以及其他有用的工具。然而,汇编语言指令和机器指令之间仍然存在一对一的对应关系,程序员需要编写每一行汇编代码。此外,每个计算机处理器都使用自己独特的汇编语言。
在汇编语言中开发程序非常昂贵。高级语言通过从低级汇编语言中创建一层抽象来解决这个问题。
编译器和解释器
Kotlin是编译而不是解释的。解释语言的指令由一个独立的程序,称为解释器,直接执行。相比之下,编译语言的源代码会被转换为不同的表示形式,作为自己的程序运行,可以直接在硬件处理器上运行,也可以在模拟处理器的虚拟机上运行:
诸如C、C++、Go和Rust之类的语言编译成在底层硬件的中央处理器(CPU)上直接运行的机器代码。而Java和Kotlin等语言编译成字节码,它是一种中间级格式,不直接在硬件CPU上运行,而是在虚拟机上运行,虚拟机是一个执行字节码指令的程序。Kotlin的JVM版本运行在Java虚拟机(JVM)上。
可移植性是虚拟机的一个重要优点。相同的字节码可以在每台安装了虚拟机的计算机上运行。虚拟机可以针对特定硬件进行优化以解决速度问题。JVM包含多年的这种优化,并已在许多平台上实施。
在编译时,编译器会检查代码以发现编译时错误(IntelliJ IDEA和其他开发环境会在您输入代码时突出显示这些错误,以便您可以快速发现和修复任何问题)。如果没有编译时错误,源代码将被编译成字节码。
运行时错误在编译时无法检测到,因此只有在运行程序时才会出现。通常,运行时错误更难发现,修复起来更加昂贵。静态类型语言(如Kotlin)会尽可能多地在编译时发现错误,而动态语言会在运行时执行安全检查(某些动态语言可能不执行太多的安全检查)。
影响 Kotlin 的编程语言
Kotlin从许多编程语言中汲取了想法和特性,而这些编程语言又受到了早期编程语言的影响。了解一些编程语言的历史可以让我们更好地理解 Kotlin 的由来。这里选择的语言是因为它们对后续语言产生了影响。这些语言最终启发了 Kotlin 的设计,有时是通过成为一种要避免的示例。
FORTRAN: FORmula TRANslation(1957)
Fortran是为科学家和工程师设计的,其目标是更容易地编写方程式。经过精心调试和测试的 Fortran 库仍然在今天使用,但通常会进行“封装”,以便从其他语言调用它们。
LISP: LISt Processor(1958)
LISP不是针对特定应用程序,而是体现了基本的编程概念;它是计算机科学家使用的语言,也是第一种函数式编程语言(您将在本书中学习函数式编程)。LISP 的强大和灵活性换来的是效率问题:在早期计算机上运行 LISP 通常代价太高,直到最近几十年,机器速度才足够快,才出现了 LISP 的复兴。例如,GNU Emacs 编辑器完全是用 LISP 编写的,并且可以使用 LISP 进行扩展。
ALGOL: ALGOrithmic Language(1958)
可以说是 1950 年代最有影响力的语言,因为它引入了在许多后续语言中仍然存在的语法。例如,C 及其派生语言都是“类 ALGOL”语言。
COBOL: COmmon Business-Oriented Language(1959)
COBOL是为业务、财务和行政数据处理而设计的。它具有类似英语的语法,并且旨在具有自解释和高可读性。尽管这种意图通常失败了,COBOL 因为错误的点号引入的 bug 而出了名——但美国国防部强制在大型机上广泛采用了 COBOL,并且至今仍在运行(并需要维护)。
BASIC: Beginners' All-purpose Symbolic Instruction Code(1964)
BASIC 是早期使编程变得简单易学的尝试之一。尽管非常成功,但它的功能和语法有限,因此对于需要学习更复杂语言的人来说只有一定的帮助。它主要是一种解释型语言,这意味着要运行它,您需要原始的程序代码。尽管如此,许多有用的程序都是用 BASIC 编写的,尤其是作为微软的“Office”产品的脚本语言。BASIC 甚至可以被认为是第一个“开放”的编程语言,因为人们制作了许多变种。
Simula 67,最早的面向对象语言(1967)
模拟通常涉及许多相互交互的“对象”。不同的对象具有不同的特征和行为。当时存在的语言在模拟方面使用起来很笨拙,因此开发了 Simula(另一种“类 ALGOL”语言),以提供直接支持创建模拟对象。事实证明,这些思想对于通用编程也非常有用,这就是面向对象(OO)语言的起源。
Pascal(1970)
Pascal通过限制语言,使其可以实现为单通道编译器,从而提高了编译速度。该语言强制程序员以特定方式组织代码,并对程序组织施加了一些笨拙且不太可读的约束。随着处理器变得更快、内存更便宜和编译器技术的改进,这些约束的影响变得过于昂贵。
Pascal的一种实现,Borland的Turbo Pascal,最初适用于 CP/M 机器,然后转移到早期的 MS-DOS(Windows 的前身),后来演变为适用于 Windows 的 Delphi 语言。通过将所有内容放入内存,Turbo Pascal 在性能较差的机器上编译速度极快,极大改善了编程体验。Turbo Pascal 的创建者 Anders Hejlsberg 后来设计了 C# 和 TypeScript。
Pascal 的发明者 Niklaus Wirth 还创建了后续的语言:Modula、Modula-2 和 Oberon。正如名称所示,Modula 侧重于将程序分解为模块,以实现更好的组织和更快的编译。大多数现代语言支持分离编译和某种形式的模块系统。
C(1972)
尽管出现了越来越多的高级语言,程序员仍然在编写汇编语言。这通常被称为系统编程,因为它是在操作系统级别进行的,但它还包括针对专用物理设备的嵌入式编程。这不仅费时费力(Bruce在他的职业生涯开始时就编写过嵌入式系统的汇编语言),而且不可移植——汇编语言只能在编写它的处理器上运行。C 被设计为一种“高级汇编语言”,仍然与硬件紧密相关,以至于很少需要编写汇编语言。更重要的是,C 程序可以在任何具有 C 编译器的处理器上运行。C 将程序与处理器分离,解决了一个巨大而昂贵的问题。因此,以前的汇编语言程序员在 C 中可以大大提高生产力。C语言非常有效,以至于最近的一些语言(尤其是Go和Rust)仍然试图取代它成为系统编程的首选语言。
Smalltalk(1972年)
Smalltalk从一开始就被设计成纯粹的面向对象语言。通过作为一个实验平台和展示快速应用开发的工具,Smalltalk在推动面向对象和语言理论方面取得了显著进展。然而,在Smalltalk诞生时,编程语言仍然是专有的,购买一个Smalltalk系统可能需要花费成千上万的价格。它是一种解释型语言,因此需要一个Smalltalk环境来运行程序。直到编程世界已经发展了很久之后,开源的Smalltalk实现才出现。Smalltalk程序员为后来的C++和Java等面向对象语言做出了重要贡献。
C++:更好的C语言与对象(1983年)
Bjarne Stroustrup创建C++的原因是他想要一个更好的C语言,并且希望能够支持他在使用Simula-67时遇到的面向对象的构造。Bruce曾在C++标准委员会任职八年,并撰写了三本关于C++的书籍,包括《Thinking in C++》。
与C的向后兼容性是C++设计的基本原则之一,因此可以几乎不修改地将C代码编译为C++代码。这为程序员提供了一条简便的迁移路径,他们可以继续使用C进行编程,享受C++的好处,并在保持生产力的同时逐步尝试C++的特性。大多数对C++的批评可以追溯到与C的向后兼容性的约束。
C语言的一个问题是内存管理。程序员必须首先获取内存,然后在使用该内存的操作中运行,最后释放内存。忘记释放内存会导致内存泄漏,可能会消耗掉可用内存并使进程崩溃。最初的C++版本在这个领域做出了一些创新,包括使用构造函数确保正确初始化。语言的后续版本在内存管理方面进行了重大改进。
Python:友好而灵活(1990年)
Python的设计者Guido Van Rossum根据他对“面向所有人的编程”的启发创建了这门语言。他对Python社区的培育使其在编程界赢得了友好和支持性最强的声誉。Python是最早的开源语言之一,几乎在所有平台上都有实现,包括嵌入式系统和机器学习。它的动态性和易用性使其非常适合自动化处理小型重复任务,同时其特性也支持开发大型复杂程序。
Python是一门真正的“草根”语言;它从未有过推广它的公司,而其粉丝们的态度是从不推动该语言,而只是帮助任何想学习它的人。这门语言不断改进,近年来其受欢迎程度飙升。
Python可能是第一门将函数式和面向对象编程结合起来的主流语言。它在自动内存管理方面领先于Java(通常无需自己分配或释放内存),并且可以在多个平台上运行程序。
Haskell(1990年):纯函数式编程
受专有语言Miranda(1985年)的启发,Haskell作为一个开放标准被创建,用于纯函数式编程的研究,尽管它也被用于产品开发。Haskell的语法和思想影响了许多后来的语言,包括Kotlin。
Java(1995年):虚拟机和垃圾回收
詹姆斯·高斯林(James Gosling)和他的团队被派任务为一台电视机顶盒编写代码。他们决定不使用C++,而是创建了Java语言。当时,Sun Microsystems公司大力推广这种免费语言(这在当时还是一个新概念),以试图在新兴的互联网领域占据主导地位。
这种被视为互联网主导时机的压力使Java语言设计承受了很大的压力,结果导致了大量的缺陷(《Thinking in Java》一书揭示了这些缺陷,以便读者能够应对它们)。甲骨文公司(Oracle)的布赖恩·戈茨(Brian Goetz),目前是Java的首席开发人员,尽管面临着继承的限制,但在Java方面取得了显著而令人惊讶的改进。尽管Java非常成功,但Kotlin的一个重要设计目标是修复Java的缺陷,以便程序员能够更加高效。
Java的成功源于两个创新功能:虚拟机和垃圾回收。其他语言也提供了这些功能,例如LISP、Smalltalk和Python都具有垃圾回收功能,UCSD Pascal在虚拟机上运行,但它们从未被认为适用于主流语言。Java改变了这一点,并因此使程序员的工作效率显著提高。
虚拟机是语言和硬件之间的中间层。语言不需要为特定处理器生成机器代码;它只需要生成在虚拟机上运行的中间语言(字节码)。虚拟机需要处理能力,在Java之前被认为是不切实际的。Java虚拟机(JVM)催生了Java的口号“一次编写,到处运行”。此外,其他语言可以更容易地通过针对JVM开发;例如Groovy是一种类似Java的脚本语言,Clojure是一种LISP的变种。
垃圾回收解决了忘记释放内存或难以确定何时不再使用某个存储单元的问题。由于内存泄漏,许多项目被大大延迟甚至取消。尽管垃圾回收在一些之前的语言中已经存在,但人们认为它会产生无法接受的开销,直到Java证明了其实用性。
JavaScript(1995年):名字上的Java而已
最初的Web浏览器只是从Web服务器复制和显示页面。Web浏览器的数量不断增加,成为需要语言支持的新的编程平台。Java希望成为这种语言,但对于这个工作来说过于笨拙。JavaScript最初被称为LiveScript,并内置于NetScape Navigator中,这是最早的Web浏览器之一。将其改名为JavaScript是NetScape的一种营销策略,因为这种语言与Java只有模糊的相似之处。
随着Web的发展,JavaScript变得非常重要。然而,JavaScript的行为如此不可预测,以至于道格拉斯·克罗克福德(Douglas Crockford)写了一本名为《JavaScript权威指南》的书,他在其中以嬉笑怒骂的方式展示了这门语言的所有问题,以便程序员可以避免它们。ECMAScript委员会的后续改进使得JavaScript对于最初的JavaScript程序员来说几乎无法识别。它现在被认为是一门稳定而成熟的语言。
WebAssembly(WASM)是从JavaScript衍生出来的一种Web浏览器的字节码。它通常比JavaScript运行速度更快,并且可以由其他语言生成。截至目前,Kotlin团队正在努力将WASM作为一个目标添加到语言中。
C#(2000年):针对.NET的Java
C#的设计目标是在.NET(Windows)平台上提供与Java相似的重要功能,同时使设计者摆脱遵循Java语言的限制。其结果包括许多对Java的改进。例如,C#引入了扩展函数(extension functions)的概念,这在Kotlin中被广泛使用。C#也比Java更具有函数式编程的特性。许多C#的特性显然影响了Kotlin的设计。
Scala(2003年):可伸缩的编程语言
Martin Odersky创建了Scala,以在Java虚拟机上运行:借助JVM的工作成果,与Java程序进行交互,并可能取代Java。作为一名研究人员,Odersky和他的团队将Scala作为一个实验语言的平台,尤其是那些未包含在Java中的语言特性。
这些实验是有启发性的,其中一些实验以修改的形式出现在了Kotlin中。例如,重定义像+
这样的运算符以在特殊情况下使用被称为运算符重载。这在C++中被包括进去了,但在Java中却没有。Scala增加了运算符重载的功能,但也允许您通过组合任意字符序列来发明新的运算符。这往往会产生令人困惑的代码。Kotlin中包含了一种有限的运算符重载形式,但您只能重载已经存在的运算符。
Scala还是一种对象-函数混合语言,类似于Python,但更注重纯函数和严格对象。这帮助启发了Kotlin也成为一种对象-函数混合语言的选择。
与Scala类似,Kotlin也在JVM上运行,但与Scala相比,它与Java的交互更加容易。此外,Kotlin还支持JavaScript、Android操作系统,并能为其他平台生成本机代码。
《Atomic Kotlin》是根据《Atomic Scala》(http://www.AtomicScala.com)中的思想和内容发展而来的。
Groovy(2007年):一种动态JVM语言
动态语言因其比静态语言更具交互性和简洁性而具有吸引力。在JVM上产生更具动态编程体验的尝试已经有很多,包括Jython(Python)和Clojure(一种Lisp方言)。Groovy是第一个被广泛接受的实现。
乍看之下,Groovy似乎是Java的清理版本,提供了更愉快的编程体验。大多数Java代码在Groovy中可以不作任何修改地运行,因此Java程序员可以迅速提高生产力,然后学习更复杂的功能,这些功能相比Java提供了显著的编程改进。
Kotlin中处理空值问题的运算符?.
和?:
最早出现在Groovy中。
有许多Groovy的特性在Kotlin中是可以辨认出来的。其中一些特性也出现在其他语言中,这可能更加推动它们被包含在Kotlin中。
为什么选择Kotlin?(引入于2011年,1.0版本于2016年发布)
正如C++最初意图成为“更好的C语言”,Kotlin最初的目标是成为“更好的Java”。但它已经在这个目标之外显著发展。
Kotlin在实践中从其他编程语言中选择了最成功和最有帮助的特性,这些特性经过了实地测试,并被证明特别有价值。
因此,如果您来自另一种语言,您可能会在Kotlin中认出该语言的一些特性。这是有意为之的:Kotlin通过利用经过测试的概念来最大化生产力。
可读性
在语言设计中,可读性是一个主要目标。Kotlin的语法简洁明了,对于大多数情况而言不需要冗长的仪式,但仍能表达复杂的思想。
工具支持
Kotlin来自JetBrains,这是一家专注于开发者工具的公司。它具有一流的工具支持,并且许多语言特性都是针对工具设计的。
多范式
Kotlin支持多种编程范式,并在本书中温和地引入了这些范式:
- 命令式编程
- 函数式编程
- 面向对象编程
多平台
Kotlin的源代码可以编译为不同的目标平台:
- JVM。源代码编译为JVM字节码(
.class
文件),可以在任何Java虚拟机(JVM)上运行。 - Android。Android有自己的运行时环境,称为ART(前身为Dalvik)。Kotlin源代码编译为Dalvik可执行格式(
.dex
文件)。 - JavaScript,用于在Web浏览器中运行。
- 本机二进制文件,通过生成特定平台和CPU的机器代码。
本书专注于语言本身,并以JVM作为唯一的目标平台。一旦掌握了该语言,您可以将Kotlin应用于不同的应用程序和目标平台。
Kotlin的两个特性
本章并不假定您是一名程序员,这样就很难解释Kotlin相对于其他选择的大部分优势。然而,有两个主题非常有影响力,可以在这个早期阶段进行解释:Java互操作性和表示“无值”的问题。
无缝的Java互操作性
作为“更好的C语言”,C++必须与C的语法保持向后兼容,但Kotlin并不需要与Java的语法保持向后兼容,它只需要与JVM配合工作。这使得Kotlin设计者能够创建一个更清晰、更强大的语法,避免了Java中的视觉噪声和复杂性。
为了成为“更好的Java语言”,尝试Kotlin的体验必须是愉快和无摩擦的,因此Kotlin能够与现有的Java项目轻松集成。您可以编写一个小小的Kotlin功能模块,并将其插入到现有的Java代码中。Java代码甚至不知道Kotlin代码的存在,它只是看起来像更多的Java代码。
公司通常会通过使用某种语言构建一个独立的程序来研究新语言。理想情况下,这个程序是有益但不是必需的,因此如果项目失败,可以以最小的损失终止它。并不是每个公司都愿意为这种类型的实验投入必要的资源。由于Kotlin可以与现有的Java系统无缝集成(并受益于该系统的测试),因此尝试Kotlin以查看是否适合变得非常廉价甚至免费。
此外,为Kotlin创建的JetBrains公司提供了IntelliJ IDEA的“Community”(免费)版本,其中包括对Java和Kotlin的支持以及轻松集成两者的能力。它甚至还提供了一个工具,将Java代码(大部分)重写为Kotlin代码。
附录B介绍了Java互操作性。
表示空值
Kotlin的一个特别有益的功能是解决了一个棘手的编程问题。
当有人给您一本字典,并要求您查找一个不存在的单词时,您会怎么做?您可以通过为未知单词编造定义来保证结果。更有用的方法是简单地说:“这个单词没有定义。”这展示了编程中的一个重要问题:如何表示未初始化的存储空间或操作的结果的“无值”?
*空引用(null reference)*是由Tony Hoare于1965年为ALGOL语言发明的,他后来称其为“我一生中价值数十亿美元的错误”。其中一个问题是它过于简单,有时仅仅告诉一个房间是空的是不够的;您可能需要知道它为空的原因。这导致了第二个问题:实现。出于效率考虑,通常只是一个特殊的值,可以适应少量内存,并且何不使用已经为该信息分配的内存呢?
原始的C语言不会自动初始化存储空间,这引发了许多问题。C++通过将新分配的存储空间设置为全零来改善了情况。因此,如果数值没有初始化,它就是一个数值零。这看起来并不那么糟糕,但它允许未初始化的值悄悄地通过漏洞滑入(新的C和C++编译器经常对此发出警告)。更糟糕的是,如果一个存储空间是一个指针,用于指示(“指向”)另一个存储空间,那么一个空指针将指向内存中的位置零,而这几乎肯定不是您想要的。
Java通过在运行时报告这些未初始化值的访问错误来防止访问未初始化值。尽管这可以发现未初始化值,但它并没有解决问题,因为您只能通过运行程序来验证您的程序不会崩溃。Java代码中有大量此类错误,程序员浪费了大量时间来查找它们。
Kotlin通过在程序运行之前,在编译时阻止可能导致空错误的操作,解决了这个问题。这是Java程序员接受Kotlin时最受欢迎的单一特性。这个特性可以最大程度地减少或消除Java的空错误。
多种好处
我们在这里能够解释的两个特性(无需更多的编程知识)对您是否是Java程序员都有很大的影响。如果Kotlin是您的第一种语言,并且您最终参与需要更多程序员的项目,那么招募众多现有的Java程序员进入Kotlin会更加容易。
Kotlin还有许多其他好处,但在您了解更多编程知识之前,我们无法对其进行解释。这就是本书的余下部分的目的。
语言通常是出于热情而选择的,而不是理性... 我正在努力使Kotlin成为一种因为理由而受人喜爱的语言。 ——Andrey Breslav,Kotlin首席语言设计师。