欢迎来到我们的第二个主题月:Mechanical March。这个月我们聚焦系统语言,也就是那些会编译成机器码的语言。
这篇内容一部分是博文,一部分是 Mechanical March 视频的文字记录。 我会先简单介绍一下这个月,然后我们一起看看系统语言、它们在历史上的演变、编译成机器码的优缺点,最后快速过一遍每门精选语言。 Erik 这次也和我一起出镜,后半部分主要由他来讲。不过我先带大家看一些实用的信息。
首先说说这个月的精选语言。它们是 C、C++、D、Go、Nim、Rust、V 和 Zig。想拿到 Mechanical March 徽章,你需要用其中一门语言完成五个练习。我们的 Go 学习路径拥有 Exercism 上最好的教学大纲之一,所以我强烈推荐你去试试。在 Exercism,我们也是 Nim 的忠实粉丝,因为它入门相对简单,写起来也很轻松,所以我也非常推荐你试一试。
我们还准备了五个精选练习供你尝试:
- Linked-list 或 simple-linked-list(取决于具体语言):内存的分配和释放、指针
- secret-handshake:位运算
- pangram:
for循环、字符串和字符 - sieve:数组、
for循环 - binary-search:数组、循环
几天前我在更新视频里公布了一枚新徽章,专门颁给在主题语言中完成这五个精选练习的人。想拿到这枚徽章,你需要在今年内用某门系统语言把这五个练习全部完成。
我们还准备了很多好玩的内容正在酝酿:采访 Go 核心团队的几位成员,希望还能采访到 Rust 和其他一些语言社区的人。整个月我们还会做很多直播。另外,一批 Mechanical March 周边也即将上线!
那我们就再深入一点,聊聊技术方面的内容。
这些语言都用在什么地方?
我们自己在 Exercism 各处都在用它们。我们的 CLI 是用 Go 写的,内部那个叫 configlet 的学习路径管理工具是用 Nim 写的,而统计你提交的解答中有多少行代码的核心库是用 Rust 写的。Erik,我们为什么为这些工具选择了这些语言?
CLI:
- 我想我们用 Go 是因为 Katrina 最熟悉它。
- Go 非常适合这类规模不大的命令行工具。
- Go 代码相对直白,别人更容易参与贡献。
- Go 编译出的二进制文件很容易部署,因为它不需要运行时。
- Go 的交叉编译做得很好。
Nim
- Nim 拥有和 Go 大体相同的优点
- 我们没有多少既懂 Go 又能维护 CLI 的人,于是换成了 Nim
你觉得这些语言还会用在哪些地方?
- 任何性能至关重要的地方(驱动程序、游戏、操作系统、构建系统/编译器)
- 任何资源受限的地方(比如嵌入式软件)
- 任何需要高度可移植的地方,也就是说能在许多不同平台上运行
什么是机器码?
就像我之前说的,Mechanical March 里的这些语言有个共同特征:它们都会编译成机器码。你能稍微解释一下什么是机器码,以及与之相对的字节码又是什么吗?
- 机器码是能直接在机器上运行的代码。
- 相比之下,字节码需要由其他代码来解释,或者编译成机器码。所以字节码在执行之前需要一个中间步骤。
相比字节码,机器码有哪些优缺点?
优点:
- 启动更快(没有 JIT 编译这一步)
- 占用内存更少(不用加载运行时,内存里也没有字节码,非常适合嵌入式系统)
- 目标机器不需要安装运行时(这对保持 Docker 容器体积小巧很重要)
缺点:
- 不可移植。字节码是可移植的,但编译后的机器码只针对特定平台
- 无法进行基于性能分析的优化这类高级优化(即在代码运行一段时间后,确定编译这段(字节)代码的最佳方式)
注意:也可以采用混合方式,即语言先编译成字节码,再用另一个工具把字节码编译成机器码。
系统编程的演变
好,那我们就来看看这个月几门语言的一些演变。先从最早的 C 说起,再看看 C++ 是如何从 C 发展而来的。给我们讲讲这两门语言吧。
C 是一门非常底层的语言。用起来感觉只比机器码高一点点。这使它非常强大、高度可优化,但也比较容易出 bug(比如空指针异常和缓冲区溢出)。 内存管理完全靠手动,因此是程序员的责任,这也可能导致 bug 或者内存泄漏。 C++ 就像 C,但增加了对面向对象编程的支持。它仍然相当底层,需要你手动管理内存。C 和 C++ 都允许你写内联汇编(ASM)!
那较新的系统语言呢?它们是如何演变的?
所有现代系统编程语言都支持自动内存管理,方式可以是引用计数、垃圾回收器,或者别的机制。
早期的系统编程语言都支持空指针,Tony Hoare 称其为自己的“十亿美元错误”。空指针因为会导致运行时错误和安全漏洞而臭名昭著。 许多现代语言干脆取消了 null,或者至少要求你付出额外的努力才能用它。
另一个变化是,从默认可变的值转向默认不可变的值。比如 Rust 和 VLang 都默认让值不可变,想改成可变需要显式选择。
所有较新的语言都支持与 C(或 C++)互操作,因为已经有太多代码是用这些语言写的。
另一个有意思的地方是,有些较新的语言并不直接编译成机器码,而是借助其他工具来完成。比如 Rust 和 Zig 使用 LLVM,而 Nim 允许你使用各种不同的编译器。这就是所谓的转译。
那宏和元编程之类的东西呢?
在宏和元编程方面,存在一个很有意思的分歧。C/C++ 的宏很强大,但名声不算太好,因为用起来很难。Rust、Nim 和 D 都提供了强大的元编程能力,但方式要友好得多。反过来,VLang 和 Zig 都明确把“没有宏”当作语言的一个特性,而 Go 则用 go generate 走出了另一条路。
系统语言素来以相当底层著称。这种说法现在还成立吗?
较新的语言都在比 C/C++ 更高的抽象层次上工作。 比如 Rust、D 和 Nim 也支持非常函数式的代码写法。 Nim 和 D 甚至还有“纯函数”这个概念。
本月语言简介
那我们最好逐一看看这些语言。它们都有相似之处:全都是强类型、静态类型。不过我们来看看它们有什么不同。先从 C 开始吧?
C
- 由 Dennis Ritchie 开发
- 世界上最古老、大概也是使用最广泛的语言之一
- 大量软件都是用 C 写的,比如 Unix 和 Linux
- 影响深远(想想看,如今“类 C 语言”已经是一大类别)
- 手动内存管理
- 性能极高(贴近底层)
- 到处都能运行
- 非常适合嵌入式系统
- 语言规模相当小
C++
- 由 Bjarne Stroustrup 开发
- C 的继任者,但加入了面向对象的特性(带类的 C)
- 推动了面向对象编程的普及
- 比 C 有更多高层语言特性
- 通过模板支持泛型编程
- 通过命名空间支持模块化
- 许多游戏(引擎)是用 C++ 写的,Windows 的很大一部分也是
- 手动内存管理
- 仍在不断演进,新特性定期加入(规范非常庞大)
D
- 由 Walter Bright 开发,后来 Andrei Alexandrescu 也加入其中
- 最初被视为重新设计的 C++(从它的“错误”中吸取教训),同时也从许多其他语言中汲取灵感
- 多范式,支持命令式、面向对象和函数式编程
- 与 C/C++ 互操作很容易
- 统一函数调用语法
- 编译期函数求值(比如在编译期生成正则状态机)
- 支持函数式编程和“纯函数”
- 大量安全特性
- 通过 @safe 实现内存安全
- 契约(前置/后置条件、不变式)
- 纯函数
- 重视单元测试,测试就放在被测源码旁边(这一点让 Exercism 网站不得不开了一个例外 :))
- Unicode
Rust
- 由 Mozilla Research 的员工 Graydon Hoare 开发,之后被 Mozilla 正式采纳,现在属于 Rust Foundation
- 多范式,支持面向对象(但有自己的主张,比如没有继承)、命令式和函数式编程(Option/Result 类型、模式匹配)
- 许多新工具是用 Rust 写的(比如 SWC,还有 Gleam,以及作为 Linux 内核支持的第二种语言;我们的代码行数统计工具就依赖它)
- 注重可靠性和性能
- 构建在 LLVM 之上
- 连续 7 年在 StackOverflow 调查中被评为最受喜爱的语言
- 速度快,部分原因是核心和标准库都很精简
- 安全,通过所有权和生命周期同时保证内存安全和线程安全,默认不可变
- 强大的类型系统,能在编译期捕获许多 bug(尤其是与内存相关的)。编译器给出的错误信息非常有用
- 该有的都有:编译器、构建工具、格式化工具、包管理器、IDE 集成
- 出色的文档(甚至有一份讲编译器如何工作的鸿篇巨制)
- 可移植:编译成单个静态二进制文件,不需要安装运行时
- 与 C 代码互操作很容易
- 零成本抽象
- 无畏并发
- 宏
Nim
- 由 Andreas Rumpf 开发(最初叫 Nimrod)
- 语法受 Python 启发
- 多范式
- 在 Exercism 的 configlet 中使用
- 性能出色:零开销迭代器,倾向于对基于值的类型使用栈分配
- 现代且富有表现力的类型系统:类型推断、元组、泛型、和类型、async/await
- 有垃圾回收,但也支持确定性的内存管理(多种内存管理选项)
- 编译期执行代码
- 统一调用语法
- 宏:轻松扩展语言
- 效果系统:把副作用编码进类型系统
Go
- 由 Google 的 Robert Griesemer、Rob Pike 和 Ken Thompson 开发
- 主要是命令式和过程式,支持类似面向对象的做法(但没有继承)
- 用于 Docker、Kubernetes 这样的大型项目。也非常适合后端和 CLI(比如 esbuild)
- 目标是简单到能整个装进脑子里(语法很少)
- 通过垃圾回收器保证内存安全
- 快:编译快、测试快、运行也快。内置对编写基准测试的支持!
- 有明确主张:花了很多心思来引导 Go 代码的风格:语法很少、用 go fmt 格式化代码、有工具检查代码是否符合惯用写法、在其他语言里只是警告的问题在 Go 里会直接报错(比如未使用的变量)、文档中列出了应当遵循的 Go 惯用法
- 轻量的类型系统让 Go 非常灵活(支持类型推断)
- 可移植:编译成单个静态二进制文件,不需要安装运行时。交叉编译也很容易。并发通过 goroutine(轻量级)实现,goroutine 之间通过 channel 通信
- 通过接口实现结构化类型(类似鸭子类型,但在静态检查下完成)
- 错误处理:这门语言鼓励你检查并处理错误
VLang
- 由 Alexander Medvednikov 和 Delyan Angelov 开发
- 灵感来自 Go:
- 同样奉行“一件事只有一种做法”的策略
- 同样奉行“语法极简”的策略
- 协程
- 与 Go 不同之处:
- 没有 nil/null,而是使用结果类型
- 默认不可变
- 和类型(函数式)
- 字符串插值
- 更小的运行时/二进制文件
- 模式匹配
- 正在努力让 GC 变成可选项(autofree)
- 零成本 C 互操作
- 从代码生成文档
- 编译器速度快,占用内存少
Zig
- 由 Andrew Kelley 开发
- 语法相当少(PEG 语法文件只有 500 行)
- 编译期执行代码和反射
- 目标是“显而易见”,没有隐藏的控制流,没有隐藏的分配,也没有宏/元编程
- 手动内存分配
- 支持不同的分配器
- 需要分配内存的标准库函数都带有一个分配器形参
- 测试框架能检测内存泄漏
- 安全方面:
- 错误就是值,必须处理
- 没有 null,使用可选类型
- 测试可以和源码写在同一个文件里(和 D 一样)
- 使用 LLVM 作为后端
- 能编译 C/C++ 代码
- 与 C 互操作很容易
- 交叉编译很容易
结语
我们就讲到这里吧,我想大家这会儿脑子里已经装得满满的了。
希望这是对本月精选语言一次有用又有趣的介绍。祝你在探索这些语言时玩得开心。Erik 和我都很想听听你选了哪门语言、用下来感觉如何,所以请一定在评论区或论坛里发帖告诉我们!
感谢观看!