Uploaded avatar of iHiD

机械三月到啦!

@iHiD
3年多前
视频

欢迎来到我们的第二个主题月: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 和我都很想听听你选了哪门语言、用下来感觉如何,所以请一定在评论区或论坛里发帖告诉我们!

感谢观看!

2023年03月01日 · 觉得有用吗?