X 编程语言文档
欢迎来到 X 编程语言的在线文档。
左侧是完整目录,你可以从「前言」或「安装 X」开始阅读本书。
License / 许可协议
本项目采用多重许可协议发布,使用者可以在以下任一许可证下使用本项目的代码:
- MIT License(MIT 许可证)
- Apache License 2.0(Apache 2.0 许可证)
- BSD 3-Clause License(BSD 三条款许可证)
除非另有说明,你可以任选其一并遵守相应条款进行使用、修改和分发。
This project is multi-licensed. You may use the code under the terms of any one of the following licenses:
- MIT License
- Apache License 2.0
- BSD 3-Clause License
Unless otherwise stated, you may choose any one of these licenses and use, modify, and distribute the project under its terms.
前言
欢迎阅读《The X Programming Language》!
X 语言是一门现代、通用的编程语言,设计目标是结合多种编程范式的优点,同时保持简单、安全和高效。X 语言深受 Rust、Haskell、Python 和 JavaScript 等语言的影响,但也有自己独特的设计理念。
为什么创造 X 语言?
我们创建 X 语言是为了满足以下目标:
-
可读性第一:代码应该像散文一样可读,宁可多打几个字符也不牺牲可读性。这就是为什么我们使用完整的英文关键字(如
function而非fn,let mutable而非var)。 -
类型安全:编译通过就不应出现类型错误。X 语言没有
null,没有异常,所有错误都通过类型系统显式处理。 -
内存安全:采用 Perceus 编译时引用计数技术,无需垃圾回收(GC),无需手动内存管理,也不会有内存泄漏。
-
多范式融合:支持函数式、面向对象、过程式、声明式四种编程范式,开发者可以根据场景选择最适合的方式。
-
工具链完整:
x命令行工具 1:1 对标 Cargo,开箱即用。
本书的目标读者
本书假设你已经有一定的编程经验,但不要求你熟悉任何特定的编程语言。如果你是编程新手,X 语言的设计理念会帮助你养成良好的编程习惯;如果你是有经验的开发者,X 语言会为你提供一套新的工具来解决问题。
如何使用本书
本书的结构是按照从简单到复杂的顺序组织的。建议你按顺序阅读,因为后面的章节会依赖前面介绍的概念。每章末尾可能会有一些练习,帮助你巩固所学内容。
- 第 1 章:介绍如何安装 X 语言,编写你的第一个程序。
- 第 2 章:讲解变量、数据类型、函数和控制流等基础概念。
- 第 3 章:深入理解 X 语言的内存管理模型——Perceus 引用计数。
- 第 4-9 章:介绍结构体、枚举、模块、集合、错误处理、泛型、Trait、类等核心特性。
- 第 10 章:探索函数式编程特性。
- 第 11 章:学习如何编写和组织测试。
- 第 12 章:了解 X 语言的标准库。
- 第 13 章:介绍高级特性。
- 附录:包含参考资料和补充信息。
致谢
感谢所有为 X 语言做出贡献的开发者,以及所有尝试使用 X 语言的用户。特别感谢 Rust 社区,X 语言从 Rust 中学到了很多。
现在,让我们开始 X 语言之旅!
X 语言文档
引言
什么是 X 语言
X 是一门现代的、通用的编程语言,适用于从底层系统编程到上层应用开发的全栈场景。它的设计融合了多种编程范式的优点,旨在提供一种既安全又高效的开发体验。
X 语言的核心特性包括:
- 可读性第一:所有关键字使用英文全称(
function、mutable、match、implement),代码读起来像清晰的英文散文 - 类型安全:Hindley-Milner 类型推断、代数数据类型、穷尽模式匹配——编译通过即无类型错误
- 无 null、无异常:用
Option<T>代替 null,用Result<T, E>代替异常,?运算符传播错误,编译器强制处理所有路径 - 内存安全:Perceus 编译时引用计数——无 GC 停顿、无手动管理、无生命周期标注,重用分析让函数式代码零分配原地更新
- 多范式:函数式(纯函数、管道
|>、模式匹配)、面向对象(类、继承、trait)、过程式(let mutable、循环)、声明式(where/sort by)
X 不是领域特定语言(DSL),而是能覆盖从底层系统到上层应用的全栈语言,适用于:
- 系统编程(OS、嵌入式、驱动)
- 应用开发(桌面、服务端、CLI 工具)
- 高性能计算(科学计算、数据处理)
- Web 后端与基础设施
X 语言的设计哲学
X 语言的设计由 17 条核心原则驱动,其中可读性第一具有最高优先级——当其他原则与可读性冲突时,可读性胜出。
核心设计原则
- 通用性:一门语言,从系统到应用
- 类型安全:编译通过 ≈ 无类型错误
- 内存安全:Perceus:无 GC、无手动管理、无泄漏
- 多范式:FP + OOP + 过程式 + 声明式,按需选择
- 博采众长:站在 Python/Rust/Go/Kotlin/Haskell/… 的肩膀上
- HM 推断:少写类型,多靠推断
- 默认不可变:
let不可变,let mutable可变;值/引用小写/大写;const编译期常量 - 多种并发:goroutine + Actor + async/await,按需选择
- 完整工具链:
xCLI 对标 Cargo - 效果系统:副作用在类型中可见(
with) - C FFI:与 C 零开销互操作
- 多后端:C / LLVM / JS / JVM / .NET,一次编写,多处运行
- 无异常:
Option+Result+?,错误即值 - 关键字全称:英文全称,含义自明,不缩写
- 可读性第一:代码应像散文一样可读,最高优先级
- 不使用奇怪的符号:只使用常见的、含义直观的符号
- AI 友好与预编译补全:语言与工具链应对机器可读、语义明确
设计准则
- 类型安全:如果编译通过,程序就不应出现类型错误。
- 内存安全:安全的内存管理不应以牺牲性能或开发体验为代价。
- 多范式:为问题选择最合适的范式,而非强迫开发者适应单一范式。
- 无异常:可能失败的操作必须在返回类型中体现,由编译器保证处理。
- 可读性:宁可多打几个字符,也不要牺牲可读性。写代码花一次时间,读代码花一百次。
- 符号使用:符号应「看一眼就懂」;需要解释时就用英文单词代替。
快速开始
X 语言提供了简洁的命令行工具,让你可以快速运行、检查和编译 X 语言程序。
运行 .x 文件
cd tools/x-cli && cargo run -- run <file.x>
例如,运行经典的 “Hello, World!” 程序:
cd tools/x-cli && cargo run -- run ../../examples/hello.x
检查语法与类型
cd tools/x-cli && cargo run -- check <file.x>
编译为可执行文件(C 后端)
cd tools/x-cli && cargo run -- compile <file.x> -o <output>
语言特性示例
X 语言支持两种编程风格:
-
脚本风格(推荐用于简单程序):
println("Hello, World!") -
传统风格(推荐用于复杂程序):
function main() { println("Hello, World!") } main()
安装与设置
X 语言的开发环境设置非常简单,只需按照以下步骤操作:
1. 克隆仓库
git clone <repository-url>
cd x-lang
2. 构建工具链
X 语言的工具链使用 Rust 开发,因此需要先安装 Rust:
- 访问 rust-lang.org 下载并安装 Rust
- 验证安装:
rustc --version cargo --version
然后构建 X 语言工具链:
cd tools/x-cli
cargo build
3. 运行测试
为了确保安装正确,可以运行测试:
cd ../../ # 回到项目根目录
cargo test
4. 环境变量(可选)
为了方便使用,可以将 tools/x-cli/target/debug 添加到系统 PATH 环境变量中,这样就可以直接使用 x 命令:
# Windows PowerShell
$env:PATH += ";C:\path\to\x-lang\tools\x-cli\target\debug"
# Linux/macOS
export PATH="$PATH:/path/to/x-lang/tools/x-cli/target/debug"
5. 依赖项
- C 后端:需要安装 GCC/Clang/MSVC 等 C 编译器
- LLVM 后端:需要安装 LLVM 15+(可选)
详细的依赖项说明和安装指南请参考 CLAUDE.md 文件。
下一步
现在你已经了解了 X 语言的基本概念和设置方法,可以开始探索以下内容:
- 语言规格:查看 spec/README.md 了解 X 语言的正式语法与语义定义
- 示例程序:浏览 examples/ 目录中的示例,学习 X 语言的各种特性
- 标准库:查看 library/stdlib/README.md 了解 X 语言的标准库功能
- 编译器架构:探索 compiler/ 目录,了解 X 语言编译器的实现细节
X 语言旨在提供一种既安全又高效的编程体验,希望你能在使用过程中感受到它的设计之美。
安装 X
在我们开始编写 X 语言代码之前,首先需要安装 X 语言工具链。
前置要求
X 语言编译器是用 Rust 编写的,因此你需要先安装 Rust。你可以通过以下命令安装 Rust:
在 Linux 或 macOS 上
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
在 Windows 上
访问 https://rustup.rs 并下载安装程序。
安装完成后,你可以通过以下命令验证 Rust 是否安装成功:
rustc --version
cargo --version
安装 X 语言
从源码安装
首先,克隆 X 语言的仓库:
git clone https://github.com/your-username/x-lang.git
cd x-lang
然后构建并安装:
cargo build --release
构建完成后,你可以在 target/release/ 目录下找到 x 可执行文件。建议将其添加到你的 PATH 环境变量中。
验证安装
安装完成后,运行以下命令验证 X 语言是否安装成功:
x --version
如果看到版本号输出,说明安装成功!
常见问题
在 macOS 上遇到 “command not found”
确保你已经将 x 可执行文件所在的目录添加到 PATH 中。你可以在 ~/.bash_profile 或 ~/.zshrc 中添加:
export PATH="$PATH:/path/to/x-lang/target/release"
在 Windows 上遇到权限问题
以管理员身份运行命令提示符或 PowerShell。
下一步
现在你已经成功安装了 X 语言,让我们在下一节中编写你的第一个 “Hello, World!” 程序!
Hello, World!
现在你已经安装了 X 语言,让我们编写第一个程序!按照传统,我们将从 “Hello, World!” 开始。
创建项目目录
首先,让我们创建一个目录来存放 X 语言代码。X 语言不关心你的代码在哪里,但对于本书的练习,我们建议在你的主目录下创建一个 x-projects 目录:
mkdir ~/x-projects
cd ~/x-projects
编写并运行 X 程序
接下来,创建一个名为 hello.x 的文件:
touch hello.x
使用你喜欢的文本编辑器打开这个文件,并输入以下代码:
// Hello, World!
println("Hello, World!")
保存文件,然后在终端中运行以下命令:
x run hello.x
你应该会看到输出:
Hello, World!
恭喜!你已经成功编写并运行了你的第一个 X 程序!
分析这个程序
让我们逐行看看这个程序做了什么:
// Hello, World!
这是一行注释。X 语言使用 // 来表示单行注释。注释会被编译器忽略,它们是为了给人类读者看的。
println("Hello, World!")
这一行调用了内置函数 println,它将括号内的内容输出到标准输出并换行。这里我们传递了字符串字面量 "Hello, World!"。
注意,在 X 语言中,语句不需要分号结尾(虽然如果你写了分号也不会报错)。
main 函数(可选)
在上面的例子中,我们直接编写了代码,没有使用 main 函数。这是 X 语言的一个便利特性:main 函数不是必须的。你可以像 Swift 一样,直接在文件顶层编写代码。
不过,对于更复杂的程序,或者当你需要更好地控制程序入口时,你仍然可以使用 main 函数:
// Hello, World! - 使用 main 函数的版本
function main() {
println("Hello, World!")
}
这个版本与之前的版本功能完全相同。选择哪种方式取决于你的个人偏好和程序的复杂度。
编译与运行
当你运行 x run hello.x 时,X 工具链会:
- 解析
hello.x文件 - 检查语法和类型
- 通过解释器执行程序
如果你想查看程序编译后的中间表示,可以使用:
x compile hello.x --emit ast
这会输出程序的抽象语法树(AST)。
练习
- 尝试修改
hello.x程序,让它打印 “Hello, X!” 而不是 “Hello, World!”。 - 尝试添加另一个
print调用,让程序打印两行内容。 - 尝试创建两个版本的程序:一个使用
main函数,一个不使用,验证它们的行为是相同的。
下一步
现在你已经掌握了基础,让我们在下一节中编写一个更复杂的程序——一个猜数字游戏!
编写一个简单的程序
让我们通过编写一个猜数字游戏来练习 X 语言。这个程序会:
- 生成一个 1 到 100 之间的随机数
- 让玩家输入一个猜测
- 告诉玩家猜测是太大了还是太小了
- 如果猜对了,打印庆祝消息并退出
注意:这个示例使用了一些我们还没介绍的特性,没关系,我们只是想给你一个 X 语言能做什么的感觉。我们会在后续章节中详细解释这些特性。
创建一个新项目
创建一个名为 guessing_game.x 的文件:
// 猜数字游戏
function main() {
println("猜数字游戏!")
println("请猜一个 1 到 100 之间的数字。")
// 生成随机数 (简化实现)
let secret = 42 // 实际应该使用随机数生成器
let mutable guesses = 0
while true {
print("请输入你的猜测: ")
// 简化:直接赋值
let guess = 50 // 实际应该从标准输入读取
guesses = guesses + 1
if guess < secret {
println("太小了!")
} else if guess > secret {
println("太大了!")
} else {
println("恭喜你,猜对了!")
println("你猜了 ", guesses, " 次。")
break
}
}
}
这是一个简化版本,实际的程序需要从标准输入读取用户输入。让我们运行它看看:
x run guessing_game.x
真实版本(使用内置函数)
让我们使用 X 语言的内置函数来创建一个更真实的版本。首先,让我们看看实际工作中会使用哪些特性:
// 猜数字游戏 - 完整版
function main() {
println("猜数字游戏!")
println("请猜一个 1 到 100 之间的数字。")
// 生成 1 到 100 之间的随机数
let secret = 42 // 实际应使用随机数生成器
let mutable guesses = 0
while true {
print("请输入你的猜测: ")
// 注意:实际需要从标准输入读取
// 这里我们用一个固定值作为示例
let guess = 50
guesses = guesses + 1
if guess < secret {
println("太小了!")
} else if guess > secret {
println("太大了!")
} else {
println("恭喜你,猜对了!")
println("你猜了 ", guesses, " 次。")
break
}
}
}
这个程序使用的特性
这个简单的程序展示了 X 语言的多个核心特性:
- 函数:使用
function关键字 - 变量:使用
let声明不可变变量 - 可变变量:使用
let mutable声明可变变量 - 循环:使用
while循环 - 条件语句:使用
if和else if - 打印:使用
print和println - 中断:使用
break退出循环
下一步
在接下来的章节中,我们将详细介绍这些概念。让我们从变量和数据类型开始!
第2章 语言基础
X 语言的设计理念是“可读性第一“,同时兼顾安全性和表达力。本章介绍 X 语言的基础概念,包括基本语法、变量与绑定、数据类型、运算符和控制流。
2.1 基本语法
2.1.1 词法结构
X 语言使用 Unicode 字符集,源文件必须使用 UTF-8 编码。代码由词法记号(token)组成,包括关键字、标识符、字面量、运算符和标点符号。
关键字
X 语言使用英文全称作为关键字,避免缩写以提高可读性:
| 类别 | 关键字 |
|---|---|
| 声明 | let, mutable, function, async, class, trait, type, module, const |
| 控制流 | if, else, for, in, while, return, match, break, continue |
| 效果 | needs, given, await, with, together, race, atomic, retry |
| 字面量 | true, false, None, Some, Ok, Err |
| 修饰符 | public, private, protected, internal, static, abstract, final, override, virtual |
| 其他 | import, export, with, where, and, or, not, is, as, weak, implement, extends, new, this, super |
标识符
标识符用于命名变量、函数、类型等程序实体:
- 以 Unicode 字母或下划线
_开头 - 可包含字母、数字和连字符
- - 大小写敏感
- 不能与关键字同名
推荐的命名约定:
- 变量、函数、参数:
snake_case或kebab-case - 类型、类、trait:
PascalCase - 常量:
SCREAMING_SNAKE_CASE - 模块:小写点分隔
注释
X 语言支持三种注释形式:
- 单行注释:
// 注释内容 - 多行注释:
/** 注释内容 */(支持嵌套) - 文档注释:
/// 注释内容或/*** 注释内容 */
// 这是单行注释
/**
* 这是多行注释
* 可以跨行
*/
/// 这是文档注释
/// 用于生成 API 文档
function add(a: Integer, b: Integer) -> Integer {
a + b
}
2.1.2 程序结构
X 语言的程序由模块组成,每个模块包含声明和表达式。程序的执行从 main 函数开始(如果存在)。
// 简单的 X 程序
function main() {
println("Hello, X!")
}
2.2 变量与绑定
X 语言使用 let 关键字声明变量,默认创建不可变绑定。要创建可变绑定,使用 let mutable。
2.2.1 不可变绑定
不可变绑定一旦创建,其值不能被修改:
let name = "Alice" // 类型推断为 String
let age: Integer = 30 // 显式类型注解
let pi = 3.14159 // 类型推断为 Float
let is_valid = true // 类型推断为 Boolean
let numbers = [1, 2, 3] // 类型推断为 [Integer]
2.2.2 可变绑定
可变绑定允许后续修改其值:
let mutable count = 0 // 可变 Integer
let mutable name: String = "Bob" // 可变 String,显式注解
let mutable items: [Integer] = [] // 可变列表
// 修改可变绑定
count += 1
name = "Charlie"
items.push(42)
2.2.3 解构绑定
let 绑定支持模式解构,可以从复杂类型中提取值:
// 元组解构
let (x, y) = get_position()
// 记录解构
let { name, age } = get_person()
// 列表解构
let [first, second, ..rest] = numbers
// 可变解构
let mutable (a, b) = (1, 2)
a += 1
2.2.4 作用域与遮蔽
X 语言使用词法作用域,变量的可见性由其在源码中的位置决定:
let x = 10
function foo() -> Integer {
x // 可以访问外层的 x
}
{
let y = 20 // y 仅在此块内可见
println(x + y) // 可以访问外层的 x
}
// println(y) // 编译错误:y 不在作用域内
内部作用域可以声明与外部作用域同名的变量,遮蔽外部变量:
let x = 10
{
let x = 20 // 遮蔽外层的 x
println(x) // 输出 20
}
println(x) // 输出 10(外层 x 未被修改)
let value = "hello"
let value = value.length() // 允许:用新类型遮蔽旧绑定
2.3 数据类型
X 语言拥有完整的类型系统,基于 Hindley-Milner 类型推断与代数数据类型。
2.3.1 基本类型
| 类型 | 描述 | 示例 |
|---|---|---|
integer | 有符号整数(默认机型友好大小) | 42, 0xFF, 0b1010 |
integer n | 有符号整数(指定位宽) | integer 32, integer 64 |
unsigned integer n | 无符号整数(指定位宽) | unsigned integer 32 |
float | 双精度浮点数(默认) | 3.14159, 1.0e-10 |
float n | 浮点数(指定位宽) | float 32, float 64 |
boolean | 布尔值 | true, false |
string | UTF-8 字符串 | "Hello", "${name}" |
character | Unicode 字符 | 'A', '🎉' |
unit | 单位类型(无值) | () |
never | 永无类型(无返回) | - |
2.3.2 复合类型
列表(List)
同构元素的有序集合:
let numbers: [Integer] = [1, 2, 3, 4, 5]
let names = ["Alice", "Bob"] // [String]
let empty: [Float] = []
字典(Dictionary)
键值对集合:
let ages: {String: Integer} = {"Alice": 30, "Bob": 25}
let config: {String: String} = {"host": "localhost", "port": "8080"}
元组(Tuple)
固定长度、异构类型的有序集合:
let pair: (Integer, String) = (42, "answer")
let triple: (Float, Float, Float) = (1.0, 2.0, 3.0)
记录(Record)
具名字段的积类型:
type Point = {
x: Float,
y: Float
}
type Person = {
name: String,
age: Integer,
email: String
}
let origin: Point = { x: 0.0, y: 0.0 }
let alice = Person { name: "Alice", age: 30, email: "alice@example.com" }
使用 with 语法从现有记录创建新记录:
let p1 = Point { x: 1.0, y: 2.0 }
let p2 = p1 with { x: 5.0 } // p2 = { x: 5.0, y: 2.0 }
联合类型(Union)
和类型,使用 type 关键字和 | 定义:
type Shape =
| Circle { radius: Float }
| Rect { width: Float, height: Float }
| Point
type Color =
| Red
| Green
| Blue
| Custom(Integer, Integer, Integer)
Option 类型
表示可能缺失的值,替代 null:
// Option<T> = Some(T) | None
function find(users: [User], id: Integer) -> Option<User> {
users |> filter(function(u) => u.id == id) |> first
}
let user = find(users, 42)
match user {
Some(u) => println("Found: ${u.name}")
None => println("Not found")
}
Result 类型
表示可能失败的操作,替代异常:
// Result<T, E> = Ok(T) | Err(E)
function read_file(path: String) -> Result<String, IoError> {
if not exists(path) {
return Err(IoError.NotFound(path))
}
Ok(read_bytes(path).decode())
}
match read_file("config.toml") {
Ok(content) => parse_config(content)
Err(e) => use_default()
}
2.4 运算符
X 语言提供丰富的运算符,按优先级从高到低排列:
| 优先级 | 运算符 | 描述 | 示例 |
|---|---|---|---|
| 1 | . () [] | 成员访问、函数调用、索引 | user.name, add(1, 2), arr[0] |
| 2 | - not ~ | 一元取负、逻辑非、位取反 | -42, not true, ~0xFF |
| 3 | * / % | 乘法、除法、取模 | a * b, a / b, a % b |
| 4 | + - | 加法、减法 | a + b, a - b |
| 5 | << >> | 位左移、位右移 | a << 2, a >> 2 |
| 6 | & | 位与 | a & b |
| 7 | ^ | 位异或 | a ^ b |
| 8 | ` | ` | 位或 |
| 9 | .. ..= | 范围(左闭右开、左闭右闭) | 0..10, 1..=100 |
| 10 | < > <= >= is as | 比较、类型检查、类型转换 | a < b, x is Integer, x as Float |
| 11 | == != | 相等、不等 | a == b, a != b |
| 12 | and | 逻辑与(短路求值) | a and b |
| 13 | or | 逻辑或(短路求值) | a or b |
| 14 | ?? ?. | 默认值、可选链 | x ?? 0, user?.name |
| 15 | ` | >` | 管道 |
| 16 | = += -= *= /= %= ^= | 赋值、复合赋值 | x = 42, x += 1 |
2.4.1 逻辑运算符
X 使用英文关键字作为逻辑运算符,均支持短路求值:
and:逻辑与or:逻辑或not:逻辑非
let both = is_valid and is_active
let either = is_admin or has_permission
let inverted = not is_valid
2.4.2 错误处理运算符
X 提供三个特殊运算符用于错误处理:
?:错误传播(自动返回Err或None)?.:可选链(安全访问Option内部成员)??:默认值(Option为None时提供默认值)
// 错误传播
function load_config() -> Result<Config, IoError> {
let content = read_file("config.toml")?
let parsed = parse_toml(content)?
Ok(parsed)
}
// 可选链
let name = user?.name // Option<String>
let city = user?.address?.city // Option<String>(链式)
// 默认值
let name = user?.name ?? "anonymous"
let port = config.get("port")?.parse_integer() ?? 8080
2.4.3 管道运算符
管道运算符 |> 将左侧表达式的结果作为右侧函数的第一个参数传入,使代码更具可读性:
let result = [1, 2, 3, 4, 5]
|> filter(function(n) => n % 2 == 0)
|> map(function(n) => n * n)
|> sum
let processed = raw_data
|> parse
|> validate
|> transform
|> serialize
2.5 控制流
2.5.1 条件语句
if 语句在 X 中是表达式,具有值:
let max = if a > b { a } else { b }
let category = if age < 13 {
"child"
} else if age < 18 {
"teen"
} else {
"adult"
}
2.5.2 循环语句
While 循环
let mutable i = 0
while i < 10 {
println("i = ${i}")
i += 1
}
For 循环
for item in items {
println(item)
}
for i in 0..10 {
println("index: ${i}")
}
for (key, value) in dictionary {
println("${key} = ${value}")
}
循环控制
break:立即退出最内层循环continue:跳过当前迭代的剩余部分,进入下一次迭代
for i in 0..100 {
if i % 2 == 0 {
continue
}
if i > 50 {
break
}
println(i)
}
2.5.3 模式匹配
match 语句用于模式匹配,支持穷尽性检查:
match command {
"quit" => {
println("Goodbye!")
exit(0)
}
"help" => show_help()
"version" => println("v1.0.0")
_ => println("Unknown command: ${command}")
}
match shape {
Circle { radius } => {
let area = 3.14159 * radius * radius
println("Circle area: ${area}")
}
Rect { width, height } => {
let area = width * height
println("Rectangle area: ${area}")
}
Point => println("Point has no area")
}
match score {
n if n >= 90 => "A"
n if n >= 80 => "B"
n if n >= 70 => "C"
n if n >= 60 => "D"
_ => "F"
}
2.5.4 返回语句
return 语句用于从函数返回:
function find_index(items: [String], target: String) -> Option<Integer> {
for i in 0..items.length() {
if items[i] == target {
return Some(i)
}
}
None
}
function greet(name: String) {
if name.is_empty() {
return
}
println("Hello, ${name}!")
}
2.6 表达式与语句
在 X 语言中,大部分构造都是表达式,具有返回值:
- 条件表达式:
if e1 { e2 } else { e3 } - 匹配表达式:
match e { p1 => e1, p2 => e2, ... } - 块表达式:
{ e1; e2; ...; en }(值为最后一个表达式) - 函数调用:
f(e1, e2, ...) - 二元运算:
a + b,a == b,a and b
语句是不产生值或值被丢弃的构造:
- 声明语句:
let x = e,let mutable x = e - 赋值语句:
x = e,x += e - 表达式语句:
e;(值被丢弃) - 循环语句:
while e { b },for p in e { b } - 返回语句:
return e
这种设计使得 X 语言更加简洁和表达力强,同时保持了代码的可读性。
变量与可变性
在 X 语言中,变量默认是不可变的。这是 X 语言鼓励你编写更安全、更符合函数式风格代码的方式之一。不过你仍然可以使用可变变量。让我们探讨一下为什么以及如何 X 语言鼓励你拥抱不可变性,以及何时你可能想要选择可变性。
变量
当变量不可变时,一旦值被绑定到一个名称上,你就不能改变这个值。作为演示,让我们在 ~/x-projects 目录创建一个名为 variables.x 的新项目:
let x = 5
println("x 的值是: ", x)
// 错误:x 是不可变的
// x = 6
// println("x 的值是: ", x)
保存并使用 x run variables.x 运行程序。你应该会看到一条关于不可变性错误的消息(如果你取消了注释的话)。
这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误可能令人沮丧,但它们只是意味着你的程序不能安全地做你想让它做的事情;它们并不意味着你不是一个好程序员!
注意,在这个例子中,我们没有使用 main 函数——直接在文件顶层编写代码也是可以的!
可变性
我们可以通过在变量名前添加 mutable 来使变量可变。除了允许改变值之外,它还向读者传达了代码的其他部分将会改变这个变量的值的意图。
让我们修改 variables.x:
let mutable x = 5
println("x 的值是: ", x)
x = 6
println("x 的值是: ", x)
现在运行程序,我们看到:
x 的值是: 5
x 的值是: 6
使用 mutable,我们被允许将 x 绑定的值从 5 改为 6。在某些情况下,你会想要使变量可变,因为这会使代码更容易编写。
常量
与不可变变量类似,常量是绑定到一个名称且不允许改变的值,但常量和变量之间有一些区别:
- 你不允许对常量使用
mutable。 - 你使用
const关键字而不是let。 - 你必须标注类型。
- 你只能设置常量为常量表达式,而不能是函数调用的结果或任何只能在运行时计算的值。
这是一个常量声明的例子:
const MAX-RETRY-COUNT: integer = 3
const APP-NAME: string = "x-lang"
X 语言的常量命名约定是使用连字符(kebab-case)分隔单词,因为连字符比下划线更易读。
遮蔽
你可以声明一个与之前的变量同名的新变量,我们说第一个变量被第二个变量遮蔽了,这意味着第二个变量的值是你使用该名称时会看到的。你可以通过使用相同的变量名并重复使用 let 关键字来遮蔽变量:
let x = 5
let x = x + 1
let x = x * 2
println("x 的值是: ", x)
这个程序首先将 x 绑定到值 5。然后通过 let x = 遮蔽 x,获取原始值并加 1,所以 x 的值变成了 6。第三个 let 语句也遮蔽了 x,将之前的值乘以 2,所以 x 的最终值是 12。
遮蔽与将变量标记为 mutable 是不同的,因为如果我们不小心尝试重新赋值给这个变量而没有使用 let 关键字,我们会得到一个编译错误。通过使用 let,我们可以对值执行一些转换,但在这些转换完成之后,变量是不可变的。
mutable 和遮蔽的另一个区别是,因为我们在再次使用 let 关键字时实际上创建了一个新变量,所以我们可以改变值的类型,同时重用相同的名称。
总结
我们已经探讨了 X 语言中变量的工作方式。让我们继续学习数据类型!
数据类型
在 X 语言中,每个值都属于某种数据类型,这告诉 X 语言正在指定哪种类型的数据,以便它知道如何处理这些数据。我们将介绍两种数据类型子集:基本类型和复合类型。
在继续之前,请记住 X 语言是静态类型语言,这意味着它必须在编译时知道所有变量的类型。通常编译器可以根据值及其使用方式推断出我们想要使用的类型。
基本类型
基本类型是由语言核心定义的简单类型。X 语言有以下基本类型:
整数类型
整数是没有小数部分的数字。X 语言中的基础整数类型对外有一对名称:
integer:值类型(primitive),用于绝大多数计算场景Integer:引用类型(boxed),在需要以对象形式存在(如放入统一的对象容器)时使用
抽象上表示数学意义上的整数(…,-2,-1,0,1,2,…),规格上定义为 任意精度整数:理论上只受内存限制,不会像传统 32/64 位整型那样静默溢出。
let a: integer = 42
let big: integer = 1_000_000_000_000_000
let sum: integer = a + big
let diff = big - a // 类型推断为 integer
固定位宽整数:完整英文短语形式
在需要与底层平台或其他语言精确对齐时,X 也提供了 固定位宽整数类型的内置别名,并且名称使用完整英文短语 + 空格,避免 i8 / u64 这类缩写和符号化命名:
- 有符号:如
signed 8bit integersigned 16bit integersigned 32bit integersigned 64bit integersigned 128bit integer
- 无符号:如
unsigned 8bit integerunsigned 16bit integerunsigned 32bit integerunsigned 64bit integerunsigned 128bit integer
示例:
let small: signed 8bit integer = 127
let port: unsigned 16bit integer = 8080
let size: unsigned 64bit integer = 1_000_000_000
let mask: unsigned 128bit integer = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF
整数可以写成以下形式:
| 数字字面量 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xff |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
注意:你可以使用下划线 _ 作为分隔符以便于阅读,例如 1_000_000。
浮点类型
X 语言中的基础浮点类型也有一对名称:
float:值类型(primitive),默认对应 64 位双精度Float:引用类型(boxed),用于需要对象语义的场合
默认对应 双精度浮点数(与大多数现代语言的 double 类似),用于近似实数计算:物理量、评分、概率、统计等。
let pi: float = 3.1415926535
let radius: float = 2.5
let area: float = pi * radius ^ 2
固定位宽浮点:32bit float / 64bit float 与十进制 decimal
类似整数,X 为浮点数提供了使用简洁完整短语的内置别名(值类型),分为二进制浮点和十进制浮点两类:
- 二进制浮点
32bit float:对应 32 位单精度64bit float:对应 64 位双精度
- 十进制浮点
32bit decimal:32 位十进制浮点64bit decimal:64 位十进制浮点128bit decimal:128 位十进制浮点,适合高精度金融 / 结算场景
示例:
let x: 32bit float = 1.0
let y: 64bit float = 3.1415926535
let price: 64bit decimal = 123.45
let amount: 128bit decimal = 1_000_000_000_000.0001
布尔类型
语义
布尔类型为 boolean,只有两个字面量:
truefalse
let is-active: boolean = true
let has-error: boolean = false
if is-active and not has-error {
start()
}
与所有主流语言保持一致,禁止 以 0 / 1 充当真假,减少隐式转换带来的歧义。配合 not / and / or 这些关键字式逻辑运算符,让布尔表达式读起来更接近自然语言。
字符类型
语义
字符类型分为:
character:值类型,代表单个 Unicode 字符Character:引用类型,用于需要对象封装时
let ch: character = '中'
let letter: character = 'A'
可用于:
- 解析文本、编写词法分析器
- 单字符处理(如分类、过滤)
注意:X 语言使用单引号 ' 来表示字符字面量,使用双引号 " 来表示字符串字面量。
字符串类型
语义
字符串类型分为:
-
string:值类型,用于绝大多数文本数据场景 -
String:引用类型,提供面向对象的字符串接口 -
普通字符串:
"..."(支持转义) -
多行字符串:
""" ... """(保留缩进和换行) -
插值字符串:
"Hello, {name}!"
let greeting: string = "Hello, X"
let multi = """
多行字符串
保留格式
"""
let name: string = "Alice"
let msg: string = "Hello, {name}!" // 插值
字符串可以包含任何 Unicode 字符,包括表情符号。我们将在后面的章节中详细讨论字符串。
Unit 类型
Unit 类型是一个特殊类型,它只有一个值:Unit(有时也写为 ())。当没有其他有意义的值可以返回时,Unit 类型用作函数的返回类型。
function do_something() {
print("做了一些事情")
// 隐式返回 Unit
}
Never 类型
Never 类型(Never)是一个永远不会有任何值的类型。它用于表示永远不会返回的表达式(例如 panic 调用或无限循环)。
复合类型
复合类型可以将多个值组合成一个类型。X 语言有两种基本的复合类型:数组/列表和记录。
列表类型
列表是一个值的集合,所有值都具有相同的类型。X 语言使用 List<T> 表示列表类型,其中 T 是元素的类型。列表类型也有一个简写语法 [T]。
let a = [1, 2, 3, 4, 5]
let months = ["一月", "二月", "三月", "四月", "五月", "六月",
"七月", "八月", "九月", "十月", "十一月", "十二月"]
你可以通过索引访问列表元素:
let first = a[0]
let second = a[1]
我们将在第 6 章中详细讨论列表。
记录类型
记录类型可以将多个具有不同类型的值组合成一个类型。记录使用字段名来标识每个值:
type Point = {
x: float,
y: float
}
let p = { x: 0.0, y: 0.0 }
我们将在第 4 章中详细讨论记录和结构体。
类型注解
虽然 X 语言通常可以推断类型,但你也可以显式注解类型:
let guess: integer = 42
let price: float = 3.99
let is_active: boolean = true
let initial: character = 'A'
let message: string = "Hello"
总结
我们已经介绍了 X 语言的基本数据类型。让我们继续学习函数!
函数
函数在 X 语言中无处不在。虽然 main 函数是许多程序的入口点,但请记住,在 X 语言中 main 函数不是必须的——你可以直接在文件顶层编写代码!你也见过 function 关键字,它用于声明一个新函数。
X 语言代码使用蛇形命名法(snake_case)作为函数和变量名的规范风格。在蛇形命名法中,所有字母都是小写的,并使用下划线分隔单词。
定义函数
让我们编写一个调用另一个函数的程序,不使用 main 函数:
println("Hello, world!")
another_function()
function another_function() {
println("另一个函数。")
}
在 X 语言中,我们通过输入 function 关键字,后跟函数名和一组圆括号来定义函数。大括号告诉编译器函数体从哪里开始和结束。
我们可以通过使用函数名后跟一组圆括号来调用我们已定义的任何函数。请注意,我们在源文件中先定义了 another_function 后才调用它;我们也可以先调用后定义。X 语言不关心你在哪里定义函数,只关心它们在调用者可以看到的作用域中的某个地方被定义。
让我们创建一个名为 functions.x 的新文件,并把上面的代码放进去。运行它,你应该会看到以下输出:
Hello, world!
另一个函数。
函数参数
函数也可以被定义为具有参数,参数是作为函数签名一部分的特殊变量。当一个函数有参数时,你可以在调用该函数时为这些参数提供具体值。从技术上讲,具体值被称为参数,但在日常对话中,人们倾向于将参数和参数这两个词互换使用,用于函数定义中的变量或调用函数时传入的具体值。
在这个版本的 another_function 中,我们添加了一个参数:
another_function(5)
function another_function(x: integer) {
println("x 的值是: ", x)
}
尝试运行这个程序;你应该会得到以下输出:
x 的值是: 5
让我们仔细看看 another_function 定义的这一部分。首先,我们有一个参数 x。在参数名后面,我们必须添加一个冒号 :,然后是参数的类型。在 X 语言中,必须在函数签名中声明每个参数的类型,这是经过深思熟虑的设计决策:在函数定义中要求类型注解意味着编译器几乎从不需要你在代码的其他地方使用它们来推断你的意思。
当一个函数有多个参数时,用逗号分隔它们,如下所示:
print_labeled_measurement(5, 'h')
function print_labeled_measurement(value: integer, unit_label: character) {
println("测量值是: ", value, unit_label)
}
这个例子创建了一个名为 print_labeled_measurement 的函数,它有两个参数。第一个参数名为 value,类型是 integer。第二个名为 unit_label,类型是 character。然后函数打印包含这两个值的文本。
让我们尝试运行这段代码。用上面的例子替换 functions.x 文件中的程序,然后用 x run functions.x 运行它:
测量值是: 5h
包含语句和表达式的函数体
函数体由一系列语句组成,可选地以一个表达式结束。到目前为止,我们只看到了没有结束表达式的函数,但你已经将表达式作为语句的一部分看到了。
语句是执行某些操作但不返回值的指令。表达式计算结果为一个值。让我们看几个例子:
let x = 5是一个语句5 + 6是一个计算为11的表达式- 调用函数是一个表达式
- 调用宏是一个表达式
- 我们用来创建新作用域的块
{ }是一个表达式
例如:
let y = {
let x = 3
x + 1
}
println("y 的值是: ", y)
块 { let x = 3; x + 1 } 是一个计算为 4 的表达式。然后该值被绑定到 y 作为 let 语句的一部分。请注意,x + 1 行末尾没有分号,这与你到目前为止看到的大多数行不同。表达式不包含结尾分号。如果在表达式末尾添加分号,则会将其变成语句,然后它将不会返回值。在探索函数返回值和表达式时请记住这一点。
带有返回值的函数
函数可以向调用它们的代码返回值。我们不给返回值命名,但我们必须在箭头 -> 之后声明它们的类型。在 X 语言中,函数的返回值与函数体块中的最终表达式的值同义。你可以使用 return 关键字并指定一个值从函数提前返回,但大多数函数隐式返回最后的表达式。这是一个返回值的函数的示例:
function five() -> integer {
5
}
let x = five()
println("x 的值是: ", x)
让我们更仔细地研究一下这段代码。首先,看一下 five 函数:它以 -> integer 结尾,这意味着它将返回一个 integer 值。请注意,我们也可以在函数体的最后一行使用 return 5,但 5 本身作为表达式也是可以的。
let x = five(); 这一行表示我们将使用函数的返回值来初始化变量 x。因为 five 返回 5,所以这一行与 let x = 5; 相同。
让我们运行这段代码;输出应该如下所示:
x 的值是: 5
这是另一个例子:
function plus_one(x: integer) -> integer {
x + 1
}
let x = plus_one(5)
println("x 的值是: ", x)
运行这段代码将打印 x 的值是: 6。但是如果我们在包含 x + 1 的行末尾放置一个分号,将其从表达式改为语句,我们会得到一个错误:
function plus_one(x: integer) -> integer {
x + 1; // 错误:这里需要表达式,不能有分号
}
let x = plus_one(5)
println("x 的值是: ", x)
这个错误说明函数期望返回一个 integer,但没有返回值。实际上,如果我们使用分号,函数会隐式返回 Unit(也写作 ())。
Unit 类型作为返回值
如果一个函数没有指定返回类型,它隐式返回 Unit 类型。Unit 是一个只有一个可能值的特殊类型,也写成 ()。当没有其他有意义的值可以返回时,就会使用 Unit。
示例:
function do_something() {
println("做了一些事情")
// 隐式返回 Unit
}
let result = do_something()
println("结果是: ", result) // 会打印 "结果是: Unit"
总结
函数是 X 语言编程的基础部分。让我们回顾一下关于函数的知识:
- 使用
function关键字定义函数 - 函数名使用蛇形命名法(snake_case)
- 必须为每个参数声明类型
- 使用
-> Type声明返回类型 - 函数体中的最后一个表达式是返回值
- 可以使用
return关键字提前返回 - 没有显式返回类型的函数返回
Unit main函数不是必须的——你可以直接在文件顶层编写代码
接下来,让我们看看注释!
注释
所有程序员都努力使他们的代码易于理解,但有时额外的解释是必要的。在这种情况下,程序员会在源代码中留下注释,编译器会忽略这些注释,但阅读源代码的人可能会发现它们有用。
这里是一个简单的注释:
// 你好,世界
在 X 语言中,注释的规范风格是使用两个斜杠开始注释,注释会持续到行尾。对于跨多行的注释,你需要在每一行都包含 //,如下所示:
// 所以我们在这里做一些复杂的事情,长到需要
// 多行注释来完成。希望这个注释能
// 解释发生了什么。
注释也可以放在包含代码的行的末尾:
function main() {
let lucky_number = 7 // 今天是我的幸运日
}
但是你不会经常看到这种格式,而是会看到注释单独一行,位于它所注释的代码上方:
function main() {
// 今天是我的幸运日
let lucky_number = 7
}
文档注释
X 语言还支持文档注释,这些注释会编译成 HTML 文档。文档注释使用三个斜杠 /// 而不是两个,并支持 Markdown 注解:
/// 将给定的数字加一
///
/// # 示例
///
/// ```
/// let five = 5
/// assert_eq!(6, add_one(5))
/// ```
function add_one(x: integer) -> integer {
x + 1
}
我们将在后面关于标准库的章节中更详细地讨论文档注释。
注释的最佳实践
关于注释,有几点需要记住:
-
好的注释解释为什么,而不是什么 - 代码本身应该解释它在做什么。注释应该解释为什么要以某种方式做某事。
-
保持注释最新 - 过时的注释比没有注释更糟糕!当你更改代码时,请务必更新相关注释。
-
使用清晰的语言 - 注释是写给人看的,所以确保它们清晰易懂。
-
不要过度注释 - 如果代码足够清晰,就不需要注释。
总结
注释是代码库中很有价值的一部分。X 语言提供了简单的 // 语法用于单行注释,以及 /// 用于文档注释。明智地使用它们来解释为什么,而不是什么。
现在,让我们谈谈控制流!
控制流
根据条件是真还是假,或者根据某个值多次运行某些代码,决定是否运行某些代码,这是大多数编程语言的基本组成部分。在 X 语言中控制执行流程的最常见构造是 if 表达式和循环。
if 表达式
if 表达式允许你根据条件分支你的代码。你提供一个条件,然后陈述:“如果满足这个条件,运行这段代码块。如果条件不满足,不要运行这段代码块。”
让我们创建一个名为 branches.x 的新文件来探索 if 表达式:
let number = 3
if number < 5 {
println("条件为真")
} else {
println("条件为假")
}
所有 if 表达式都以 if 关键字开头,后跟一个条件。在这种情况下,条件检查 number 的值是否小于 5。我们将要执行的代码块放在条件之后的大括号中,条件为真时执行。
可选地,我们还可以包含一个 else 表达式,如果条件为假,我们选择执行该表达式,我们在这里选择了这样做。或者,你可以将 else 块看作是给程序一个默认的代码块来执行。
当你运行这个程序时,你会看到:
条件为真
让我们试着把 number 的值改为使条件为假的值,看看会发生什么:
let number = 7
再次运行程序,并查看输出:
条件为假
还值得注意的是,此代码中的条件必须是 boolean 类型。如果条件不是 boolean,我们会得到一个错误。让我们尝试运行这段代码:
let number = 3
if number { // 错误:条件必须是 boolean 类型
println("数字是三")
}
编译器会告诉你,X 语言期望条件是一个 boolean 类型,但得到了一个 integer。与 Ruby 和 JavaScript 等语言不同,X 语言不会自动尝试将非布尔类型转换为布尔类型。你必须显式并始终为 if 提供一个布尔值作为条件。
使用 else if 处理多个条件
你可以通过将 if 和 else 组合在一个 else if 表达式中来使用多个条件。例如:
let number = 6
if number % 4 == 0 {
println("数字能被 4 整除")
} else if number % 3 == 0 {
println("数字能被 3 整除")
} else if number % 2 == 0 {
println("数字能被 2 整除")
} else {
println("数字不能被 4、3 或 2 整除")
}
这个程序有四个可能的路径。当你运行它时,你会看到:
数字能被 3 整除
当这个程序执行时,它会依次检查每个 if 表达式,并执行第一个条件为真的代码块。请注意,即使 6 也能被 2 整除,但我们没有看到 “数字能被 2 整除” 的输出,也没有看到来自 else 块的 “数字不能被 4、3 或 2 整除” 的文本。这是因为 X 语言只执行第一个条件为真的块,一旦找到一个块,它就不会检查其余的块。
使用太多的 else if 表达式会使你的代码变得混乱,所以如果我们有多个,我们可能想要重构我们的代码。我们将在后面关于模式匹配的章节中学习一个强大的 X 语言分支构造,叫做 match,正是针对这种情况。
在 let 语句中使用 if
因为 if 是一个表达式,我们可以在 let 语句的右侧使用它来将结果赋值给一个变量:
let condition = true
let number = if condition { 5 } else { 6 }
println("number 的值是: ", number)
number 变量将被绑定到一个值,该值取决于 if 表达式的结果。让我们运行这段代码:
number 的值是: 5
请记住,代码块计算结果为其最后一个表达式的值,而 if 本身就是一个表达式。在这种情况下,整个 if 表达式的值取决于执行哪个代码块。这意味着 if 的每个分支的可能结果必须是同一类型。让我们看看当类型不匹配时会发生什么:
let condition = true
let number = if condition { 5 } else { "six" } // 错误!类型不匹配
整数和字符串是不同的类型,X 语言不允许这样做,因为在编译时它必须确定 number 变量的确切类型。如果 if 和 else 臂具有不同的类型,这可能导致潜在的错误,编译器会在编译时捕获这些错误。
使用循环重复执行代码
多次执行一段代码通常很有用。对于这个任务,X 语言提供了几种循环,它们将遍历循环体中的代码,直到结束,然后立即回到开头再执行一次。
为了尝试循环,让我们创建一个名为 loops.x 的新文件。
while 条件循环
程序中经常需要评估循环内的条件。当条件为真时,循环运行。当条件不再为真时,循环退出。
下面的示例使用 while:程序循环三次,每次倒计时,然后在循环结束后打印一条消息并退出:
let mutable number = 3
while number != 0 {
println(number, "!")
number = number - 1
}
println("发射!")
只要条件为真,代码就会运行;否则,它会退出循环。
运行这个程序,你应该会看到:
3!
2!
1!
发射!
for 循环
for 循环用于遍历迭代器(如列表、范围等)中的每个元素:
for i in 0..5 {
println(i)
}
这个程序会打印 0 到 4(不包含 5)。
你也可以使用包含范围 ..= 来包含右端点:
for i in 0..=5 {
println(i)
}
这个程序会打印 0 到 5(包含 5)。
你可以遍历任何可迭代的集合,例如列表:
let names = ["Alice", "Bob", "Charlie"]
for name in names {
println("Hello, ", name, "!")
}
break 和 continue
你可以使用 break 关键字提前退出循环,使用 continue 关键字跳过循环的当前迭代并继续下一次:
let mutable counter = 0
while counter < 10 {
counter = counter + 1
if counter == 5 {
continue // 跳过 5
}
if counter == 8 {
break // 在 8 时退出
}
println("counter = ", counter)
}
这个程序会打印:
counter = 1
counter = 2
counter = 3
counter = 4
counter = 6
counter = 7
总结
在本章中,我们介绍了通过 if 表达式和循环进行控制流的基础知识。有了这些,你应该能够编写一些基本的 X 语言程序了!
以下是我们学到的内容:
if表达式根据条件进行分支if可以与else if和else组合使用if可以在let中使用,因为它是一个表达式while只要条件为真就循环for ... in遍历迭代器break退出循环continue跳到循环的下一次迭代main函数不是必须的——你可以直接在文件顶层编写代码
到目前为止,你应该对 X 语言的工作方式有了很好的了解。接下来,让我们了解 X 语言的一个独特特性:所有权系统!
理解 Perceus
Perceus 是 X 语言最独特的特性,它使 X 语言能够在不需要垃圾回收器(GC)的情况下保证内存安全,同时也不需要开发者手动管理内存。理解 Perceus 的工作原理很重要。
所有程序在运行时都必须管理它们使用计算机内存的方式。一些语言有垃圾回收(GC),在程序运行时定期寻找不再使用的内存;在其他语言中,程序员必须显式分配和释放内存。X 语言使用第三种方法:Perceus 编译时引用计数。
Perceus 由 Microsoft Research 开发,最初用于 Koka 语言。它在编译时插入所有必要的内存管理操作,因此运行时没有任何开销。
因为 Perceus 对许多程序员来说是一个新概念,需要一些时间来理解。好消息是,你不需要理解 Perceus 的所有细节就能编写 X 语言代码——编译器会为你处理所有复杂的工作!
在本章中,我们将讨论 Perceus 的工作原理,以及它如何让你在享受 GC 便利性的同时获得手动内存管理的性能。
Perceus 基础
X 语言使用了一个名为 Perceus 的引用计数算法,这是一种编译时引用计数技术。让我们从内存管理的基本概念开始。
内存管理的三种方式
在编程语言的历史上,有三种主要的内存管理方式:
| 方式 | 优点 | 缺点 | 示例语言 |
|---|---|---|---|
| 手动管理 | 性能最高,完全控制 | 容易出错(内存泄漏、悬空指针、双重释放) | C、C++ |
| 垃圾回收(GC) | 安全,开发者省心 | 运行时开销,stop-the-world 停顿 | Java、Go、JavaScript |
| Perceus | 安全,无运行时开销,无停顿 | 编译时间稍长 | X、Koka |
Perceus 结合了两者的优点:它像 GC 一样安全,但像手动管理一样高效。
Perceus 的核心思想
Perceus 在编译时分析你的代码,并在需要的地方自动插入内存管理操作:
- dup:增加引用计数
- drop:减少引用计数
当引用计数达到零时,内存会自动释放。这一切都在编译时发生,因此运行时没有任何开销。
让我们看一个简单的例子:
let s1 = "Hello"
let s2 = s1 // Perceus 在这里插入 dup
println(s1)
println(s2)
// Perceus 在这里插入 drop s2
// Perceus 在这里插入 drop s1
在这个例子中:
- 当我们将
s1赋值给s2时,Perceus 知道我们需要两个引用,所以它插入一个dup来增加引用计数 - 当
s2超出作用域时,Perceus 插入一个drop - 当
s1超出作用域时,Perceus 插入另一个drop - 当第二个
drop执行时,引用计数归零,内存被释放
变量作用域
作为理解 Perceus 的第一步,让我们看看变量的作用域。作用域是一个项目在程序中有效的范围。
// s 在这里无效,它还没有声明
let s = "Hello" // s 从这里开始有效
// 用 s 做一些事情
// 这个作用域现在结束了,s 不再有效
换句话说,这里有两个重要的时间点:
- 当
s进入作用域时,它是有效的。 - 它一直保持有效,直到它超出作用域。
当变量超出作用域时,Perceus 会自动插入 drop 操作。
string 类型
让我们用 string 类型来看看 Perceus 是如何工作的。字符串字面量很方便,但它们并不适合我们可能想要使用文本的每一种情况。一个原因是它们是不可变的。另一个原因是并非所有字符串值都能在编写代码时知道。
let s = "Hello" // 字符串字面量
let s2 = String::from("Hello") // 堆分配的字符串
String::from 创建一个在堆上分配的字符串。这种字符串可以被修改:
let mutable s = String::from("Hello")
s = s + ", World!"
println(s)
Perceus 的工作原理
让我们看看 Perceus 如何处理这个例子:
let s1 = String::from("Hello")
let s2 = s1.clone() // 显式克隆,Perceus 在后台 dup
println(s1)
println(s2)
当我们调用 clone() 时,Perceus 在后台插入一个 dup 操作来增加引用计数。当 s1 和 s2 超出作用域时,drop 操作会减少引用计数。
与传统引用计数的区别
你可能熟悉其他语言中的运行时引用计数(如 Python 或 Swift)。Perceus 与它们有几个关键区别:
| 特性 | Perceus | 传统运行时引用计数 |
|---|---|---|
| 何时执行 | 编译时 | 运行时 |
| 运行时开销 | 无 | 有 |
| 线程安全 | 是,无需原子操作 | 需要原子操作 |
| 内存重用 | 支持重用分析 | 通常不支持 |
最重要的是:Perceus 的所有工作都在编译时完成!你的程序运行时,没有任何引用计数的开销。
重用分析(Reuse Analysis)
Perceus 最强大的特性之一是重用分析。当引用计数为 1 时,Perceus 可以重用对象的内存,而不是分配新内存:
let mutable s = String::from("Hello")
s = s + ", World!" // 可能重用 s 的内存!
println(s)
在这个例子中,如果在修改时 s 的引用计数为 1,Perceus 可以原地修改字符串而不是分配新内存。这可能会带来显著的性能提升!
循环引用
与所有引用计数系统一样,Perceus 可能会遇到循环引用的问题:
// 注意:这是说明性的,X 语言通过类型系统帮助防止这种情况
type Node = {
value: integer,
next: Option<&Node>
}
X 语言的类型系统通过要求明确的所有权来帮助防止循环。对于真正需要循环的数据结构(如双向链表),你可以使用标准库中的特殊类型,如 Weak 引用。
总结
Perceus 是 X 语言内存管理的核心:
- 编译时引用计数:所有决策都在编译时做出
- dup 和 drop:基本操作
- 无运行时开销:没有 GC 或引用计数成本
- 重用优化:可能时重用内存
- 线程安全:无需原子操作
Perceus 允许 X 语言在保持类似手动内存管理的性能的同时,提供垃圾回收的便利性。
接下来,让我们更深入地了解 Perceus 的高级特性!
Perceus 高级特性
在本章中,我们将深入探讨 Perceus 的一些高级特性,包括 dup 和 drop 操作的详细工作原理,以及重用分析如何优化性能。
dup 和 drop 详解
Perceus 使用两个基本操作来管理内存:
dup 操作
dup 操作用于增加引用计数。当你需要多个引用指向同一个值时,Perceus 会插入 dup:
let s1 = String::from("Hello")
let s2 = s1 // Perceus 插入 dup(s1)
println(s1)
println(s2)
// Perceus 插入 drop(s2)
// Perceus 插入 drop(s1)
在这个例子中:
s1创建,引用计数 = 1s2 = s1时,Perceus 插入dup(s1),引用计数 = 2s2超出作用域,drop(s2),引用计数 = 1s1超出作用域,drop(s1),引用计数 = 0,内存释放
drop 操作
drop 操作用于减少引用计数。当引用计数达到零时,内存会被自动释放:
function use_string(s: string) {
println(s)
} // Perceus 插入 drop(s)
let s = String::from("Hello")
use_string(s) // s 的所有权传递给函数
// s 在这里不再有效,但 Perceus 不需要 drop,因为所有权已转移
函数参数传递
当你将值传递给函数时,Perceus 会分析是否需要 dup:
function greet(name: string) {
println("Hello, ", name)
}
let s = String::from("World")
greet(s) // 直接传递,不需要 dup
// s 在这里不再有效
如果你想在调用函数后继续使用该值,需要显式 clone():
function greet(name: string) {
println("Hello, ", name)
}
let s = String::from("World")
greet(s.clone()) // clone() 触发 dup
println("Goodbye, ", s) // s 仍然有效
重用分析深入
重用分析是 Perceus 最强大的特性之一。让我们看一个更复杂的例子:
function add_greeting(s: string) -> string {
"Hello, " + s
}
let mutable message = String::from("World")
message = add_greeting(message)
println(message)
在这个例子中:
message创建,引用计数 = 1- 调用
add_greeting(message),传递所有权 - 在
add_greeting中,由于引用计数为 1,Perceus 可能重用内存 - 返回新字符串
message重新赋值
不可变性和 Perceus
X 语言的默认不可变性与 Perceus 配合得非常好:
let s1 = String::from("Hello")
let s2 = s1 // 两个引用指向同一个不可变值
println(s1)
println(s2)
由于 s1 和 s2 都是不可变的,Perceus 可以安全地让它们共享同一个内存,而不需要担心数据竞争。
可变性和 Perceus
当你使用可变数据时,Perceus 仍然可以优化:
let mutable s = String::from("Hello")
s = s + ", World!" // 引用计数为 1,可以重用
println(s)
由于 s 是唯一的引用(引用计数 = 1),Perceus 可以原地修改字符串,而不需要分配新内存。
性能特点
让我们总结 Perceus 的性能特点:
| 操作 | 开销 |
|---|---|
| dup/drop(编译时) | 无运行时开销 |
| 内存分配 | 仅在必要时 |
| 内存重用 | 引用计数为 1 时 |
| 原子操作 | 无需(线程安全通过其他方式保证) |
与其他内存管理方式的性能比较
让我们看一个假设的性能比较:
// X 语言 - Perceus
let s = String::from("Hello")
let t = s.clone() // dup,引用计数 += 1
// 使用 s 和 t
// drop t,引用计数 -= 1
// drop s,引用计数 -= 1,释放
对比手动管理:
// C 语言 - 手动管理
char* s = malloc(6);
strcpy(s, "Hello");
char* t = malloc(6); // 必须显式分配
strcpy(t, s);
// 使用 s 和 t
free(t); // 必须显式释放
free(s); // 必须显式释放
对比垃圾回收:
// Java - GC
String s = "Hello";
String t = s; // 只复制引用
// 使用 s 和 t
// GC 最终会回收(不确定何时)
最佳实践
使用 Perceus 时的一些最佳实践:
- 默认不可变:利用 X 语言的默认不可变性,这有助于 Perceus 的优化
- 避免不必要的 clone():只在确实需要多个引用时才使用 clone()
- 利用重用分析:通过可变变量和线性使用模式来最大化重用机会
- 信任编译器:Perceus 很智能,让它为你处理复杂的内存管理
总结
Perceus 的高级特性:
- dup/drop:基本操作,在编译时插入
- 重用分析:引用计数为 1 时可以原地修改
- 与可变性配合:可变和不可变数据都能高效处理
- 无运行时开销:所有工作都在编译时完成
Perceus 是 X 语言的核心优势之一——它让你在享受 GC 般便利性的同时,获得 C 语言般的性能!
现在我们已经理解了 Perceus,让我们继续讨论结构体!
结构体
结构体(struct)是一种自定义数据类型,允许你命名和包装多个相关的值,这些值构成一个有意义的组。如果你熟悉面向对象的语言,结构体就像对象的数据属性。在本章中,我们将比较和对比记录与结构体,以建立你已经知道的知识。
定义和实例化结构体
结构体类似于我们在第 2 章中讨论的记录类型,但它们有一些额外的功能。让我们首先回顾一下记录类型,然后看看结构体是如何扩展它们的。
快速回顾:记录类型
在第 2 章中,我们看到了如何定义记录类型:
type Point = {
x: Float,
y: Float
}
let p = { x: 0.0, y: 0.0 }
记录对于分组相关数据非常有用。结构体通过添加方法和其他功能来扩展这个概念。
定义结构体
我们使用与定义记录类型相同的语法来定义结构体,但我们可以选择添加方法。让我们定义一个存储关于用户帐户信息的结构体:
type User = {
username: String,
email: String,
sign_in_count: integer,
active: boolean
}
这个结构体的定义有四个字段:username 是 String 类型,email 是 String 类型,sign_in_count 是 integer 类型,active 是 boolean 类型。
实例化结构体
要使用这个结构体,我们通过为每个字段指定具体值来创建该结构体的实例。我们通过声明结构体名称,然后在大括号内添加 key: value 对来创建实例,其中键是字段的名称,值是我们要存储在这些字段中的数据。我们不必按照我们在结构体中声明它们的相同顺序指定字段。换句话说,结构体定义就像该类型的通用模板,实例用特定数据填充该模板。例如,我们可以声明一个特定的用户,如下所示:
let user1 = {
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
active: true
}
为了从结构体中获取特定值,我们使用点表示法。如果我们只想要这个用户的电子邮件地址,我们可以在需要该值的任何地方使用 user1.email。如果实例是可变的,我们可以通过使用点表示法并赋值到特定字段来更改值。清单显示了如何更改可变 User 实例的 email 字段中的值。
let mutable user2 = {
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
active: true
}
user2.email = String::from("anotheremail@example.com")
请注意,整个实例必须是可变的;X 语言不允许我们只将某些字段标记为可变。与任何表达式一样,我们可以在函数体的最后一个表达式中构造并隐式返回该结构体的新实例。
清单显示了一个 build_user 函数,它返回一个 User 实例,其中包含给定的电子邮件和用户名。active 字段的值为 true,sign_in_count 的值为 1。
function build_user(email: String, username: String) -> User {
{
username: username,
email: email,
sign_in_count: 1,
active: true
}
}
让函数参数与结构体字段同名是有意义的,但必须重复 email 和 username 字段名和变量有点乏味。如果结构体有更多字段,重复每个名称会变得更加烦人。幸运的是,有一个方便的简写!
使用字段初始化简写语法
因为参数名和字段名完全相同,我们可以使用字段初始化简写语法重写 build_user,使其行为完全相同,但不必重复 username 和 email,如下所示:
function build_user(email: String, username: String) -> User {
{
username,
email,
sign_in_count: 1,
active: true
}
}
在这里,我们正在创建一个 User 类型的新实例,它有一个名为 username 的字段。我们想将 username 字段的值设置为 build_user 函数的 username 参数中的值。因为 username 字段和 username 参数具有相同的名称,我们只需要写 username 而不是 username: username。
使用结构体更新语法从另一个实例创建实例
通常,从现有实例创建新实例并重用其大部分值但更改某些值是很有用的。你可以使用结构体更新语法来做到这一点。
首先,清单显示了如何在不使用更新语法的情况下从 user1 创建一个新的 User 实例 user2。我们在 user2 中为 email 设置了一个新值,但其他值与我们在 user1 中创建的实例相同。
let user2 = {
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
active: user1.active
}
使用结构体更新语法,我们可以用更少的代码实现相同的效果,如清单所示。.. 语法指定未显式设置的剩余字段应与给定实例中的字段具有相同的值。
let user2 = {
email: String::from("another@example.com"),
...user1
}
清单中的代码也创建了一个 user2 实例,它的 email 与 user1 中的不同,但 username、sign_in_count 和 active 字段的值与 user1 中的相同。...user1 必须放在最后,以指定任何剩余字段应从 user1 中的字段获取它们的值,但我们可以选择以任何顺序为任意数量的字段指定值,而不管结构体定义中字段的顺序如何。
请注意,结构更新语法会像赋值一样移动数据,就像我们在第 3 章中讨论的那样。在这种情况下,我们在创建 user2 后不能再使用 user1,因为 user1 的 username 字段中的 String 已移动到 user2。如果我们给 user2 赋予新的 email 和 username 值,并且只使用 user1 中的 sign_in_count 和 active 值,那么 user1 在创建 user2 后仍然有效。sign_in_count 和 active 是实现 Copy trait 的类型,因此我们在第 3 章中讨论的行为适用。
没有命名字段的元组结构体
你还可以定义看起来类似于元组的结构体,称为元组结构体。元组结构体具有结构体名称提供的含义,但没有与其字段关联的名称;相反,它们只有字段的类型。当你想给整个元组一个名称并使元组与其他元组具有不同类型时,元组结构体很有用,并且当你像常规结构体那样命名每个字段时会显得冗长或多余。
要定义元组结构体,请以 type 关键字开头,后跟结构体名称和元组中的类型。例如,这里我们定义并使用两个名为 Color 和 Point 的元组结构体:
type Color = (integer, integer, integer)
type Point = (integer, integer, integer)
let black = Color(0, 0, 0)
let origin = Point(0, 0, 0)
请注意,black 和 origin 值是不同的类型,因为它们是不同元组结构体的实例。你定义的每个结构体都是它自己的类型,即使结构体中的字段具有相同的类型。例如,一个以 Color 作为参数的函数不能接受 Point 作为参数,即使这两种类型都由三个 integer 值组成。否则,元组结构体实例的行为类似于元组:你可以将它们解构为它们的单独部分,你可以使用 . 后跟索引来访问单个值,依此类推。
结构体的方法语法
方法类似于函数:我们用 function 关键字声明它们,它们可以有参数和返回值,并且它们包含一些代码,当从其他地方调用该方法时会运行这些代码。与函数不同,方法是在结构体(或枚举或 trait 对象,我们将在第 4 章和第 13 章分别介绍)的上下文中定义的,并且它们的第一个参数总是 self,它代表调用该方法的结构体的实例。
定义方法
让我们将以 Rectangle 实例为参数的 area 函数改为定义在 Rectangle 结构体上的 area 方法。
首先,让我们定义一个 Rectangle 类型:
type Rectangle = {
width: integer,
height: integer
}
要在 Rectangle 的上下文中定义函数,我们需要为这个结构体实现方法。让我们看看如何添加一个计算矩形面积的 area 方法:
function Rectangle::area(self: &Rectangle) -> integer {
self.width * self.height
}
要在 Rectangle 的上下文中定义函数,我们使用 Rectangle:: 前缀将函数与 Rectangle 类型关联起来。然后,我们可以使用点语法在 Rectangle 实例上调用这个方法:
let rect = { width: 30, height: 50 }
println(
"矩形的面积是 ",
rect.area(),
" 平方单位。"
)
在 area 的签名中,我们使用 self: &Rectangle 而不是 rectangle: &Rectangle,因为该方法位于 Rectangle:: 命名空间中,所以我们知道 self 的类型是 Rectangle。
注意,我们可以选择将 self 作为第一个参数,或者像我们在这里做的那样借用 self,就像我们可以使用任何其他参数一样。在这里,我们选择了借用 self,就像我们在函数版本中所做的那样。我们不希望方法获取所有权,我们只想读取结构体中的数据,而不是写入它。如果我们想在我们作为方法调用的一部分的实例中改变某些东西,我们会使用 self: &mut Rectangle 作为第一个参数。
通过在第一个参数中使用 self 而不是借用结构体的所有权,方法可以选择获取所有权、不可变借用或可变借用,就像它们可以对任何其他参数一样。
在主要调用 area 时,我们使用 rect.area() 来调用 rect 上的 area 方法。这比 area(&rect) 好得多,因为它更符合我们将方法附加到我们的值的直觉。
具有更多参数的方法
让我们通过实现 Rectangle 类型的第二个方法来练习使用方法。这一次,我们希望一个 Rectangle 实例接受另一个 Rectangle 实例,如果第二个 Rectangle 可以完全放入 self(第一个 Rectangle)中,则返回 true;否则,它应该返回 false。也就是说,一旦我们定义了 can_hold 方法,我们就想能够编写程序。
let rect1 = { width: 30, height: 50 }
let rect2 = { width: 10, height: 40 }
let rect3 = { width: 60, height: 45 }
println("rect1 能容纳 rect2 吗?", rect1.can_hold(&rect2))
println("rect1 能容纳 rect3 吗?", rect1.can_hold(&rect3))
我们希望看到这样的输出,因为 rect2 的两个维度都小于 rect1 的维度,但 rect3 比 rect1 宽:
rect1 能容纳 rect2 吗? true
rect1 能容纳 rect3 吗? false
我们知道我们想要定义一个方法,它将在 Rectangle:: 命名空间中。对于参数,我们将使用 other: &Rectangle,它将是对我们传递给 can_hold 的第二个 Rectangle 实例的不可变借用。该方法将返回一个 boolean。实现将检查 self 的宽度是否大于 other 的宽度,以及 self 的高度是否大于 other 的高度。让我们添加新的 can_hold 方法。
function Rectangle::can_hold(self: &Rectangle, other: &Rectangle) -> boolean {
self.width > other.width && self.height > other.height
}
当我们在 rect1 上使用 rect2 和 rect3 作为参数运行这段代码时,我们将看到所需的输出。方法可以接受多个参数,我们在 self 参数之后将其添加到签名中,这些参数的工作方式与函数中的参数完全相同。
关联函数
在 Rectangle:: 命名空间中定义的不将 self 作为第一个参数的函数称为关联函数,因为它们与该类型关联。它们仍然是函数,而不是方法,因为它们没有可以处理的结构体的实例。你已经使用了 String::from 关联函数。
关联函数通常用于将返回该结构体新实例的构造函数。例如,我们可以提供一个接受一个维度参数并将其用作宽度和高度的关联函数,这样我们就可以轻松创建正方形的 Rectangle,而不必指定相同的值两次:
function Rectangle::square(size: integer) -> Rectangle {
{ width: size, height: size }
}
要调用这个关联函数,我们使用结构体名称和 :: 语法;例如 let sq = Rectangle::square(3)。这个函数由结构体命名::: 语法既用于关联函数,也用于模块创建的命名空间。我们将在第 5 章讨论模块。
总结
结构体允许你创建自定义类型,这些类型对于你的域有意义。通过使用结构体,你可以保持相关的数据片段相互关联,并为每个数据片段命名,使代码更清晰。在结构体的命名空间中,你可以定义方法并指定关联函数,这些方法指定你的自定义类型可以具有的行为。
但是结构体并不是创建自定义类型的唯一方法:让我们转向 X 语言的枚举特性,为你的工具箱添加另一个工具。
记录类型
在第 2 章中,我们简要介绍了记录类型作为 X 语言的基本复合类型之一。在本章中,我们将更深入地了解记录,它们与结构体的关系,以及何时你可能想要使用它们。
什么是记录?
记录是一种简单的数据结构,它将多个具有不同类型的值组合成一个类型。记录使用字段名来标识每个值。你可以将记录视为没有方法的轻量级结构体。
下面是我们在第 2 章中看到的记录类型定义的回顾:
type Point = {
x: Float,
y: Float
}
记录类型定义以 type 关键字开头,后跟记录的名称,然后是大括号内的字段列表。每个字段都有一个名称(如 x 或 y)和一个类型(如 Float),用冒号分隔。
创建记录实例
要创建记录的实例,我们使用与定义记录相同的大括号语法,但我们为每个字段提供具体值:
let p = { x: 0.0, y: 0.0 }
字段顺序不必与类型定义中的顺序匹配。以下代码也是有效的:
let p = { y: 0.0, x: 0.0 }
访问记录字段
要访问记录的字段,我们使用点表示法:
let p = { x: 1.0, y: 2.0 }
println("x = ", p.x)
println("y = ", p.y)
这将打印:
x = 1
y = 2
可变记录
如果要修改记录的字段,则需要将记录变量声明为可变的:
let mutable p = { x: 1.0, y: 2.0 }
p.x = 5.0
p.y = 6.0
println("x = ", p.x)
println("y = ", p.y)
请注意,整个记录必须是可变的;X 语言不允许只将某些字段标记为可变。
记录更新语法
就像结构体一样,记录也支持更新语法,以从现有实例创建新实例:
let p1 = { x: 1.0, y: 2.0 }
let p2 = { x: 5.0, ...p1 }
println("p2.x = ", p2.x)
println("p2.y = ", p2.y)
这将打印:
p2.x = 5
p2.y = 2
...p1 语法指定未显式设置的剩余字段应与 p1 中的字段具有相同的值。
记录与结构体
你可能想知道什么时候应该使用记录,什么时候应该使用结构体。以下是一些指导原则:
使用记录的情况
- 当你只需要一个简单的数据容器时
- 当你不需要任何方法时
- 当你想要轻量级的东西时
- 当你主要处理数据传输对象(DTOs)时
记录示例:
// 简单的坐标点
type Point = { x: Float, y: Float }
// 表示用户数据
type UserData = {
id: integer,
name: String,
email: String
}
// 配置选项
type Config = {
debug: boolean,
log_level: String,
max_connections: integer
}
使用结构体的情况
- 当你需要方法时
- 当你想要封装行为时
- 当你需要更复杂的功能时
- 当你实现数据抽象时
结构体示例:
// 带有区域计算方法的 Rectangle
type Rectangle = {
width: integer,
height: integer
}
function Rectangle::area(self: &Rectangle) -> integer {
self.width * self.height
}
// 带有操作方法的 BankAccount
type BankAccount = {
balance: integer,
account_number: String
}
function BankAccount::deposit(self: &mut BankAccount, amount: integer) {
self.balance = self.balance + amount
}
function BankAccount::withdraw(self: &mut BankAccount, amount: integer) -> boolean {
if self.balance >= amount {
self.balance = self.balance - amount
true
} else {
false
}
}
记录模式匹配
你也可以在模式匹配中使用记录:
type Point = { x: Float, y: Float }
let p = { x: 1.0, y: 2.0 }
when p is {
{ x: 0.0, y: 0.0 } => println("原点"),
{ x: x, y: 0.0 } => println("在 x 轴上,x = ", x),
{ x: 0.0, y: y } => println("在 y 轴上,y = ", y),
{ x: x, y: y } => println("在 (", x, ", ", y, ")")
}
这允许你根据记录字段的值轻松地分支代码。
记录作为函数参数和返回值
记录作为函数参数和返回值非常有用:
type Point = { x: Float, y: Float }
// 计算两点之间的距离
function distance(p1: Point, p2: Point) -> Float {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
sqrt(dx * dx + dy * dy)
}
// 创建一个新的点
function create_point(x: Float, y: Float) -> Point {
{ x: x, y: y }
}
// 使用这些函数
let p1 = create_point(0.0, 0.0)
let p2 = create_point(3.0, 4.0)
let dist = distance(p1, p2)
println("距离是 ", dist)
总结
记录是 X 语言中一种简单而强大的数据结构。它们:
- 使用字段名将多个值组合成一个类型
- 可以是可变的或不可变的
- 支持更新语法以方便实例化
- 可以与模式匹配一起使用
- 是简单数据容器的绝佳选择
对于更复杂的需求,你可以使用添加了方法的结构体。记录和结构体共同为你提供了在 X 语言中组织数据所需的灵活性。
既然我们已经很好地理解了结构体、枚举和记录,让我们转到模块系统,这将帮助我们组织更大的程序!
枚举与模式匹配
在本章中,我们将介绍枚举(也称为 enums),它允许你通过枚举可能的变体来定义类型。首先,我们将定义并使用枚举来展示枚举如何能够同时编码含义和数据。接下来,我们将探索一个特别有用的枚举,称为 Option,它表示一个值可以是某物或什么都不是。然后,我们将研究如何使用 match 表达式根据枚举的值轻松地为不同代码分支执行不同的代码。
定义枚举
在我们看到什么是枚举之前,让我们想一个我们可能需要在代码中表达并看看为什么枚举有用且合适的情况。假设我们需要处理 IP 地址。目前,用于 IP 地址的两个主要标准是第四版和第六版。这些是我们的程序可能遇到的唯一 IP 地址类型:我们可以枚举所有可能的值,这就是枚举得名的地方。
任何 IP 地址要么是第 4 版要么是第 6 版,但不能同时是两者。IP 地址的这个属性使枚举数据结构合适,因为枚举值只能是其变体之一。第 4 版和第 6 版地址仍然是 IP 地址,所以它们应该被视为同一类型,当代码处理适用于任何类型的 IP 地址的情况时。
我们可以通过在代码中定义一个 IpAddrKind 枚举来表达这个概念,并列出可能的 IP 地址类型:V4 和 V6。这些是枚举的变体:
type IpAddrKind = V4 | V6
我们可以如下使用 IpAddrKind 的每个变体:
let four = V4
let six = V6
换句话说,IpAddrKind 的变体在枚举的命名空间下,我们使用竖线语法来分隔它们。现在我们可以定义一个以任何 IpAddrKind 作为参数的函数:
function route(ip_kind: IpAddrKind) {
// ...
}
我们可以使用任一变体调用此函数:
route(V4)
route(V6)
将数据附加到枚举变体
使用枚举,我们可以通过将数据直接放入每个枚举变体中来更简洁地表达相同的概念,而不是在枚举内部只包含类型:
type IpAddr = V4(String) | V6(String)
我们可以直接将数据附加到枚举的每个变体,而不是让枚举只包含类型,这样我们就不需要额外的结构体。在这里,IpAddr 枚举的名称也成为一个构造函数:V4(String) 和 V6(String) 都是可以接受参数的函数调用,分别创建 IpAddr 类型的实例。
我们可以这样使用这个新的枚举定义:
let home = V4("127.0.0.1")
let loopback = V6("::1")
请注意,我们现在有两个不同类型的值,它们的类型都是 IpAddr。这很好,因为我们现在可以定义一个以任何 IpAddr 作为参数的函数:
function route(ip: IpAddr) {
// ...
}
我们可以使用任一变体调用此函数:
route(home)
route(loopback)
这种用枚举代替结构体的能力还有另一个好处:每个变体可以有不同类型和数量的关联数据。第 4 版 IP 地址总是有四个介于 0 和 255 之间的数字组成部分。如果我们想将 V4 地址存储为四个 integer 值,但仍然将 V6 地址表示为一个 String,我们将无法使用结构体。枚举轻松处理这种情况:
type IpAddr = V4(integer, integer, integer, integer) | V6(String)
let home = V4(127, 0, 0, 1)
let loopback = V6("::1")
我们已经展示了多种不同的方法来定义数据结构来存储第 4 版和第 6 版 IP 地址。但是,正如你所看到的,我们可以将数据附加到枚举变体,并且每个变体的类型可以不同,这恰好是我们想要的。让我们看看另一个枚举的例子:这个例子有各种各样的类型嵌入在它的变体中。
type Message =
Quit
| Move(integer, integer)
| Write(String)
| ChangeColor(integer, integer, integer)
这个枚举有四个变体,具有不同的类型:
Quit没有与之关联的数据。Move包含两个integer。Write包含一个String。ChangeColor包含三个integer。
以这种方式定义枚举类似于定义不同类型的结构体定义,只是枚举没有 type 关键字,并且所有变体都被分组在 Message 类型下。以下结构体可以保存与前面的枚举变体相同的数据:
type QuitMessage = { }
type MoveMessage = { x: integer, y: integer }
type WriteMessage = { content: String }
type ChangeColorMessage = { r: integer, g: integer, b: integer }
但是如果我们使用不同的结构体(每个结构体都有自己的类型),我们不能像使用我们在上面定义的 Message 枚举那样轻松地定义一个可以接受任何这些类型消息的函数。
选项枚举及其优于空值的地方
在本节中,我们将讨论 Option 的一个案例研究,Option 是标准库中定义的另一个枚举。Option 类型在许多情况下非常常用,它编码了一个非常常见的场景,其中一个值可以是某物,也可以什么都不是。
例如,如果你请求非空列表中的第一个项目,你会得到一个值。如果你请求空列表中的第一个项目,你什么也得不到。在类型系统中表达这个概念意味着编译器可以检查你是否处理了应该处理的所有情况;这个功能可以防止其他编程语言中非常常见的错误。
编程语言设计经常从包含哪些功能的角度来考虑,但你排除的功能也很重要。X 语言没有许多其他语言有的空值功能。空值是一个表示那里什么都没有的值。在有空值的语言中,变量总是可以处于两种状态之一:空或非空。
空值的问题是,如果你尝试将空值用作非空值,你会得到某种错误。因为这个空或非空属性无处不在,很容易犯这种错误。
然而,空值试图表达的概念仍然有用:空值是一个由于某种原因当前无效或不存在的值。
问题不在于概念本身,而在于特定的实现。因此,X 语言没有空值,但它确实有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,它由标准库定义如下:
type Option<T> = Some(T) | None
Option<T> 枚举非常有用,它甚至包含在 prelude 中;你不需要将其纳入作用域。它的变体也包含在 prelude 中:你可以直接使用 Some 和 None,而不需要 Option:: 前缀。Option<T> 仍然只是一个普通的枚举,Some(T) 和 None 仍然是 Option<T> 类型的变体。
<T> 语法是我们尚未讨论的 X 语言特性。它是一个泛型类型参数,我们将在第 8 章更详细地介绍泛型。目前,你需要知道的是,<T> 意味着 Option 枚举的 Some 变体可以容纳一个任何类型的数据,并且 <T> 可以是任何具体类型。这里有一些使用 Option 值来保存数字类型和字符串类型的示例:
let some_number = Some(5)
let some_string = Some("字符串")
let absent_number: Option<integer> = None
如果我们使用 None 而不是 Some,我们需要告诉 X 语言我们想要什么类型的 Option<T>,因为编译器无法仅通过查看 None 值就推断出 Some 变体将持有的类型。
当我们有一个 Some 值时,我们知道存在一个值,并且该值保存在 Some 中。当我们有一个 None 值时,在某种意义上,它意味着与空值相同:我们没有一个有效的值。那么为什么拥有 Option<T> 比拥有空值更好呢?
简而言之,因为 Option<T> 和 T(其中 T 可以是任何类型)是不同的类型,编译器不会让我们使用 Option<T> 值,就好像它绝对是一个有效值一样。例如,这段代码不会编译,因为它试图将一个 Option<integer> 加到一个 integer 上:
let x: integer = 5
let y: Option<integer> = Some(5)
let sum = x + y // 错误!不能将 integer 和 Option<integer> 相加
实际上,这个错误消息意味着 X 语言不理解如何将 Option<integer> 和 integer 相加,因为它们是不同的类型。当我们在 X 语言中有一个像 integer 这样类型的值时,编译器将确保我们始终有一个有效值。我们可以放心地进行操作,而不必在使用该值之前检查空值。只有当我们有一个 Option<integer>(或我们正在使用的任何类型的 Option)时,我们才必须担心可能没有值,编译器会确保我们在使用该值之前处理这种情况。
换句话说,在你可以对 Option<T> 进行 T 操作之前,你必须将其转换为 T。一般来说,这有助于捕获空值最常见的问题之一:假设某事不是空的,而实际上它是空的。
不必担心错误地假设一个非空值,这有助于你对代码更有信心。为了拥有一个可能为空的值,你必须通过使该值的类型 Option<T> 来明确选择加入。然后,当你使用该值时,你必须明确处理该值为空的情况。每当一个值的类型不是 Option<T> 时,你可以安全地假设该值不为空。这是 X 语言的一个深思熟虑的设计决策,目的是限制空值的普遍存在,并增加 X 语言代码的安全性。
那么,当你有一个 Option<T> 类型的值时,你如何从 Some 变体中获取 T 值以便你可以使用它呢?Option<T> 枚举有大量有用的方法,可用于各种情况;你可以在其文档中查看它们。熟悉 Option<T> 上的方法将对你的 X 语言之旅非常有用!
通常,为了处理 Option<T>,你会有代码来处理每个变体。你想要一些代码,这些代码只在有 Some(T) 值时运行,并且允许该代码使用内部的 T。你想要一些其他代码,这些代码在有 None 值时运行,并且该代码没有可用的 T 值。match 表达式是一个控制流构造,正是这样做的:它允许我们通过枚举的变体来分支代码。
使用 match 进行模式匹配
X 语言有一个极其强大的控制流构造,称为 match,它允许我们将值与一系列模式进行比较,并根据匹配的模式执行代码。模式可以由字面值、变量名、通配符和许多其他东西组成;第 17 章涵盖了所有不同类型的模式以及它们的作用。match 的威力来自于模式的表现力以及编译器验证所有可能的情况都得到处理的事实。
将 match 表达式想象成一个硬币分拣机:硬币沿着带有不同尺寸孔的滑道滚动,每个硬币都通过它遇到的第一个合适的孔掉落。以同样的方式,值会通过 match 中的每个模式,并且在第一个模式“匹配“该值时,该值会落入要在执行期间使用的关联代码块中。
既然我们已经使用了枚举,让我们使用 match!让我们看一个例子:
type Coin = Penny | Nickel | Dime | Quarter
function value_in_cents(coin: Coin) -> integer =
match coin {
Penny => 1,
Nickel => 5,
Dime => 10,
Quarter => 25
}
让我们分解 value_in_cents 函数中的 match。首先,我们列出 match 关键字,后跟一个值,在这个例子中是 coin。这看起来与 if 表达式的使用非常相似,但有一个很大的区别:对于 if,条件需要评估为一个 boolean,但在这里,它可以是任何类型。这个例子中的 coin 类型是我们之前定义的 Coin 枚举。
接下来是用大括号括起来的模式匹配臂。一个臂有两个部分:一个模式和一些代码。第一个臂的模式是值 Penny,然后是 => 运算符,它将模式和要运行的代码分开。在这种情况下,代码只是值 1。每个臂用逗号与下一个臂分隔。
当 match 表达式执行时,它会将结果值按顺序与每个臂的模式进行比较。如果模式与值匹配,则执行与该模式关联的代码。如果该模式与值不匹配,执行将继续到下一个臂,就像在硬币分拣机中一样。我们可以根据需要拥有任意数量的臂!
与模式关联的代码是一个表达式,该表达式在匹配臂中的结果值是整个 match 表达式返回的值。
如果匹配臂代码很短,我们通常不使用大括号,就像我们的例子中每个匹配臂只返回一个值一样。如果你想在一个匹配臂中运行多行代码,你可以使用大括号,并且在大括号之后可以选择使用逗号分隔该臂与下一个臂。例如,以下代码在使用 Penny 调用时每次都会打印 “幸运的便士!”,但仍会返回块中的最后一个值 1:
function value_in_cents(coin: Coin) -> integer =
match coin {
Penny => {
println("幸运的便士!")
1
},
Nickel => 5,
Dime => 10,
Quarter => 25
}
绑定值的模式
匹配臂的另一个有用特性是它们可以绑定到匹配模式部分的值。这就是我们如何从枚举变体中提取值的方法。
作为一个例子,让我们修改我们的 Quarter 变体,使其内部保存一个状态值。在 1999 年到 2008 年之间,美国为 50 个州中的每一个州都铸造了带有特殊设计的 25 美分硬币。没有其他硬币有州设计,所以只有 Quarter 变体有这个额外的值。我们可以通过将 UsState 类型添加到我们的 Quarter 变体来将此信息添加到我们的枚举中,如下所示:
type UsState =
Alabama
| Alaska
// -- 等等 --
type Coin =
Penny
| Nickel
| Dime
| Quarter(UsState)
假设一个朋友正在尝试收集所有 50 个州的 25 美分硬币。当我们按硬币类型对零钱进行排序时,我们也可以说出与 25 美分硬币相关的州名称,这样如果我们的朋友没有这个州的硬币,他们可以将其添加到他们的收藏中。
在这段代码的匹配表达式中,我们在匹配 Quarter 变体值的模式中添加了一个名为 state 的变量。当一个 Quarter 匹配时,state 变量将绑定到该 25 美分硬币的州值。然后我们可以在该臂的代码中使用 state,如下所示:
function value_in_cents(coin: Coin) -> integer =
match coin {
Penny => 1,
Nickel => 5,
Dime => 10,
Quarter(state) => {
println("州 25 美分硬币来自 ", state, "!")
25
}
}
如果我们调用 value_in_cents(Quarter(Alaska)),coin 将是 Quarter(Alaska)。当我们将该值与每个匹配臂进行比较时,直到我们到达 Quarter(state),此时 state 将是值 Alaska。然后我们可以在 println 中使用该绑定,从而从 Coin 枚举的 Quarter 变体中获取内部州值。
匹配 Option
在上一节中,我们希望在使用 Option<T> 时从 Some 案例内部获取 T 值;我们也可以使用 match 来处理 Option<T>,就像我们对 Coin 枚举所做的那样!我们将比较 Option 的变体,而不是比较硬币,但 match 表达式的工作方式保持不变。
假设我们想编写一个函数,它接受一个 Option<integer>,如果里面有一个值,则将该值加 1。如果里面没有值,该函数应该返回 None 值而不尝试执行任何操作。
由于 match,这个函数很容易编写,看起来会像这样:
function plus_one(x: Option<integer>) -> Option<integer> =
match x {
None => None,
Some(i) => Some(i + 1)
}
let five = Some(5)
let six = plus_one(five)
let none = plus_one(None)
让我们更仔细地看一下 plus_one 的第一次执行。当我们调用 plus_one(five) 时,plus_one 主体中的变量 x 将具有值 Some(5)。然后我们将其与每个匹配臂进行比较:
None => None,
Some(5) 值不匹配模式 None,所以我们继续下一个臂。
Some(i) => Some(i + 1),
Some(5) 是否匹配 Some(i)?确实匹配!我们有相同的变体。i 绑定到 Some 内部的值,所以 i 具有值 5。然后执行该匹配臂中的代码:我们将 1 加到 i 的值上,并创建一个新的 Some 值,其中包含我们的总和 6。
现在让我们看看清单中的第二次调用 plus_one,其中 x 是 None。我们进入 match 并与第一个臂进行比较:
None => None,
它匹配!没有要添加的值,所以程序停止并在 => 的右侧返回 None。因为第一个臂匹配,所以其他臂都不会被比较。
以这种方式将 match 与枚举结合使用在许多情况下都很有用。你会在 X 语言代码中看到很多这种模式:match,绑定到内部数据的模式,然后基于该模式执行代码。一开始有点棘手,但一旦你习惯了它,你就会希望在所有语言中都有它。它始终是用户的最爱。
匹配是穷尽的
我们需要讨论 match 的另一个方面:臂的模式必须覆盖所有可能性。看看这个版本的 plus_one 函数,它有一个错误,不会编译:
function plus_one(x: Option<integer>) -> Option<integer> =
match x {
Some(i) => Some(i + 1)
}
我们没有处理 None 案例,所以这段代码会导致错误。幸运的是,这是 X 语言编译器知道如何捕获的错误。如果我们尝试编译这段代码,我们会得到这个错误:
error: 模式 `None` 未覆盖
X 语言知道我们没有覆盖所有可能的情况,甚至知道我们忘记了哪个模式!X 语言中的匹配是穷尽的:我们必须穷尽最后一个可能的情况,才能使代码有效。特别是在 Option 的情况下,当 X 语言防止我们忘记显式处理 None 情况时,它可以保护我们免受假设我们有一个值而实际上可能有一个空值的错误,从而使我们之前讨论的数亿美元错误不可能发生。
通配符模式和 where 守卫
X 语言还有一个模式,我们可以在不想列出所有可能值的情况下使用。例如,signed 8bit integer 可以包含 -128 到 127 的有效数字。如果我们只关心 1、3、5 和 7,我们不想列出 -128、-127、…、0、2、4、6、8、9 一直到 127。幸运的是,我们不必这样做:我们可以使用特殊的通配符模式 _ 代替。
我们还可以在模式中使用 where 子句来添加额外的条件,这被称为“守卫“:
let some_value = 5
match some_value {
1 => println("一"),
3 => println("三"),
5 => println("五"),
7 => println("七"),
n where n % 2 == 0 => println("偶数: ", n),
_ => println("其他数字")
}
_ 模式将匹配任何未指定的值。通过在所有其他臂之后添加 _,通配符将匹配所有可能的情况,这些情况在它之前没有列出。
通配符模式对于我们想要忽略除少数情况之外的所有情况的场景非常有用。
总结
在本章中,我们介绍了如何使用枚举来创建可以是一组变体之一的自定义类型。我们展示了标准库的 Option<T> 类型如何帮助你使用类型系统来防止错误。当枚举值在其变体内部包含数据时,你可以使用 match 来处理这些值并决定对每种情况执行什么代码。
现在你已经了解了结构体和枚举的强大功能,让我们转到记录类型,这是一种在 X 语言中对相关数据进行分组的更简单方法。
包和 Crate
在本章中,我们将讨论包和 crate——X 语言中代码组织的两个基本概念。让我们首先看看包和 crate 是什么,然后看看它们如何协同工作。
什么是 Crate?
Crate 是 X 语言中编译的最小单位。每次运行 x compile something.x 时,那个 something.x 文件都被视为一个 crate。Crate 可以是二进制 crate 或库 crate。
二进制 Crate
二进制 crate 是可以编译为可以运行的可执行文件的程序。它们必须有一个名为 main 的函数,该函数定义了可执行文件运行时发生的事情。我们到目前为止创建的所有 crate 都是二进制 crate。
库 Crate
库 crate 没有 main 函数,也不会编译为可执行文件。相反,它们定义了旨在在多个项目之间共享的功能。例如,如果我们写了一个提供有用数学函数的 crate,我们可以在多个不同的项目中使用该 crate。
我们使用 crate 的方式是将它们用作库。大多数时候,当人们说“crate“时,他们指的是库 crate,并且他们几乎可以互换使用“crate“和“library“。
什么是包?
包是提供一组功能的一个或多个 crate。包包含一个 x.toml 文件,该文件描述了如何构建这些 crate。让我们创建一个包!
创建包
让我们创建一个名为 hello_world 的新包。为此,请运行以下命令:
x new hello_world
这将创建一个名为 hello_world 的目录,其中包含以下文件:
hello_world/
├── x.toml
└── src/
└── main.x
让我们看看 x.toml 包含什么:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"
[dependencies]
此文件采用 TOML(Tom’s Obvious, Minimal Language)格式,这是 X 包的配置格式。
第一行 [package] 表示以下部分正在配置包。接下来的三行设置我们的包构建所需的配置:名称、版本和要使用的 X 语言 edition。最后一部分 [dependencies] 是我们将为包添加任何依赖项的部分。
现在让我们看看 src/main.x:
function main() {
println("Hello, world!")
}
x new 已经为我们生成了一个“Hello, world!“程序!让我们运行它:
cd hello_world
x run
你应该会看到输出:
Hello, world!
包的规则
包可以包含多个二进制 crate,但最多只能包含一个库 crate。让我们看看 x new 生成的包:在 src 目录中,我们有:
src/main.x- 这是一个与包同名的二进制 crate 的根src/lib.x- 如果我们添加这个文件,包将有一个与包同名的库 crate
我们也可以通过将文件放在 src/bin 目录中来拥有多个二进制 crate:src/bin 目录中的每个文件都是一个单独的二进制 crate。
库 Crate
让我们向我们的包中添加一个库 crate。创建一个名为 src/lib.x 的文件,内容如下:
// src/lib.x
export function add(a: integer, b: integer) -> integer {
a + b
}
现在我们的包同时包含一个库 crate 和一个二进制 crate!我们可以在二进制 crate 中使用我们的库 crate。修改 src/main.x:
// src/main.x
import hello_world::add
function main() {
println("2 + 3 = {}", add(2, 3))
}
运行它:
x run
你应该会看到输出:
2 + 3 = 5
使用外部 Crate
让我们看看如何使用外部 crate。假设我们想使用一个名为 rand 的 crate,它提供随机数生成功能。首先,我们需要将它添加到我们的 x.toml 中:
[dependencies]
rand = "0.8.5"
现在我们可以在代码中使用 rand crate:
import rand::random
function main() {
let secret_number = random::<integer>()
println("随机数: {}", secret_number)
}
运行 x build,X 会自动下载 rand 及其所有依赖项,构建它们,然后构建我们的包。
构建和运行包
X 提供了几个用于处理包的命令:
x new- 创建一个新包x build- 构建包x run- 构建并运行二进制 cratex test- 运行包的测试x doc- 构建包的文档x check- 快速检查包的错误而不生成可执行文件
总结
在本章中,我们介绍了:
- Crate - X 语言中编译的最小单位(二进制或库)
- 包 - 一个或多个 crate,带有
x.toml描述如何构建它们 x.toml- 包的配置文件src/main.x- 二进制 crate 的根src/lib.x- 库 crate 的根src/bin/- 额外二进制 crate 的目录- 依赖项 - 在
x.toml中指定,并由 X 自动管理
包和 crate 是组织 X 语言代码的基础!
模块系统
随着项目的增长,你需要通过将代码拆分为多个文件,然后拆分为多个模块来组织你的代码。当代码库变大时,将相关功能分组并分离不同的功能变得至关重要。
X 语言的模块系统包括:
- 模块:允许你组织代码并控制路径的隐私
- 路径:一种命名项目的方式,如函数、类型或模块
- import:将路径引入作用域
- export:使项目可从外部访问
让我们按顺序讨论所有这些概念!
模块定义
你可以使用 module 关键字声明一个新模块,后跟模块的名称和大括号,这些大括号包含模块的内容。让我们从定义一些模块开始,以组织我们的示例代码。
// 在名为 garden.x 的文件中
module plants {
// 植物相关的代码
}
module animals {
// 动物相关的代码
}
模块可以嵌套:
module garden {
module plants {
function grow() {
println("植物正在生长!")
}
}
module animals {
function eat() {
println("动物正在吃东西!")
}
}
}
路径
要引用模块中的项目,我们需要知道它的路径。路径有两种形式:
- 绝对路径:从根模块开始,使用模块名或字面量路径。
- 相对路径:从当前模块开始,使用
self、super或当前模块中的标识符。
绝对路径和相对路径后面跟着一个或多个由双冒号(::)分隔的标识符。
让我们看一下我们的花园模块示例:
module garden {
module plants {
function grow() {
println("植物正在生长!")
}
}
}
function main() {
// 绝对路径
garden::plants::grow()
// 相对路径(如果在适当的模块中)
// plants::grow()
}
我们可以使用绝对路径从 main 函数调用 grow 函数,该路径从根模块开始并导航到 garden,然后到 plants,最后到 grow。
使用 super 开始相对路径
我们还可以通过使用 super 开头来构建从父模块开始的相对路径。这就像在文件系统中使用 .. 语法从父目录开始一样。使用 super 允许我们引用父模块中的项目,当模块彼此靠近时,这有助于重新组织模块树,而不必重写大量路径。
module garden {
function water() {
println("给花园浇水!")
}
module plants {
function grow() {
super::water() // 调用父模块中的 water 函数
println("植物正在生长!")
}
}
}
导出项目
默认情况下,模块中的所有内容都是私有的(private)。父模块中的代码不能使用子模块中的私有代码,但子模块中的代码可以使用其祖先模块中的代码。这是因为模块应该封装其实现细节。
要使模块中的项目公开,我们可以使用 export 关键字。让我们看一个例子:
module garden {
export module plants {
export function grow() {
println("植物正在生长!")
}
}
}
function main() {
// 现在可以访问,因为 plants 模块和 grow 函数都已导出
garden::plants::grow()
}
在这个例子中,我们导出了 plants 模块和 grow 函数,使它们可以从 garden 模块外部访问。
导入项目
使用模块的完整路径调用函数可能很长并且会重复。在 X 语言中,我们有一个方法可以使用 import 关键字将路径引入作用域,从而使这个过程更短。让我们看看如何将 garden::plants::grow 路径引入作用域:
module garden {
export module plants {
export function grow() {
println("植物正在生长!")
}
}
}
import garden::plants::grow
function main() {
grow() // 现在我们可以直接调用 grow!
}
我们也可以使用 import 来导入整个模块:
import garden::plants
function main() {
plants::grow()
}
导入多个项目
我们可以使用大括号导入多个项目:
import garden::plants::{ grow, water }
使用 as 重命名导入
有时,你可能想要导入两个具有相同名称的项目,或者你可能想要为导入的项目赋予不同的名称。我们可以使用 as 关键字来做到这一点:
import garden::plants::grow as grow_plant
import garden::animals::grow as grow_animal
function main() {
grow_plant()
grow_animal()
}
将模块拆分为多个文件
到目前为止,我们已经在单个文件中定义了所有模块。当模块变大时,你可能希望将它们的定义移动到单独的文件中,以便代码更易于导航。
让我们重构我们的花园示例,将每个模块放在自己的文件中。首先,让我们创建一个项目结构,如下所示:
garden.x
plants.x
animals.x
在 garden.x 中:
// garden.x
import "plants.x" as plants
import "animals.x" as animals
function main() {
plants::grow()
animals::eat()
}
在 plants.x 中:
// plants.x
export function grow() {
println("植物正在生长!")
}
在 animals.x 中:
// animals.x
export function eat() {
println("动物正在吃东西!")
}
通过使用 import 和文件路径,我们可以将代码拆分为多个文件,同时仍然能够在它们之间引用项目。
总结
X 语言的模块系统允许你:
- 使用
module关键字组织代码到模块中 - 使用绝对路径或相对路径引用项目
- 使用
export关键字使项目公开 - 使用
import关键字将路径引入作用域 - 将模块拆分为多个文件以提高可读性
模块系统是构建大型代码库的强大工具。通过适当使用模块,你可以保持代码的组织性和可维护性!
引用模块树中项目的路径
在第 5-01 章中,我们介绍了模块树的基础。在本章中,我们将更深入地讨论如何使用路径(path)引用模块树中的项目。
路径语法
引用模块树中的项目有两种方式:
- 绝对路径(absolute path)从包根(crate root)开始,以包名或
crate开头 - 相对路径(relative path)从当前模块开始,以
self、super或当前模块中的标识符开头
绝对路径和相对路径后面都跟随着一个或多个由双冒号(::)分隔的标识符。
让我们回到第 5-01 章的例子:
// package.x
name = "restaurant"
// src/main.x
mod front_of_house {
mod hosting {
function add_to_waitlist() {}
}
}
function eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist()
// 相对路径
front_of_house::hosting::add_to_waitlist()
}
在 eat_at_restaurant 中,我们可以用两种方式调用 add_to_waitlist。第一种是绝对路径:我们从 crate(包根)开始,然后列出每个连续的模块,就像在文件系统中浏览目录一样。
第二种是相对路径:它从当前模块(main.x 的根)开始,然后 front_of_house、hosting、add_to_waitlist 跟在后面。这就像文件系统中的路径 front_of_house/hosting/add_to_waitlist。
选择使用相对路径还是绝对路径是一个取决于你的项目的决定,取决于你是更倾向于将项目的定义代码与使用代码一起移动还是分开移动。例如,如果我们将 front_of_house 模块和 eat_at_restaurant 函数一起移动到一个名为 customer_experience 的模块中,我们就需要更新绝对路径,但相对路径仍然有效!但是,如果我们将 eat_at_restaurant 函数单独移动到一个名为 dining 的模块中,绝对路径将保持不变,但相对路径需要更新。我们的偏好是优先使用绝对路径,因为我们更可能希望代码的定义和使用彼此独立地移动。
使用 pub 关键字控制可见性
在第 5-01 章中,我们简要介绍了 pub 关键字,但没有深入讨论它的细节。默认情况下,X 语言中的所有内容都是私有的(private),但父模块中的项不能使用子模块中的私有项,而子模块中的项可以使用其所有祖先模块中的项。
让我们再次看看 hosting 模块。默认情况下,它是私有的。我们可以在模块前添加 pub 关键字使其公开:
mod front_of_house {
pub mod hosting {
function add_to_waitlist() {}
}
}
但是,add_to_waitlist 函数仍然是私有的!使模块公开不会使其内容公开。模块上的 pub 关键字只允许其父模块引用它,而不能访问其内部代码。因为模块是一个容器,仅仅使模块公开并没有太大作用;我们还需要选择使其中的一些项公开。
让我们通过在 add_to_waitlist 函数前添加 pub 关键字使其公开:
mod front_of_house {
pub mod hosting {
pub function add_to_waitlist() {}
}
}
function eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist()
// 相对路径
front_of_house::hosting::add_to_waitlist()
}
现在代码可以编译了!让我们看看绝对路径和相对路径为什么能够访问 add_to_waitlist,这与私有性规则有关。
因为我们在 hosting 模块和 add_to_waitlist 函数上都使用了 pub,所以我们可以从 eat_at_restaurant 访问和调用 add_to_waitlist。
pub 的其他用法
让我们看另一个例子:
mod back_of_house {
function fix_incorrect_order() {
cook_order()
super::serve_order()
}
function cook_order() {}
}
function serve_order() {}
back_of_house 模块及其函数都是私有的,但 fix_incorrect_order 可以调用同一模块中的 cook_order,因为它们在同一模块中。fix_incorrect_order 还可以使用 super:: 调用 serve_order:super:: 让我们从父模块开始,这类似于在文件系统中使用 .. 开头。
使用 use 关键字将路径引入作用域
到目前为止,我们一直通过完整路径调用函数,这可能会重复和冗长。在 X 语言中,我们可以使用 use 关键字将路径引入作用域,这样我们就可以像使用本地项一样使用它们。
让我们看看如何使用 use 将 front_of_house::hosting 模块引入作用域,这样我们就可以直接调用 hosting::add_to_waitlist:
mod front_of_house {
pub mod hosting {
pub function add_to_waitlist() {}
}
}
use crate::front_of_house::hosting
function eat_at_restaurant() {
hosting::add_to_waitlist()
}
将 hosting 模块引入作用域就像在文件系统中创建符号链接一样。通过在包根添加 use crate::front_of_house::hosting,hosting 现在在该作用域中是一个有效的名称,就像 hosting 模块是在包根中定义的一样。
我们也可以使用 use 和相对路径引入项:
use front_of_house::hosting
创建惯用的 use 路径
在前面的例子中,你可能想知道为什么我们要写 use crate::front_of_house::hosting 然后在 eat_at_restaurant 中调用 hosting::add_to_waitlist,而不是直接引入 add_to_waitlist 函数:
mod front_of_house {
pub mod hosting {
pub function add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist
function eat_at_restaurant() {
add_to_waitlist()
}
虽然两者都可以工作,但前者(引入模块)是惯用的方式。引入模块而不是单个函数可以让我们调用 hosting::add_to_waitlist() 而不是直接 add_to_waitlist(),这使得 add_to_waitlist 来自哪个模块更加清晰。
对于结构体、枚举和其他项,习惯上引入完整路径:
use std::collections::Map
function main() {
let mut map = Map::new()
map.insert(String::from("key"), String::from("value"))
}
这个习惯用法没有硬性规定:它只是一个人们习惯的约定。
使用 as 关键字提供新名称
使用 use 引入项时的另一个解决方案是在路径后使用 as 关键字和新名称:
use std::collections::Map as HashMap
function main() {
let mut map = HashMap::new()
}
使用 pub use 重新导出名称
当我们使用 use 关键字将名称引入作用域时,该名称在新作用域中是私有的。如果我们想让调用我们代码的代码能够像在该代码自己的作用域中定义那样使用该类型,我们可以将 pub 和 use 结合起来。这种技术称为“重新导出“(re-exporting),因为我们将一个项引入作用域,同时也使该项可供其他人引入他们的作用域。
mod front_of_house {
pub mod hosting {
pub function add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting
function eat_at_restaurant() {
hosting::add_to_waitlist()
}
现在,外部代码可以使用 restaurant::hosting::add_to_waitlist() 了!如果我们没有指定 pub use,eat_at_restaurant 可以在其作用域内调用 hosting::add_to_waitlist(),但外部代码不能利用这个新路径。
使用嵌套路径清理大型 use 列表
如果我们使用同一个包或模块中的多个项,为每个项单独列出一行会占用我们文件中大量的垂直空间。
use std::collections::List
use std::collections::Map
use std::collections::Set
相反,我们可以使用嵌套路径将相同的项引入作用域,但只用一行!我们可以通过指定路径的公共部分,然后是两个冒号,然后是花括号内路径的不同部分来实现,如下所示:
use std::collections::{List, Map, Set}
全局引入操作符 *
如果我们想将一个路径下的所有公共项引入作用域,我们可以使用 * 操作符:
use std::collections::*
这个 use 语句将 std::collections 中定义的所有公共项引入当前作用域。使用 * 时要小心:它会使更难分辨作用域中有哪些名称以及程序中使用的名称来自哪里。
全局引入操作符通常用于测试,用于将所有测试的内容引入 tests 模块;我们将在第 11 章“如何编写测试“中讨论这一点。它也有时用于 prelude 模式;查看标准库文档以获取该模式的更多详细信息。
总结
在本章中,我们学习了:
- 如何使用绝对路径或相对路径引用模块树中的项
- 如何使用
pub使项公开 - 如何使用
use将项引入作用域 - 惯用的引入方式
- 如何使用
as重命名引入的项 - 如何使用
pub use重新导出项 - 如何使用嵌套路径和全局引入简化
use语句
这些是模块系统的核心概念!有了这些知识,你就可以组织代码并控制哪些内容是公开可见的了。
使用列表存储多个值
集合数据结构通常可以包含多个值。与内置的元组和记录类型不同,这些集合指向存储在堆上的数据,这意味着数据量在编译时不必知道,并且可以随着程序的运行而增长或缩小。每种集合都有不同的功能和权衡,选择适合你当前情况的集合是你会随着时间积累的技能。在本章中,我们将讨论 X 语言标准库中使用的三个非常常见的集合:
- 列表:允许你存储可变数量的值,一个接一个。
- Map:允许你将一个值(键)与另一个值(值)相关联。
- Set:允许你存储唯一值的集合。
在本章中,我们将介绍列表。我们将在接下来的章节中介绍 Map 和 Set。
什么是列表?
列表是一个值的集合,所有值都具有相同的类型。X 语言使用 List<T> 表示列表类型,其中 T 是元素的类型。列表类型也有一个简写语法 [T]。
列表在许多编程语言中也被称为数组或向量。在 X 语言中,List 是标准库提供的集合类型。
创建列表
创建列表有几种方法。让我们从最简单的方法开始:使用方括号语法列出值:
let v = [1, 2, 3, 4, 5]
这将创建一个包含五个整数的新列表。列表的类型从它包含的值中推断出来;在这种情况下,v 的类型是 List<integer>。
我们也可以显式注解类型:
let v: [integer] = [1, 2, 3, 4, 5]
或者使用完整的 List<T> 语法:
let v: List<integer> = [1, 2, 3, 4, 5]
创建空列表
我们也可以创建一个空列表,然后向其中添加元素。为此,我们通常需要注解类型,因为编译器无法从空列表中推断出元素类型:
let v: List<integer> = []
使用 List::new
另一种创建空列表的方法是使用 List::new 函数:
let v: List<integer> = List::new()
读取列表元素
现在我们知道如何创建列表,让我们谈谈如何访问它们的元素。有两种方法可以引用列表中存储的值:通过索引或使用 get 方法。
索引访问
我们可以使用方括号和索引直接访问列表元素:
let v = [1, 2, 3, 4, 5]
let third: integer = v[2]
println("第三个元素是 ", third)
注意:与大多数编程语言一样,列表使用从零开始的索引,所以第一个元素在索引 0 处。
使用 get 方法
访问元素的另一种方法是使用 get 方法,它返回一个 Option<T>:
let v = [1, 2, 3, 4, 5]
let third: Option<integer> = v.get(2)
when third is {
Some(x) => println("第三个元素是 ", x),
None => println("没有第三个元素。")
}
当我们使用 get 方法时,我们得到一个 Option<T>,如果索引超出范围,它将是 None,而不是导致程序崩溃。当你尝试访问超过列表末尾的元素时,你应该使用哪种方法?这取决于你!让我们看看当我们有一个包含五个元素的列表,然后尝试访问索引为 100 的元素时会发生什么:
let v = [1, 2, 3, 4, 5]
// let does_not_exist = v[100] // 这会导致错误!
let does_not_exist = v.get(100) // 这会返回 None
当索引超出范围时,第一种方法 [] 会导致错误(我们称之为 panic)。第二种方法 get 只会返回 None,而不会导致程序崩溃。你应该选择哪种方法取决于你认为尝试访问超出列表末尾的元素是正常的、偶尔发生的情况,还是应该被视为错误的情况,在这种情况下,你希望程序停止并显示错误。
遍历列表中的元素
如果我们想依次访问列表中的每个元素,我们可以遍历所有元素,而不是使用索引一次访问一个元素。清单显示了如何使用 for 循环来获取列表中每个元素的不可变引用并打印它们。
let v = [100, 32, 57]
for i in v {
println(i)
}
我们还可以遍历可变列表中元素的可变引用,以便对所有元素进行更改:
let mutable v = [100, 32, 57]
for i in &mut v {
i = i + 50
println(i)
}
为了更改可变引用指向的值,我们需要在为其赋值之前解引用 i。我们将在第 13 章更详细地讨论解引用运算符。现在,你只需要知道 i 是对列表中每个元素的可变引用,并且你需要使用 = 来更改该引用指向的值。
常见的 List 操作
List 类型有许多有用的方法。让我们看看其中一些最常见的。
获取列表长度
我们可以使用 len 方法获取列表的长度:
let v = [1, 2, 3, 4, 5]
println("列表长度: ", v.len()) // 打印 5
检查列表是否为空
我们可以使用 is_empty 方法检查列表是否为空:
let v: List<integer> = []
println("列表为空: ", v.is_empty()) // 打印 true
向列表添加元素
虽然列表是不可变的,但我们可以通过创建包含旧元素和新元素的新列表来有效地向它们添加元素。让我们看看如何使用标准库中的方法来做到这一点:
let v = [1, 2, 3]
let v2 = List::push(v, 4) // [1, 2, 3, 4]
let v3 = List::prepend(v2, 0) // [0, 1, 2, 3, 4]
注意:这些方法返回新列表,而不是修改原始列表。这符合 X 语言对不可变性的偏好。
连接列表
我们可以使用 concat 方法连接两个列表:
let v1 = [1, 2, 3]
let v2 = [4, 5, 6]
let v3 = List::concat(v1, v2) // [1, 2, 3, 4, 5, 6]
映射列表
我们可以使用 map 方法对列表中的每个元素应用函数:
let v = [1, 2, 3]
let v2 = List::map(v, function(x) { x * 2 }) // [2, 4, 6]
过滤列表
我们可以使用 filter 方法只保留满足条件的元素:
let v = [1, 2, 3, 4, 5, 6]
let v2 = List::filter(v, function(x) { x % 2 == 0 }) // [2, 4, 6]
使用枚举存储多种类型
列表只能存储相同类型的值。这可能不方便;当然有需要存储不同类型项目列表的用例。幸运的是,枚举的变体是在同一枚举类型下定义的,所以当我们需要一种类型来表示不同类型的元素时,我们可以定义并使用枚举!
例如,假设我们想从电子表格的一行中获取值,该行中的某些列包含整数,一些包含浮点数,一些包含字符串。我们可以定义一个枚举,其变体将容纳不同类型的值,然后所有枚举变体将被视为同一类型:枚举的类型。然后我们可以创建一个该枚举类型的列表,以最终容纳不同的类型。我们在清单中演示了这一点。
type SpreadsheetCell = Integer(integer) | Float(float) | Text(string)
let row = [
Integer(3),
Float(10.12),
Text(String::from("你好"))
]
通过将枚举与列表一起使用,我们明确表示只允许哪些值进入我们的列表。此外,X 语言在编译时确切知道列表中存在哪些类型,因此我们可以安全地使用 when/is 模式匹配来处理列表中的每个元素。
for cell in row {
when cell is {
Integer(i) => println("整数: ", i),
Float(f) => println("浮点数: ", f),
Text(t) => println("文本: ", t)
}
}
如果你在编写程序时不知道程序运行时列表将需要存储的完整类型集,枚举技术将不起作用。相反,你可以使用 trait 对象,我们将在第 8 章介绍。
现在我们已经讨论了列表,让我们继续讨论 Map!
使用字符串存储 UTF-8 编码的文本
我们之前已经见过字符串了,但现在让我们更深入地了解它们。字符串在许多编程语言中是一个令人惊讶的复杂话题,X 语言也不例外。由于 X 语言对内存安全保证的关注,关于字符串的决策比其他语言更多。让我们深入研究!
什么是字符串?
首先,我们需要定义我们所说的字符串是什么意思。X 语言的核心只有一种字符串类型:字符串切片 str,它通常以借用形式 &str 出现。我们在第 3 章中谈到了字符串切片:这些是对存储在别处的 UTF-8 编码字符串数据的引用。例如,字符串字面量存储在程序的二进制文件中,因此是字符串切片。
String 类型由 X 语言标准库提供,而不是由核心语言编码,它是一个可增长、可变、拥有、UTF-8 编码的字符串类型。当 X 语言程序员在 X 语言中说“字符串“时,他们通常指的是 String 类型或字符串切片 &str 类型,而不仅仅是其中一种。尽管本章主要是关于 String 的,但这两种类型在 X 语言标准库中都被大量使用,并且 String 和 &str 都是 UTF-8 编码的。
创建新字符串
许多与 List<T> 可用的相同操作也可用于 String,因为 String 实际上是作为字节向量的包装器实现的,并添加了一些额外的保证、限制和功能。让我们看一个使用 List 的方式也适用于 String 的例子。
我们可以用 new() 函数创建一个新的空字符串,如下所示:
let mut s = String::new()
这行创建了一个名为 s 的新空字符串,然后我们可以向其中加载数据。通常,我们会有一些想要用来启动字符串的初始数据。为此,我们使用 from 函数:
let s = String::from("initial contents")
这行创建了一个包含字符串 "initial contents" 的字符串。
由于字符串被频繁使用,有很多方便的函数可以在各种情况下使用。让我们看看更多。
更新字符串
String 的大小可以增长,其内容可以更改,就像 List 的内容一样,如果你向其中推送更多数据。此外,你可以方便地使用 + 运算符或 format! 宏来组合 String 值。
使用 push_str 和 push 附加到字符串
我们可以使用 push_str 方法附加一个字符串切片来增长 String:
let mut s = String::from("foo")
s = s + "bar" // s 现在是 "foobar"
push 方法接受一个字符作为参数并将其附加到 String:
let mut s = String::from("lo")
s = s + "l" // s 现在是 "lol"
使用 + 运算符或 format! 宏组合
通常,你会想要组合两个现有的字符串。一种方法是使用 + 运算符:
let s1 = String::from("Hello, ")
let s2 = String::from("world!")
let s3 = s1 + &s2 // 注意 s1 在这里被移动,不能再使用
执行这些行后,字符串 s3 将包含 Hello, world!。
+ 运算符在幕后调用 add 函数,如下所示:
function add(self: String, s: &str) -> String {
// ...
}
我们可以在 add 函数的签名中看到 + 运算符使用了 self,这意味着 + 运算符获取了 self 的所有权。这就是为什么 s1 在加法后不再有效。
对于更复杂的字符串组合,我们可以使用 format! 宏:
let s1 = String::from("tic")
let s2 = String::from("tac")
let s3 = String::from("toe")
let s = format!("{}-{}-{}", s1, s2, s3)
执行这些行后,s 将是 tic-tac-toe。format! 宏的工作方式与 println! 类似,但它不是将输出打印到屏幕上,而是返回一个带有结果内容的 String。使用 format! 的版本也更容易阅读,并且不会获取任何参数的所有权。
字符串索引
在许多其他语言中,通过索引引用字符串中的单个字符是有效且常见的操作。然而,在 X 语言中,如果你尝试使用索引语法访问 String 的部分,你会得到一个错误。让我们看看:
let s1 = String::from("hello")
let h = s1[0] // 错误!
内部表示
String 是 List<u8> 的包装器。让我们看看:
let len = String::from("Hola").len()
在这里,len 将是 4,这意味着存储字符串 "Hola" 的向量是 4 字节长:这些字母中的每一个在 UTF-8 编码中都占 1 字节。那么下面这个呢?(注意,这个字符串以西里尔字母 Ze 开头,而不是阿拉伯数字 3。)
let len = String::from("Здравствуйте").len()
你可能期望 len 是 12,但实际上是 24:这是 UTF-8 中 “Здравствуйте” 编码所需的字节数,因为每个 Unicode 标量值在该字符串中占 2 字节。因此,字符串字节的索引并不总是与有效的 Unicode 标量值相关。为了演示,考虑以下无效的 X 语言代码:
let hello = String::from("Здравствуйте")
let answer = &hello[0]
answer 应该是什么?它应该是 З,第一个字符吗?当用 UTF-8 编码时,З 的第一个字节是 208,第二个是 151。所以 answer 实际上应该是 208,但 208 本身不是一个有效的字符。返回 208 可能不是用户想要的,但这是 X 语言在字节索引 0 处唯一可用的数据。用户通常不想要字节值,即使字符串只包含拉丁字母:即使 &"hello"[0] 是返回字节值的有效代码,它也应该返回 104,而不是 'h'。
因此,X 语言不允许这种语法,以避免意外行为并在早期防止错误。
字节、标量值和字素簇!天哪!
关于 UTF-8 的另一点是,从 X 语言的角度来看,实际上有三种相关的方式来查看字符串:作为字节、作为 Unicode 标量值、以及作为字素簇(最接近人们通常所说的“字符“)。
让我们看一下用天城文书写的印地语单词 “नमस्ते”:
नमस्ते
它最终作为 u8 值的向量存储,如下所示:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
这是 18 个字节,是计算机最终存储这些数据的方式。如果我们将它们视为 Unicode 标量值,也就是 X 语言的 character 类型,这些字节看起来像这样:
['न', 'म', 'स', '्', 'त', 'े']
这里有六个 character 值,但第四和第六个不是字母:它们本身没有意义的变音符号。最后,如果我们将它们视为字素簇,我们会得到一个人会说的组成印地语单词的四个字符:
["न", "म", "स्", "ते"]
X 语言提供了不同的方式来解释计算机存储的原始字符串数据,以便每个程序都可以选择它需要的解释,无论数据是什么人类语言。
最后一个原因是 X 语言不允许我们索引 String 以获取字符是索引操作预计总是需要常数时间(O(1))。但对于 String 不可能保证这种性能,因为 X 语言必须从开头到索引遍历内容来确定有多少有效字符。
切片字符串
索引字符串通常是一个坏主意,因为不清楚字符串索引操作的返回类型应该是什么:字节值、字符、字素簇或字符串切片。因此,X 语言要求你在真正需要创建字符串切片时更具体,而不是使用 [] 带单个数字进行索引。
为了更具体地说明你想要的内容,而不是使用带单个数字的 [] 进行索引,你可以使用带范围的 [] 来创建包含特定字节的字符串切片:
let hello = "Здравствуйте"
let s = &hello[0..4]
在这里,s 将是一个包含字符串前 4 个字节的 &str;之前我们提到这些字符中的每一个都是 2 字节,所以这意味着 s 将是 “Зд”。
如果我们尝试使用 &hello[0..1] 只切片一个字节会发生什么?答案是:X 语言会在运行时 panic,就像我们访问列表中的无效索引时一样:
let hello = "Здравствуйте"
let s = &hello[0..1] // 这会 panic!
因此,你应该谨慎使用范围来创建字符串切片,因为这样做可能会导致程序崩溃。
遍历字符串的方法
好消息是,你可以通过其他方式访问字符串中的元素。
如果你需要对单个 Unicode 标量值执行操作,最好的方法是使用 chars 方法。对 “नमस्ते” 调用 chars 会分离并返回六个 character 类型的值,你可以遍历结果以访问每个元素:
for c in "नमस्ते".chars() {
println("{}", c)
}
这将打印:
न
म
स
्
त
े
bytes 方法返回每个原始字节,这可能适合你的领域:
for b in "नमस्ते".bytes() {
println("{}", b)
}
这将打印组成此字符串的 18 个字节:
224
164
168
// --snip--
请记住,有效的 Unicode 标量值可能由超过 1 个字节组成。
从字符串中获取字素簇很复杂,因此标准库中没有提供此功能。如果你需要此功能,可以在 crates.io 上找到可用的 crate。
字符串并不简单
总而言之,字符串很复杂!不同的语言对程序员如何处理这种复杂性做出不同的选择。X 语言选择将正确处理 String 数据作为所有 X 语言程序的默认行为,这意味着程序员必须花更多时间提前思考如何处理 UTF-8 数据。这种权衡比起其他编程语言更多地暴露了字符串的复杂性,但它将防止你在开发生命周期的后期处理涉及非 ASCII 字符的错误。
好消息是,标准库提供了许多基于 String 和 &str 类型的功能,以帮助正确处理这些情况。确保查看这些函数的文档,如用于安全地附加到字符串的 push_str 等。
总结
字符串是复杂的,X 语言让你正确处理它们!这是一个权衡,但它在开发生命周期的早期暴露了可能的 UTF-8 问题,而不是允许可能导致错误的假设持续到生产中。在处理非 UTF-8 数据时,这可能会更困难,但这是值得的额外努力!
使用 Map 存储键值对
最后一个我们将讨论的常用集合是 Map。Map<K, V> 类型存储了 K 类型的键与 V 类型的值之间的映射。它通过一个称为哈希的过程来实现这一点,该过程决定了如何在内存中放置这些键和值。许多编程语言都支持这种数据结构,但通常使用不同的名称,例如 hash、map、object、hash table、dictionary 或 associative array,仅举几例。
当你想使用键(可以是任何类型,而不仅仅是整数)查找数据时,Map 很有用,而不是像列表那样使用索引。例如,在游戏中,你可以将每个团队的分数保存在 Map 中,其中每个键是团队的名称,值是团队的分数。
创建新 Map
创建空 Map 的一种方法是使用 Map::new。让我们创建一个空 Map 来存储团队名称和分数:
let scores: Map<String, integer> = Map::new()
请注意,我们需要显式注解类型,因为我们还没有插入任何值。
另一种创建 Map 的方法是使用键值对语法:
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
这将创建一个包含两个条目的 Map:“蓝队” 的分数为 10,“红队” 的分数为 50。这种语法类似于我们用于记录的语法,但有一个重要区别:Map 可以在运行时增长和缩小,而记录具有固定的字段集,在编译时已知。
读取 Map 的值
我们可以通过向 get 方法提供其键来从 Map 中获取值,如清单所示。
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
let team_name = String::from("蓝队")
let score = scores.get(team_name)
when score is {
Some(s) => println("分数: ", s),
None => println("未找到团队。")
}
在这里,score 将具有与蓝队相关联的值,结果将是 Some(10)。如果 Map 中没有该键的条目,get 将返回 None。
我们也可以像遍历列表元素那样使用 for 循环遍历 Map 中的每个键值对:
for (key, value) in scores {
println(key, ": ", value)
}
这将以任意顺序打印每个键值对:
蓝队: 10
红队: 50
更新 Map
虽然 Map(像列表一样)通常是不可变的,但我们可以创建包含新条目的新 Map。让我们看看如何更新 Map。
添加新条目
我们可以使用 Map::insert 方法向 Map 添加新条目:
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
let scores2 = Map::insert(scores, String::from("黄队"), 30)
这将创建一个包含所有原始条目加上新条目的新 Map。
覆盖值
如果我们插入一个键已经存在的条目,该键的旧值将被替换。即使我们调用 Map::insert 两次,使用相同的键但不同的值,Map 中该键只会有一个值:
let scores = { String::from("蓝队") => 10 }
let scores2 = Map::insert(scores, String::from("蓝队"), 25)
此时,scores2 将只有一个键 “蓝队”,值为 25。原始值 10 已被覆盖。
仅在键没有值时插入
通常,检查特定键是否已有值,如果没有,则为其插入一个值是很有用的。Map 为此有一个特殊的 API,称为 Map::entry,它将你要检查的键作为参数。让我们看看如何使用 entry:
let scores = { String::from("蓝队") => 10 }
let scores2 = Map::entry(scores, String::from("蓝队"), 50) // 不会覆盖
let scores3 = Map::entry(scores2, String::from("黄队"), 50) // 会插入新值
entry 方法检查键是否存在;如果存在,它会保留现有值;如果不存在,它会插入新值。
常见的 Map 操作
Map 类型有许多有用的方法。让我们看看其中一些。
获取 Map 大小
我们可以使用 len 方法获取 Map 中的条目数:
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
println("Map 大小: ", scores.len()) // 打印 2
检查键是否存在
我们可以使用 contains_key 方法检查 Map 是否包含特定键:
let scores = { String::from("蓝队") => 10 }
println("有蓝队吗? ", scores.contains_key(String::from("蓝队"))) // true
println("有黄队吗? ", scores.contains_key(String::from("黄队"))) // false
删除条目
我们可以使用 Map::remove 方法从 Map 中删除条目:
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
let scores2 = Map::remove(scores, String::from("蓝队"))
获取所有键或值
我们可以使用 keys 和 values 方法分别获取所有键或所有值的列表:
let scores = {
String::from("蓝队") => 10,
String::from("红队") => 50
}
let team_names = scores.keys() // [String::from("蓝队"), String::from("红队")]
let score_values = scores.values() // [10, 50]
Map 和所有权
对于实现 Copy trait 的类型,如 integer,值会被复制到 Map 中。对于拥有的值,如 String,值会被移动,Map 将成为这些值的所有者,如清单所示。
let field_name = String::from("最喜欢的颜色")
let field_value = String::from("蓝色")
let map = { field_name => field_value }
// field_name 和 field_value 在这里不再有效,
// 因为它们已被移动到 map 中
如果我们将对值的引用插入到 Map 中,这些值不会被移动到 Map 中。引用指向的值必须在 Map 有效的至少同一时间内有效。我们将在第 10 章讨论这些问题。
哈希函数
默认情况下,Map 使用密码学上强大的哈希函数,该函数可以抵抗拒绝服务(DoS)攻击。这不是可用的最快哈希算法,但为了安全性而放弃一点性能是值得的。如果你发现默认哈希函数对于你的目的太慢,你可以通过指定不同的 hasher 来切换到另一个函数。Hasher 是实现 Hasher trait 的类型。我们将在第 8 章讨论 trait 以及如何实现它们。
总结
列表、Map 和 Set 将涵盖你在编程中需要存储、访问和修改数据的许多常见情况。以下是一些练习,让你有机会实践我们在本章中学到的知识:
- 给定一个整数列表,使用 Map 返回它们的平均值(mean)、中位数(当排序后位于中间位置的值)和众数(出现频率最高的值)。
- 将字符串转换为 pig latin。每个单词的第一个辅音字母移到单词末尾并添加 “ay”,因此 “first” 变成 “irst-fay”。以元音开头的单词则在末尾添加 “hay”(“apple” 变成 “apple-hay”)。
- 使用 Map 和 Set,创建一个文本界面,允许用户将员工姓名添加到公司的部门中;例如,“Add Sally to Engineering” 或 “Add Amir to Sales”。然后让用户检索部门中所有人员的列表,或按部门检索公司中所有人员的列表,并进行字母排序。
标准库 API 文档中描述了这些集合类型提供的方法,因此一定要检查一下!
现在我们已经介绍了 X 语言中一些比较常见的集合,让我们继续讨论 Set。
使用 Set 存储唯一值
我们将在本章讨论的最后一个常用集合是 Set。Set<T> 类型是唯一值的集合。与列表不同,Set 保证不包含重复元素。与 Map 一样,Set 是一个用于存储值的集合,但在 Set 中,每个值都是自己的键,并且没有关联的值。
当你想确保没有重复值时,Set 很有用。例如,你可以使用 Set 来跟踪访问过的网站、事件中的唯一访客,或单词在文档中出现的唯一单词。
创建新 Set
创建空 Set 的一种方法是使用 Set::new:
let unique_numbers: Set<integer> = Set::new()
请注意,我们需要显式注解类型,因为我们还没有插入任何值。
另一种创建 Set 的方法是使用从列表或值序列创建 Set 的语法:
let unique_numbers = Set::from([1, 2, 3, 4, 5])
或者使用花括号语法:
let unique_numbers = { 1, 2, 3, 4, 5 }
这两种方法都会创建一个包含五个唯一整数的 Set。
Set 自动处理重复项
Set 最有用的特性之一是它们会自动删除重复项。让我们看看这是如何工作的:
let numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
let unique_numbers = Set::from(numbers)
此时,unique_numbers 将只包含 {1, 2, 3, 4}。所有重复项都已自动删除。
读取 Set 的值
我们可以检查 Set 中是否存在某个值,使用 contains 方法:
let numbers = { 1, 2, 3, 4, 5 }
println("有 3 吗? ", numbers.contains(3)) // true
println("有 6 吗? ", numbers.contains(6)) // false
我们也可以像遍历列表元素那样使用 for 循环遍历 Set 中的每个元素:
let numbers = { 1, 2, 3, 4, 5 }
for number in numbers {
println(number)
}
这将以任意顺序打印每个数字。
更新 Set
与其他集合一样,Set 通常是不可变的,但我们可以创建包含新元素的新 Set。
添加元素
我们可以使用 Set::insert 方法向 Set 添加元素:
let numbers = { 1, 2, 3 }
let numbers2 = Set::insert(numbers, 4)
let numbers3 = Set::insert(numbers2, 4) // 不会改变 Set,因为 4 已经存在
Set::insert 返回一个新 Set,如果元素已存在,则新 Set 与原始 Set 相同。
删除元素
我们可以使用 Set::remove 方法从 Set 中删除元素:
let numbers = { 1, 2, 3, 4, 5 }
let numbers2 = Set::remove(numbers, 3)
此时,numbers2 将包含 {1, 2, 4, 5}。
集合操作
Set 对集合操作有很好的支持,例如并集、交集、差集和对称差集。让我们看看这些如何工作。
并集
两个 Set 的并集是一个包含任一 Set 中所有元素的新 Set:
let a = { 1, 2, 3, 4 }
let b = { 3, 4, 5, 6 }
let union = Set::union(a, b) // {1, 2, 3, 4, 5, 6}
交集
两个 Set 的交集是一个包含同时出现在两个 Set 中的所有元素的新 Set:
let a = { 1, 2, 3, 4 }
let b = { 3, 4, 5, 6 }
let intersection = Set::intersection(a, b) // {3, 4}
差集
两个 Set 的差集是一个包含第一个 Set 中但不在第二个 Set 中的所有元素的新 Set:
let a = { 1, 2, 3, 4 }
let b = { 3, 4, 5, 6 }
let difference = Set::difference(a, b) // {1, 2}
对称差集
两个 Set 的对称差集是一个包含任一 Set 中但不同时在两个 Set 中的所有元素的新 Set:
let a = { 1, 2, 3, 4 }
let b = { 3, 4, 5, 6 }
let symmetric_difference = Set::symmetric_difference(a, b) // {1, 2, 5, 6}
常见的 Set 操作
Set 类型有许多有用的方法。让我们看看其中一些。
获取 Set 大小
我们可以使用 len 方法获取 Set 中的元素数:
let numbers = { 1, 2, 3, 4, 5 }
println("Set 大小: ", numbers.len()) // 打印 5
检查 Set 是否为空
我们可以使用 is_empty 方法检查 Set 是否为空:
let empty_set: Set<integer> = {}
println("Set 为空: ", empty_set.is_empty()) // true
子集和超集
我们可以检查一个 Set 是否是另一个 Set 的子集(包含在其中)或超集(包含另一个):
let a = { 1, 2, 3 }
let b = { 1, 2, 3, 4, 5 }
println("a 是 b 的子集吗? ", Set::is_subset(a, b)) // true
println("b 是 a 的超集吗? ", Set::is_superset(b, a)) // true
转换为列表
我们可以使用 to_list 方法将 Set 转换为列表:
let numbers = { 1, 2, 3, 4, 5 }
let number_list = Set::to_list(numbers) // [1, 2, 3, 4, 5] 或其他顺序
Set 实际应用示例
让我们看一个 Set 在实际中有用的示例。假设我们正在构建一个网站跟踪器,想要跟踪访问过我们网站的唯一用户:
type Visitor = {
id: String,
name: String
}
// 创建一些访问者
let alice = { id: String::from("1"), name: String::from("Alice") }
let bob = { id: String::from("2"), name: String::from("Bob") }
let charlie = { id: String::from("3"), name: String::from("Charlie") }
// 跟踪唯一访问者
let mutable unique_visitors = { alice, bob }
// Alice 再次访问 - 不会添加重复项
unique_visitors = Set::insert(unique_visitors, alice)
// Charlie 第一次访问
unique_visitors = Set::insert(unique_visitors, charlie)
println("唯一访问者数量: ", unique_visitors.len()) // 3
在这个示例中,即使 Alice 访问了两次,她在 Set 中也只被计算一次。
总结
Set 是 X 语言标准库中一个强大的集合类型。它们:
- 自动处理重复值
- 支持快速查找操作
- 提供标准集合操作,如并集、交集和差集
- 对于跟踪唯一值或执行集合数学运算非常有用
列表、Map 和 Set 共同为你提供了在 X 语言中管理数据集合所需的大多数工具。
现在我们已经介绍了 X 语言中的常见集合,让我们继续讨论 X 语言处理错误的方式!
无异常的错误处理
错误是软件中不可避免的事实,因此 X 语言有一些处理错误发生情况的特性。在许多情况下,X 语言要求你承认错误的可能性,并在代码编译之前采取一些行动。这个要求使你的程序更加健壮;它确保你在将代码部署到生产环境之前发现错误并适当地处理它们!
X 语言将错误分为两大类:可恢复的和不可恢复的错误。对于可恢复的错误,例如未找到文件,我们很可能只想将问题报告给用户并重试操作。不可恢复的错误总是 bug 的症状,例如尝试访问超出数组末尾的位置,因此我们希望立即停止程序。
大多数语言不会区分这两类错误,而是使用异常等机制统一处理它们。X 语言没有异常。相反,它有用于可恢复错误的 Result<T, E> 类型和用于在遇到不可恢复错误时停止执行的 panic 宏。本章将首先介绍调用 panic,然后讨论返回 Result<T, E> 值。此外,我们将探讨在决定是尝试从错误中恢复还是 panic 时的注意事项。
Panic:处理不可恢复的错误
有时,会发生一些不好的事情,而你对此无能为力。在这些情况下,X 语言有 panic 宏。当 panic 宏执行时,你的程序会打印一条错误消息,展开并清理堆栈,然后退出。这种情况最常发生在检测到某种类型的 bug 并且程序员不清楚如何处理该错误时。
简单的 panic!
让我们看一个简单的程序,它调用 panic:
function main() {
panic("crash and burn")
}
当你运行这个程序时,你会看到类似这样的内容:
panic: crash and burn
该程序将立即退出,并显示给定的消息。
什么时候应该 panic?
决定何时应该 panic 以及何时应该返回 Result 可能很困难。让我们讨论一些可以帮助你做出决定的指导原则。
示例、原型设计和测试
在示例代码中,使用 panic(通常通过 unwrap 或 expect)通常是可以接受的。示例应该清晰易读,而 unwrap 和 expect 有助于实现这一点。
在原型设计时,在你决定如何处理错误情况之前,unwrap 和 expect 也是方便的占位符。它们明确标记了你稍后想要返回并实现正确错误处理的位置。
在测试中,如果测试失败,panic 是可以接受的——事实上,这正是测试检查预期行为的方式。
你比编译器知道得更多
在某些情况下,你可以确定 Result 将是 Ok,即使一般情况下不能保证。在这些情况下,使用 unwrap 或 expect 是可以接受的。这是一个例子:
let home: IpAddr = "127.0.0.1".parse().unwrap()
我们正在解析一个硬编码的字符串 "127.0.0.1",我们知道它是一个有效的 IP 地址。因此,在这里使用 unwrap 是可以接受的。但是,如果字符串来自用户输入,你应该正确处理 Result 而不是使用 unwrap。
不可恢复的错误
panic 的主要用例是当你遇到不可恢复的错误时——即程序无法继续执行的错误。这可能是由于:
- 代码中的 bug(例如,数组索引越界)
- 外部系统出现严重问题(例如,磁盘已满且无法写入)
- 违反了你的代码所依赖的不变量(例如,某人向你的函数传递了无效输入)
在这些情况下,继续执行可能比停止更糟糕,因此 panic 是合适的。
可恢复错误与不可恢复错误的指导原则
那么,你如何决定是返回 Result 还是 panic?这里有一些指导原则:
-
预计在正常情况下可能会失败的操作应该返回
Result。- 解析用户输入
- 连接到网络
- 打开可能不存在的文件
-
永远不应该失败的操作可以
panic。- 访问硬编码的有效索引处的数组元素
- 解析你知道有效的字符串
- 违反内部不变量
-
在原型设计中,你可以使用
unwrap或expect作为临时措施。 -
在示例代码中,
panic或unwrap/expect通常是可以接受的,因为它们使示例更清晰。
总结
panic宏停止程序执行并显示错误消息。- 在示例、原型设计和测试中使用
panic(通常通过unwrap或expect)通常是可以接受的。 - 对于你比编译器知道得更多并且可以保证
Result是Ok的情况,unwrap和expect也很有用。 - 对于预计在正常情况下可能会失败的操作,返回
Result而不是panic。 - 对于不可恢复的错误,使用
panic。
在本章中,我们介绍了 panic,它用于处理不可恢复的错误。结合上一章关于 Option 和 Result 的内容,你现在拥有了在 X 语言中处理各种错误情况所需的工具!
接下来,让我们学习泛型和 trait,它们允许你定义适用于多种类型的代码。
Result:处理可恢复的错误
在上一章中,我们研究了 Option,它表示一个值可能存在也可能不存在。在本章中,我们将研究 Result,它类似于 Option,但它表示一个操作可能成功也可能失败。
Result 是处理可恢复错误的 X 语言方式——你通常希望向用户报告这种错误并重试操作。让我们看看 Result 是什么,以及如何使用它。
什么是 Result?
Result 类型表示操作的结果:每个 Result 要么是 Ok,表示成功并包含一个值,要么是 Err,表示失败并包含一个错误值。Result 在 X 语言标准库中定义,如下所示:
type Result<T, E> = Ok(T) | Err(E)
Result 有两个泛型类型参数:T 表示成功时包含在 Ok 变体中的值的类型,E 表示失败时包含在 Err 变体中的错误的类型。
让我们看一个使用 Result 的简单示例。回想一下第 2 章,我们有一个从字符串解析数字的函数:
function parse_number(s: String) -> Result<integer, String> {
// 让我们假装这是从字符串解析整数
if s == String::from("42") {
Ok(42)
} else {
Err(String::from("无效的数字"))
}
}
在这个例子中,parse_number 函数返回一个 Result<integer, String>。如果输入是字符串 “42”,它返回 Ok(42)。否则,它返回 Err(String::from("无效的数字"))。
为什么 Result 有用?
像 Option 一样,Result 比在其他语言中常见的使用异常或特殊值的方法更好,因为它强制你在编译时处理错误情况。编译器会确保你不会忘记处理 Err 情况,这有助于防止在运行时出现意外的错误。
让我们看看如何使用 Result 值。
使用 Result
与 Option 一样,我们可以使用 when/is 表达式来处理 Result 值:
let result = parse_number(String::from("42"))
when result is {
Ok(n) => println("解析的数字: ", n),
Err(e) => println("解析错误: ", e)
}
在这个例子中,如果结果是 Ok,我们打印解析的数字。如果是 Err,我们打印错误消息。
匹配 Result 的简写:if let
与 Option 一样,我们可以使用 if let 语法作为使用 when/is 处理 Result 的简写:
let result = parse_number(String::from("42"))
if let Ok(n) = result {
println("解析的数字: ", n)
} else if let Err(e) = result {
println("解析错误: ", e)
}
Result 的常用方法
与 Option 一样,Result 类型有许多有用的方法可以让你的生活更轻松。让我们看看其中一些最常见的。
is_ok 和 is_err
is_ok 方法在 Result 为 Ok 时返回 true,在为 Err 时返回 false。is_err 正好相反:
let ok_result = Ok(42)
let err_result: Result<integer, String> = Err(String::from("出错了"))
println(ok_result.is_ok()) // true
println(ok_result.is_err()) // false
println(err_result.is_ok()) // false
println(err_result.is_err()) // true
ok
ok 方法将 Result<T, E> 转换为 Option<T>,将 Ok 映射到 Some,将 Err 映射到 None:
let ok_result = Ok(42)
let option_from_ok = Result::ok(ok_result) // Some(42)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let option_from_err = Result::ok(err_result) // None
err
err 方法将 Result<T, E> 转换为 Option<E>,将 Err 映射到 Some,将 Ok 映射到 None:
let ok_result = Ok(42)
let option_from_ok = Result::err(ok_result) // None
let err_result: Result<integer, String> = Err(String::from("出错了"))
let option_from_err = Result::err(err_result) // Some(String::from("出错了"))
unwrap
unwrap 方法返回 Ok 内部的值,但如果 Result 是 Err,它会 panic:
let ok_result = Ok(42)
let value = ok_result.unwrap() // 42
let err_result: Result<integer, String> = Err(String::from("出错了"))
// err_result.unwrap() // 这会 panic!
与 Option 上的 unwrap 一样,通常最好在示例或快速原型设计之外避免使用它。
expect
expect 方法与 unwrap 类似,但它允许你提供自定义的 panic 消息:
let ok_result = Ok(42)
let value = ok_result.expect("应该有一个值") // 42
let err_result: Result<integer, String> = Err(String::from("出错了"))
// err_result.expect("应该有一个值") // 这会 panic 并显示我们的消息!
map
map 方法接受一个函数,并将其应用于 Result 内部的成功值(如果存在):
let ok_result = Ok(42)
let new_result = Result::map(ok_result, function(x) { x * 2 }) // Ok(84)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let new_err = Result::map(err_result, function(x) { x * 2 }) // Err(...)
map_err
map_err 方法接受一个函数,并将其应用于 Result 内部的错误值(如果存在):
let ok_result = Ok(42)
let new_result = Result::map_err(ok_result, function(e) { String::from("错误: ") + e }) // Ok(42)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let new_err = Result::map_err(err_result, function(e) { String::from("错误: ") + e }) // Err("错误: 出错了")
and_then
and_then 方法类似于 map,但它接受一个返回 Result 的函数。当你有一个返回 Result 的函数链时,这很有用:
function divide(x: integer, y: integer) -> Result<integer, String> {
if y == 0 {
Err(String::from("除以零"))
} else {
Ok(x / y)
}
}
let ok_result = Ok(8)
let result = Result::and_then(ok_result, function(x) { divide(x, 2) }) // Ok(4)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let no_result = Result::and_then(err_result, function(x) { divide(x, 2) }) // Err(...)
or
or 方法返回第一个 Result(如果它是 Ok),否则返回第二个 Result:
let ok_result = Ok(42)
let fallback = Ok(0)
let result = Result::or(ok_result, fallback) // Ok(42)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let fallback = Ok(0)
let result_with_fallback = Result::or(err_result, fallback) // Ok(0)
or_else
or_else 方法类似于 or,但它接受一个函数,该函数仅在第一个 Result 是 Err 时才被调用以计算 fallback Result:
let ok_result = Ok(42)
let result = Result::or_else(ok_result, function() { Ok(0) }) // Ok(42)
let err_result: Result<integer, String> = Err(String::from("出错了"))
let result_with_fallback = Result::or_else(err_result, function() { Ok(0) }) // Ok(0)
当 fallback 的计算成本很高时,这很有用。
unwrap_or
unwrap_or 方法返回 Ok 内部的值,或者如果是 Err,则返回提供的默认值:
let ok_result = Ok(42)
let value = Result::unwrap_or(ok_result, 0) // 42
let err_result: Result<integer, String> = Err(String::from("出错了"))
let value_with_default = Result::unwrap_or(err_result, 0) // 0
unwrap_or_else
unwrap_or_else 方法类似于 unwrap_or,但它接受一个函数,该函数仅在 Result 是 Err 时才被调用以计算默认值:
let ok_result = Ok(42)
let value = Result::unwrap_or_else(ok_result, function(e) { 0 }) // 42
let err_result: Result<integer, String> = Err(String::from("出错了"))
let value_with_default = Result::unwrap_or_else(err_result, function(e) { 0 }) // 0
传播错误
在编写代码时,你经常会发现自己编写的函数调用可能会失败的其他函数。你可以让错误传播,而不是在函数内部处理错误,这样调用者就可以决定如何处理它。让我们看一个例子:
function parse_and_double(s: String) -> Result<integer, String> {
let result = parse_number(s)
when result is {
Ok(n) => Ok(n * 2),
Err(e) => Err(e)
}
}
在这个例子中,parse_and_double 调用 parse_number,如果成功,它将结果加倍。如果失败,它会将错误传播给调用者。
这种模式在 X 语言代码中非常常见。我们可以使用 when/is 模式匹配来传播错误,但这有点冗长。
组合 Result 和 Option
你经常会发现自己同时使用 Result 和 Option。幸运的是,它们有很好的组合方式。例如,你可以使用 ok 方法将 Result 转换为 Option,或者使用 ok_or 方法(如果可用)将 Option 转换为 Result。
总结
Result 类型是 X 语言处理可恢复错误的方式。它比异常更安全,因为编译器会强制你在编译时处理错误情况。以下是一些关键点:
Result<T, E>可以是Ok(T)或Err(E)- 使用
when/is以类型安全的方式处理Result值 if let是简单情况的简写Result有许多有用的方法,如map、and_then、unwrap_or等- 你可以将错误传播给调用者
- 谨慎使用
unwrap和expect,因为它们可能会 panic
在本章中,我们介绍了 Result,它用于处理可恢复的错误。在下一章中,我们将介绍 panic,它用于处理不可恢复的错误。
Option:表示可能不存在的值
在第 4 章中,我们简要介绍了 Option 枚举作为一种编码一个值可以存在或不存在的方式的方法。在本章中,我们将更详细地探讨 Option,包括它是什么、如何使用以及如何在你的代码中充分利用它。
什么是 Option?
Option 类型表示可选值:每个 Option 要么是 Some,其中包含一个值,要么是 None,表示没有值。Option 在 X 语言标准库中定义,如下所示:
type Option<T> = Some(T) | None
<T> 语法表示 Option 枚举是泛型的,这意味着 Some 变体可以保存任何类型的数据。我们将在第 8 章更详细地介绍泛型。
让我们看一个使用 Option 的简单示例:
let some_number = Some(5)
let some_string = Some(String::from("一个字符串"))
let absent_number: Option<integer> = None
在第一行中,我们创建了一个 Option<integer> 类型的 Some 值,并将其绑定到变量 some_number。在第二行中,我们创建了一个 Option<String> 类型的 Some 值。在第三行中,我们创建了一个 None 值,它表示没有任何值。对于 None,我们需要告诉 X 语言我们想要什么类型的 Option,因为编译器无法仅通过查看 None 值就推断出 Some 变体将持有的类型。
为什么 Option 有用?
你可能想知道为什么我们需要 Option 类型。其他语言使用空值或 nil 来表示没有值,这有什么问题呢?
问题在于,如果你尝试将空值用作非空值,你会得到某种错误。因为这个空或非空属性无处不在,很容易犯这种错误。空值的发明者 Tony Hoare 将其称为“十亿美元的错误“。
X 语言没有空值,但它有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,它由标准库定义。Option<T> 如此常用,以至于它甚至包含在 prelude 中。它的变体也包含在 prelude 中:你可以直接使用 Some 和 None,而不需要 Option:: 前缀。
Option<T> 比空值好在哪里?简而言之,因为 Option<T> 和 T(其中 T 可以是任何类型)是不同的类型,编译器不会让我们使用 Option<T> 值,就好像它绝对是一个有效值一样。例如,这段代码不会编译,因为它试图将一个 Option<integer> 加到一个 integer 上:
let x: integer = 5
let y: Option<integer> = Some(5)
let sum = x + y // 错误!不能将 integer 和 Option<integer> 相加
实际上,这个错误消息意味着 X 语言不理解如何将 Option<integer> 和 integer 相加,因为它们是不同的类型。当我们在 X 语言中有一个像 integer 这样类型的值时,编译器将确保我们始终有一个有效值。我们可以放心地进行操作,而不必在使用该值之前检查空值。只有当我们有一个 Option<integer>(或我们正在使用的任何类型的 Option)时,我们才必须担心可能没有值,编译器会确保我们在使用该值之前处理这种情况。
换句话说,在你可以对 Option<T> 进行 T 操作之前,你必须将其转换为 T。一般来说,这有助于捕获空值最常见的问题之一:假设某事不是空的,而实际上它是空的。
使用 Option
现在我们知道 Option 是什么以及为什么它有用,让我们看看如何实际使用它!我们可以使用 when/is 表达式来处理 Option 值,就像我们在第 4 章看到的那样:
function plus_one(x: Option<integer>) -> Option<integer> {
when x is {
None => None,
Some(i) => Some(i + 1)
}
}
let five = Some(5)
let six = plus_one(five)
let none = plus_one(None)
在这个例子中,plus_one 函数接受一个 Option<integer> 并返回一个 Option<integer>。如果输入是 None,它返回 None。如果输入是 Some(i),它返回 Some(i + 1)。
匹配 Option 的简写:if let
when/is 表达式对于处理 Option 值非常强大,但有时对于简单情况可能有点冗长。对于只想在值为 Some 时执行某些操作的情况,X 语言提供了 if let 语法作为简写:
let some_value = Some(3)
if let Some(i) = some_value {
println("值是 ", i)
}
这段代码与使用 when/is 相同,但更简洁:
let some_value = Some(3)
when some_value is {
Some(i) => println("值是 ", i),
None => ()
}
我们也可以在 if let 中包含 else 子句,以在值为 None 时执行某些操作:
let some_value: Option<integer> = None
if let Some(i) = some_value {
println("值是 ", i)
} else {
println("没有值")
}
Option 的常用方法
Option 类型有许多有用的方法可以让你的生活更轻松。让我们看看其中一些最常见的。
is_some 和 is_none
is_some 方法在 Option 为 Some 时返回 true,在为 None 时返回 false。is_none 正好相反:
let some_value = Some(3)
let no_value: Option<integer> = None
println(some_value.is_some()) // true
println(some_value.is_none()) // false
println(no_value.is_some()) // false
println(no_value.is_none()) // true
unwrap
unwrap 方法返回 Some 内部的值,但如果 Option 是 None,它会 panic:
let some_value = Some(3)
let value = some_value.unwrap() // 3
let no_value: Option<integer> = None
// no_value.unwrap() // 这会 panic!
因为 unwrap 可能会 panic,所以通常最好在示例或快速原型设计之外避免使用它。对于生产代码,你应该更喜欢显式处理 None 情况,或者使用 expect 方法,它允许你提供自定义的 panic 消息。
expect
expect 方法与 unwrap 类似,但它允许你提供自定义的 panic 消息:
let some_value = Some(3)
let value = some_value.expect("应该有一个值") // 3
let no_value: Option<integer> = None
// no_value.expect("应该有一个值") // 这会 panic 并显示我们的消息!
像 unwrap 一样,expect 应该谨慎使用。
map
map 方法接受一个函数,并将其应用于 Option 内部的值(如果存在):
let some_value = Some(3)
let new_value = Option::map(some_value, function(x) { x * 2 }) // Some(6)
let no_value: Option<integer> = None
let new_none = Option::map(no_value, function(x) { x * 2 }) // None
and_then
and_then 方法类似于 map,但它接受一个返回 Option 的函数。当你有一个返回 Option 的函数链时,这很有用:
function divide_two(x: integer) -> Option<integer> {
if x % 2 == 0 {
Some(x / 2)
} else {
None
}
}
let some_value = Some(8)
let result = Option::and_then(some_value, divide_two) // Some(4)
let odd_value = Some(7)
let no_result = Option::and_then(odd_value, divide_two) // None
or
or 方法返回第一个 Option(如果它是 Some),否则返回第二个 Option:
let some_value = Some(3)
let fallback = Some(5)
let result = Option::or(some_value, fallback) // Some(3)
let no_value: Option<integer> = None
let fallback = Some(5)
let result_with_fallback = Option::or(no_value, fallback) // Some(5)
unwrap_or
unwrap_or 方法返回 Some 内部的值,或者如果是 None,则返回提供的默认值:
let some_value = Some(3)
let value = Option::unwrap_or(some_value, 0) // 3
let no_value: Option<integer> = None
let value_with_default = Option::unwrap_or(no_value, 0) // 0
unwrap_or_else
unwrap_or_else 方法类似于 unwrap_or,但它接受一个函数,该函数仅在 Option 为 None 时才被调用以计算默认值:
let some_value = Some(3)
let value = Option::unwrap_or_else(some_value, function() { 0 }) // 3
let no_value: Option<integer> = None
let value_with_default = Option::unwrap_or_else(no_value, function() { 0 }) // 0
当默认值的计算成本很高时,这很有用,因为你只想在需要时计算它。
总结
Option 类型是 X 语言处理可能不存在的值的方式。它比空值更安全,因为编译器会强制你在使用值之前处理 None 情况。以下是一些关键点:
Option<T>可以是Some(T)或None- 使用
when/is以类型安全的方式处理Option值 if let是简单情况的简写Option有许多有用的方法,如map、and_then、unwrap_or等- 谨慎使用
unwrap和expect,因为它们可能会 panic
在本章中,我们介绍了 Option,它用于表示可能不存在的值。在下一章中,我们将介绍 Result,它用于处理可能失败的操作。
泛型数据类型
我们可以使用泛型来定义可用于多种具体类型的函数、类型和其他构造。让我们先看看如何定义函数、结构体、枚举和方法,然后再讨论 trait 以及它们如何与泛型协同工作。
首先,让我们回顾一下如何编写一个找出列表中最大整数的函数。
function largest_integer(list: List<integer>) -> integer {
let mutable largest = list[0]
for item in list {
if item > largest {
largest = item
}
}
largest
}
这个函数接受一个 List<integer> 作为参数,并返回列表中的最大 integer。如果我们想找出字符列表或浮点数列表中的最大值,该怎么办?我们必须为每种类型复制并粘贴相同的逻辑!
为了避免这种重复,我们可以使用泛型类型参数来定义一个可以处理任何类型的函数。泛型允许我们抽象出处理特定类型的细节,使我们的代码更灵活和可重用。
在函数定义中使用泛型
当我们使用泛型定义函数时,我们将泛型类型参数放在函数签名中,位于函数名称之后和参数列表之前,用尖括号 <> 括起来。
让我们使用泛型重写 largest 函数,使其可以处理任何类型:
function largest<T>(list: List<T>) -> T {
let mutable largest = list[0]
for item in list {
if item > largest {
largest = item
}
}
largest
}
等等,这里有个问题!我们的 largest 函数现在使用了类型 T,但我们不知道任何类型 T 都可以与 > 运算符比较。为了使这个函数工作,T 需要实现比较 trait。我们将在下一章讨论 trait 时回到这个问题。但首先,让我们探讨一下如何在其他地方使用泛型类型参数。
目前,让我们看看如何在结构体定义中使用泛型。
在结构体定义中使用泛型
我们也可以在一个或多个字段中使用泛型类型参数来定义结构体。语法与函数定义类似:我们将类型参数名称放在结构体名称之后的尖括号中。
type Point<T> = {
x: T,
y: T
}
这个 Point 结构体是泛型的,适用于某种类型 T,并且它有两个字段 x 和 y,它们都是同一类型 T。让我们创建 Point 实例:
let integer_point = { x: 5, y: 10 }
let float_point = { x: 1.0, y: 4.0 }
注意,因为我们只在 Point 结构体的定义中使用了一个泛型类型参数,所以 x 和 y 必须是同一类型。如果我们尝试创建一个 x 是整数而 y 是浮点数的 Point 实例,这将无法工作。
如果我们希望 x 和 y 是不同类型的,我们可以使用多个泛型类型参数:
type Point<T, U> = {
x: T,
y: U
}
现在 x 和 y 可以是不同类型:
let both_integer = { x: 5, y: 10 }
let both_float = { x: 1.0, y: 4.0 }
let integer_and_float = { x: 5, y: 4.0 }
在枚举定义中使用泛型
正如我们在第 4 章中看到的,我们也可以在枚举中使用泛型类型参数来让枚举的变体持有泛型数据类型。让我们回顾一下标准库提供的 Option<T> 枚举:
type Option<T> = Some(T) | None
这个定义现在应该更容易理解了。Option 是一个泛型枚举,适用于某种类型 T,并且它有两个变体:Some,它持有一个 T 类型的值,和 None,它不持有任何值。通过使用 Option 枚举,我们可以表达一个可选值的概念,而且因为 Option 是泛型的,我们可以将这个概念用于任何类型。
正如我们在第 7 章中看到的,Result 枚举是另一个泛型枚举。Result 有两个类型参数:T 和 E:
type Result<T, E> = Ok(T) | Err(E)
Result 枚举用于操作可能成功(返回类型 T 的值)或失败(返回类型 E 的错误值)。
在方法定义中使用泛型
我们也可以在方法定义中使用泛型。让我们为 Point<T> 结构体实现一个方法,名为 x,它返回对字段 x 中数据的引用:
type Point<T> = {
x: T,
y: T
}
function Point::x<T>(self: &Point<T>) -> &T {
&self.x
}
let p = { x: 5, y: 10 }
println("p.x = ", p.x())
注意,我们必须在 impl 之后声明 T,以便我们可以在我们正在为其实现方法的类型 Point<T> 上使用它。通过在 impl 之后的尖括号中声明 T 为泛型类型,X 语言可以识别出尖括号中的类型是泛型类型而不是具体类型。
我们还可以指定对泛型类型的约束,只允许具有特定属性的类型拥有方法。我们将在下一章中讨论如何使用 trait 来做到这一点。
泛型代码的性能
你可能想知道在使用泛型类型参数时是否会有运行时成本。好消息是,X 语言实现泛型的方式使得使用泛型的代码不会比使用具体类型的代码慢!
X 语言通过在编译时对使用泛型的代码进行单态化来实现这一点。单态化是通过填充编译时使用的具体类型将通用代码转换为特定代码的过程。
在这个过程中,编译器做了我们在本章开始时为了创建 largest 函数的两个版本而做的相反的事情:编译器采用通用定义,并为实际代码中使用的具体类型生成特定定义。
例如,让我们看看标准库中的 Option 是如何工作的:
let integer = Some(5)
let float = Some(5.0)
当 X 语言编译这段代码时,它会执行单态化。在这个过程中,编译器读取已在 Option 实例中使用的值,并标识出两种 Option:一种是 integer,另一种是 Float。因此,它将通用定义 Option<T> 扩展为两个特定于类型的定义,用具体类型替换 T。
单态化的结果是,泛型的特定版本就像我们手动复制了定义一样。X 语言的泛型系统意味着我们编写的代码更少,但最终运行的代码更多。
总结
在本章中,我们学习了如何使用泛型类型参数来避免代码重复。我们已经看到了如何在函数、结构体、枚举和方法中使用泛型类型参数!
接下来,让我们讨论 trait,我们可以用它来定义多个类型共有的行为。我们可以将 trait 与泛型类型参数结合使用,将泛型类型参数限制为仅具有特定行为的类型,而不是任何类型。
Trait:定义共享行为
Trait 类似于其他语言中通常称为“接口“的特性,尽管有一些差异。Trait 允许我们以一种抽象的方式定义共享行为。我们可以使用 trait 约束来指定泛型类型是具有特定行为的任何类型。
注意:Trait 类似于其他语言中的接口,但有一些差异。
定义 Trait
类型的行为由我们可以对该类型调用的方法组成。不同类型共享相同的行为,如果我们可以对所有这些类型调用相同的方法。Trait 定义是一种将方法签名分组在一起的方法,用于定义实现某些目的所需的一组行为。
例如,假设我们有多个类型,它们都持有某种文本:NewsArticle 类型持有新闻故事,Tweet 类型持有最多 280 个字符的推文,以及可能有一些元数据。我们希望对这些类型中的每一个都能够显示摘要。因此,我们希望能够对每个类型调用 summarize 方法来获取该摘要。让我们看看如何在 trait 中表达这一点。
trait Summary {
function summarize(self: &Self) -> String
}
在这里,我们使用 trait 关键字声明一个 trait,后面跟着 trait 的名称,在这种情况下是 Summary。在大括号内部,我们声明描述实现这个 trait 的类型的行为的方法签名,在这种情况下是 function summarize(self: &Self) -> String。
在方法签名之后,而不是在大括号内提供一个实现,我们使用分号。每个实现这个 trait 的类型都必须提供自己的 summarize 方法的自定义行为。编译器将强制任何具有 Summary trait 的类型都具有完全此签名的 summarize 方法。
实现 Trait 在类型上
现在我们已经定义了 Summary trait,我们可以在我们的媒体聚合器类型上实现它。
type NewsArticle = {
headline: String,
location: String,
author: String,
content: String
}
impl Summary for NewsArticle {
function summarize(self: &NewsArticle) -> String {
String::format("{}-{}, by {} ({})", self.headline, self.location, self.author, self.content)
}
}
type Tweet = {
username: String,
content: String,
reply: boolean,
retweet: boolean
}
impl Summary for Tweet {
function summarize(self: &Tweet) -> String {
String::format("{}: {}", self.username, self.content)
}
}
在类型上实现 trait 类似于实现常规方法,只是我们在 impl 之后添加 trait 名称,然后使用 for 关键字,后面跟着我们正在为其实现 trait 的类型的名称。在 impl 块中,我们放置 trait 定义中定义的方法签名。我们不是在每个签名后添加分号,而是在大括号中填写方法体,以指定我们希望 trait 的方法对特定类型具有的行为。
一旦我们实现了 trait,我们就可以像调用非 trait 方法一样在 NewsArticle 和 Tweet 的实例上调用 trait 方法:
let tweet = {
username: String::from("horse_ebooks"),
content: String::from("当然,伙计们,你可能已经知道了"),
reply: false,
retweet: false
}
println("1 条新推文: {}", tweet.summarize())
这个代码打印 1 条新推文: horse_ebooks: 当然,伙计们,你可能已经知道了。
Trait 作为参数
现在我们知道如何定义和实现 trait 了,让我们看看如何使用 trait 来定义接受许多不同类型的函数。例如,我们可以定义一个 notify 函数,该函数在其 item 参数上调用 summarize 方法,该参数是实现 Summary trait 的某种类型。为此,我们使用 impl Trait 语法:
function notify(item: impl Summary) {
println("突发新闻!{}", item.summarize())
}
我们可以使用 impl 语法,而不是具体类型作为参数的类型,而是使用 trait 名称。这个参数接受实现了我们指定的 trait 的任何类型。在 notify 函数体中,我们可以调用来自 Summary trait 的任何方法,包括 summarize。我们可以调用 notify 并传入 NewsArticle 或 Tweet 的实例。使用具体类型(如 String 或 integer)调用此函数的代码将无法编译,因为这些类型不实现 Summary。
Trait Bound 语法
impl Trait 语法是 trait bound 的语法糖,看起来像这样:
function notify<T: Summary>(item: T) {
println("突发新闻!{}", item.summarize())
}
这与上一个示例等效,但稍微冗长一些。我们将 trait bound 与泛型类型参数的声明放在一起,放在尖括号内,在冒号之后。
使用 trait bound 语法的 impl Trait 语法很方便,在简单的情况下使代码更简洁。trait bound 语法可以在更复杂的情况下表达更多内容,例如,我们可以强制两个参数具有相同的类型。这只有在使用 trait bound 时才有可能:
function notify<T: Summary>(item1: T, item2: T) {
// ...
}
我们指定的泛型类型 T 同时指定了 item1 和 item2 的类型,它们必须是实现 Summary trait 的相同具体类型。如果我们使用 impl Trait 语法,我们不能这样做。
通过 + 指定多个 Trait Bound
我们也可以指定多个 trait bound。例如,如果我们想要求 notify 中的 item 既具有 summarize 方法又具有 Display trait,我们可以使用 + 语法:
function notify(item: impl Summary + Display) {
// ...
}
+ 语法也与 trait bound 上的泛型类型参数一起使用:
function notify<T: Summary + Display>(item: T) {
// ...
}
通过这两个 trait bound,notify 的主体可以调用 summarize 并使用 {} 格式化 item。
通过 where 子句简化 Trait Bound
使用多个 trait bound 可能会有很多括号,每个泛型类型的 trait bound 列表可能会变得很长且难以阅读。出于这个原因,X 语言在函数签名之后有一个可选的 where 子句用于 trait bound。所以与其这样写:
function some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> integer {
// ...
}
我们可以使用 where 子句:
function some_function<T, U>(t: T, u: U) -> integer
where T: Display + Clone,
U: Clone + Debug
{
// ...
}
这个函数签名不那么杂乱:函数名、参数列表和返回类型彼此靠近,类似于没有许多 trait bound 的函数。
返回实现 Trait 的类型
我们还可以在返回位置使用 impl Trait 语法,以返回实现 trait 的某种类型的值:
function returns_summarizable() -> impl Summary {
{
username: String::from("horse_ebooks"),
content: String::from("当然,伙计们,你可能已经知道了"),
reply: false,
retweet: false
}
}
通过使用 impl Summary 作为返回类型,我们指定 returns_summarizable 函数返回实现 Summary trait 的某种类型,但没有指定具体类型。在这个例子中,returns_summarizable 返回一个 Tweet,但调用该函数的代码不需要知道这一点。
能够在闭包和迭代器的上下文中指定仅通过 impl Trait 语法知道实现某个 trait 的返回类型特别有用,我们将在第 10 章中介绍。闭包和迭代器创建只有编译器知道或指定起来非常冗长的类型。impl Trait 语法允许你简洁地指定一个函数返回实现 Iterator trait 的某种类型。
但是,你只能在返回单个类型时使用 impl Trait。例如,如果你有返回 NewsArticle 或 Tweet 的代码,两者都实现 Summary,那么你不能使用 impl Summary 作为返回类型。
使用 Trait Bound 有条件地实现方法
通过在 impl 块上使用带有泛型类型参数的 trait bound,我们可以有条件地仅针对实现指定 trait 的类型实现方法。
type Pair<T> = {
x: T,
y: T
}
function Pair::new<T>(x: T, y: T) -> Pair<T> {
{ x: x, y: y }
}
impl<T: Display + PartialOrd> Pair<T> {
function cmp_display(self: &Pair<T>) {
if self.x >= self.y {
println("最大的成员是 x = {}", self.x)
} else {
println("最大的成员是 y = {}", self.y)
}
}
}
Pair<T> 类型总是实现 new 函数。但是,只有当 T 实现了允许比较的 PartialOrd trait 和允许打印的 Display trait 时,它才会实现 cmp_display 方法。
我们也可以有条件地为实现另一个 trait 的任何类型实现 trait。在实现 trait 时,在满足 trait bound 的任何类型上实现 trait 称为全覆盖实现,它们在 X 语言标准库中被广泛使用。例如,标准库为实现 Display trait 的任何类型实现 ToString trait。标准库中的这个 impl 块看起来类似于以下代码:
impl<T: Display> ToString for T {
// --snip--
}
因为标准库有这个全覆盖实现,我们可以在实现 Display trait 的任何类型上调用 ToString trait 中定义的 to_string 方法。例如,我们可以将整数变成字符串,因为整数实现了 Display:
let s = 3.to_string()
总结
Trait 和 trait bound 允许我们以抽象的方式定义共享行为,并让我们在不导致代码重复的情况下利用这种共享行为。它们还允许我们指定泛型类型将具有特定行为,而不仅仅是任何类型。
现在我们已经讨论了 X 语言中泛型和 trait 的一些主要特性,让我们继续讨论类和面向对象编程!
使用生命周期验证引用
在本章中,我们将讨论生命周期——X 语言的一个特性,它允许我们确保引用的有效性,而不仅仅是所有权。
我们已经了解了借用,并且知道 X 语言有一个借用检查器,它比较作用域以确保所有借用都是有效的。
生命周期防止悬空引用
生命周期的主要目标是防止悬空引用,这会导致程序引用它不应该引用的数据。考虑下面的程序,它有一个外部作用域和一个内部作用域。
let r;
{
let x = 5;
r = &x;
} // x 超出作用域
println("r: {}", r); // 错误!r 引用了一个已被释放的值
在这里,我们尝试在 x 超出作用域后使用引用 r。X 语言会阻止这种情况,使用其借用检查器确保引用在被引用的数据超出作用域之前不会超出作用域。
生命周期注解语法
生命周期注解不会改变任何引用的存活时间。相反,它们描述了多个引用的生命周期之间的关系,而不影响生命周期。就像当函数接受具有泛型类型参数的任何类型时一样,函数可以通过声明泛型生命周期参数来接受具有任何生命周期的引用。
生命周期注解有一个稍微不寻常的语法:生命周期参数的名称必须以撇号(')开头,并且通常全是小写和短的,就像泛型类型一样。大多数人使用 'a 作为第一个生命周期参数。让我们看看生命周期注解的语法在各个位置的样子。
首先,让我们回顾一下,我们可以通过生命周期注解来命名引用的生命周期。
// 没有生命周期注解
function first_word(s: &String) -> &str {
// ...
}
// 带有生命周期注解
function first_word<'a>(s: &'a String) -> &'a str {
// ...
}
函数签名中的生命周期注解与泛型类型参数相关联:它们在函数名称后面的尖括号中声明。
生命周期注解的目的是告诉借用检查器多个引用的生命周期之间的关系。我们想要表达的约束是,只要生命周期 'a 存在,参数和返回值都必须有效。
函数签名中的生命周期
让我们看一些生命周期注解的例子:
// 一个没有生命周期参数的函数
// 我们返回与输入相同生命周期的引用
function longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
在这个例子中,我们有一个接受两个字符串切片并返回一个字符串切片的函数。生命周期注解表示返回的引用将与两个输入引用中寿命较短的那个一样长。这是有道理的,因为我们不知道我们会在运行时返回 x 还是 y。
当我们从函数返回一个引用时,返回类型的生命周期参数需要与其中一个参数的生命周期参数匹配。如果返回的引用不引用任何参数,它必须引用函数内部创建的值,这将是一个悬空引用,因为该值将在函数结束时超出作用域。
// 这是一个错误的尝试,返回一个引用
// 到函数内部创建的值
function longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str() // 错误!result 在函数结束时超出作用域
}
即使我们在这个函数中有一个生命周期参数 'a,这也不会编译,因为返回值没有与任何参数相关联。问题是 result 在函数结束时超出作用域,所以我们不能从函数中返回对它的引用。
最好的解决方案是返回一个拥有的数据类型而不是引用,这样调用函数就会负责清理值。
结构体定义中的生命周期
到目前为止,我们定义的所有结构体都只拥有类型。我们也可以定义包含引用的结构体,但在这种情况下,我们需要在结构体定义中每个引用的类型上添加生命周期注解。
type ImportantExcerpt<'a> = {
part: &'a str
}
这个结构体有一个字段 part,它包含一个字符串切片——这是一个引用。与泛型类型一样,我们在结构体名称后面的尖括号中声明泛型生命周期参数的名称,以便我们可以在结构体定义中使用生命周期参数。
这个注解意味着 ImportantExcerpt 的实例不能比它在 part 字段中持有的引用存活时间更长。
下面是一个使用这个结构体的示例:
let novel = String::from("这是一个很长的小说。它有很多句子...")
let first_sentence = novel.split('.').next().expect("找不到 '.'")
let i = {
part: first_sentence
}
println("重要摘录: {}", i.part)
在这里,我们创建了一个包含对 novel 字符串切片的引用的 ImportantExcerpt 实例。novel 在 ImportantExcerpt 创建之前就存在了,并且 novel 在 ImportantExcerpt 超出作用域之后才超出作用域,所以 ImportantExcerpt 中的引用是有效的。
生命周期省略
到目前为止,你已经了解到,每次使用引用时都需要显式标注生命周期,这可能会很重复和烦人。好消息是,对于某些常见情况,X 语言允许我们省略生命周期注解,使用一组称为生命周期省略规则的模式。
让我们看看这些规则是如何工作的。首先,一些定义:
- 输入生命周期:函数或方法参数上的生命周期
- 输出生命周期:函数或方法返回值上的生命周期
生命周期省略规则是一组借用检查器应用的规则,以确定何时可以省略生命周期注解。这些规则不是完整的推断;它们是一组特定的情况,如果你的代码符合这些情况,你就不需要编写生命周期。
让我们看看三个生命周期省略规则。第一条规则适用于输入生命周期,第二和第三条规则适用于输出生命周期。
规则 1
分配给每个引用参数的生命周期都是它自己的生命周期。换句话说,一个有一个参数的函数得到一个生命周期参数:function foo<'a>(x: &'a i32);有两个参数的函数得到两个独立的生命周期参数:function foo<'a, 'b>(x: &'a i32, y: &'b i32);等等。
规则 2
如果只有一个输入生命周期参数,那么该生命周期被分配给所有输出生命周期参数:function foo<'a>(x: &'a i32) -> &'a i32。
规则 3
如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,因为这是一个方法,那么 self 的生命周期被分配给所有输出生命周期参数。
让我们看看这些规则在实践中是如何工作的。让我们从我们在本章早些时候使用的 first_word 函数开始:
// 没有生命周期注解:
function first_word(s: &String) -> &str {
// ...
}
// 应用规则后:
function first_word<'a>(s: &'a String) -> &'a str {
// ...
}
规则 1 说我们给每个引用参数自己的生命周期。让我们称之为 'a,就像往常一样。
规则 2 说,因为只有一个输入生命周期参数,输出生命周期也被赋值为 'a,所以签名现在看起来像我们在前面的例子中看到的那样。
让我们看看当我们以 longest 函数开始时会发生什么,它没有生命周期注解:
// 没有生命周期注解:
function longest(x: &str, y: &str) -> &str {
// ...
}
// 应用规则 1 后:
function longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// ...
}
// 但是规则 2 不适用,因为有两个输入生命周期,而不是一个。
// 所以我们需要显式注解生命周期!
规则 1 适用,因为我们有两个引用参数,所以我们给每个参数自己的生命周期。但是规则 2 不适用,因为有多个输入生命周期。规则 3 也不适用,因为 longest 是一个函数而不是方法。在所有规则都应用完之后,我们仍然不知道返回类型的生命周期是什么!这就是为什么在这种情况下我们需要显式注解生命周期的原因。
方法定义中的生命周期注解
当我们在具有生命周期的结构体上实现方法时,我们可以再次省略生命周期注解。让我们看看:
type ImportantExcerpt<'a> = {
part: &'a str
}
impl<'a> ImportantExcerpt<'a> {
function level(self: &Self) -> integer {
3
}
function announce_and_return_part(self: &Self, announcement: &str) -> &str {
println("注意!{}", announcement)
self.part
}
}
首先,注意我们不必在 impl 之后声明生命周期,因为我们可以让编译器根据结构体定义推断它们。
第一个方法 level,不需要任何生命周期注解,因为规则 3 适用(这是一个方法,所以我们可以省略生命周期注解)。
第二个方法 announce_and_return_part,有两个参数和一个返回类型。因为这是一个方法,规则 3 适用,所以我们不需要在这个方法中注解生命周期!让我们看看为什么,首先不写生命周期注解:
function announce_and_return_part(self: &Self, announcement: &str) -> &str {
// ...
}
规则 1 说我们给 self 和 announcement 都分配自己的生命周期。规则 3 说,因为 self 是一个参数,返回类型的生命周期与 self 相同,所以我们可以省略所有生命周期注解!太棒了!
静态生命周期
一个特殊的生命周期值得讨论:'static,它表示整个程序的持续时间。例如,所有字符串字面量都有 'static 生命周期,我们可以注解如下:
let s: &'static str = "我有一个静态生命周期";
这个字符串的文本直接存储在程序的二进制文件中,它总是可用的。因此,所有字符串字面量的生命周期都是 'static。
在使用 'static 生命周期注解之前,先想想你所引用的引用是否真的会在程序的整个生命周期内都存在,以及你是否希望它这样做。大多数情况下,问题在于尝试创建一个悬空引用或可用生命周期不匹配;在这些情况下,解决方案是修复这些问题,而不是指定 'static 生命周期。
总结
生命周期是让 X 语言的借用检查器在没有垃圾回收的情况下工作的一部分,同时确保引用是有效的。尽管大多数时候生命周期是隐式和推断的,但我们必须在多个引用的生命周期可能以不同方式相互关联时注解它们。
通过显式注解函数签名中的生命周期,我们告诉编译器返回的引用将与参数中最短的引用一样长。这确保了我们不会意外创建悬空引用或其他无效引用!
面向对象编程
X 语言支持面向对象编程(OOP)范式,通过类、对象、继承等概念实现。本章将详细介绍 X 语言中的面向对象编程特性。
9.1 类定义
类是面向对象编程的核心构造,用于封装数据和行为。在 X 语言中,使用 class 关键字定义类。
基本语法
class ClassName {
// 字段声明
let field1: Type
let mutable field2: Type = defaultValue
// 构造函数
new(parameters) {
// 初始化代码
}
// 方法
function methodName(parameters) -> ReturnType {
// 方法实现
}
}
字段声明
字段是类中存储数据的成员,分为不可变和可变两种:
- 不可变字段:使用
let关键字声明,初始化后不能修改 - 可变字段:使用
let mutable关键字声明,可以在对象生命周期内修改 - 默认值:字段可以在声明时指定默认值
class Person {
let name: String
let mutable age: Integer = 0
let isAdult: Boolean = age >= 18
}
构造函数
构造函数使用 new 关键字声明,用于初始化新创建的对象:
- 构造函数负责初始化对象的字段
- 可以有多个构造函数(重载)
- 子类构造函数必须调用父类构造函数
class Person {
let name: String
let age: Integer
new(name: String, age: Integer) {
this.name = name
this.age = age
}
// 便捷构造函数
new(name: String) {
this.name = name
this.age = 0
}
}
创建对象
要创建类的实例,使用类名加构造函数参数:
let person = Person("Alice", 30)
let baby = Person("Bob")
9.2 继承
X 语言支持单继承,即每个类最多只能有一个父类。子类继承父类的非私有字段和方法。
基本语法
class ChildClass extends ParentClass {
// 子类特有的字段和方法
}
方法重写
子类可以重写父类的虚方法,使用 override 关键字标记:
- 只有标记为
virtual的方法才能被重写 - 重写方法的签名必须与父类方法兼容
- 使用
super关键字调用父类方法
class Vehicle {
let mutable speed: Float = 0.0
virtual function accelerate(amount: Float) {
speed = speed + amount
}
function describe() -> String = "Vehicle at speed {speed}"
}
class Car extends Vehicle {
let brand: String
new(brand: String) {
this.brand = brand
}
override function accelerate(amount: Float) {
super.accelerate(amount * 1.5)
}
override function describe() -> String = "{brand} car at speed {speed}"
}
抽象类和抽象方法
抽象类使用 abstract 关键字标记,不能直接实例化:
- 抽象类可以包含抽象方法
- 抽象方法只有签名,没有实现
- 子类必须实现所有抽象方法
abstract class Animal {
abstract function speak() -> String
function greet() -> String = "I say: {speak()}"
}
class Dog extends Animal {
override function speak() -> String = "Woof!"
}
class Cat extends Animal {
override function speak() -> String = "Meow!"
}
9.3 方法
方法是类中定义的函数,用于实现对象的行为。
方法类型
X 语言支持多种类型的方法:
- 普通方法:默认不可被子类重写
- 虚方法:使用
virtual关键字标记,可被子类重写 - 抽象方法:使用
abstract关键字标记,无实现 - 静态方法:使用
static关键字标记,属于类而非实例 - 最终方法:使用
final关键字标记,禁止子类重写
class Calculator {
// 普通方法
function add(a: Integer, b: Integer) -> Integer {
a + b
}
// 虚方法
virtual function multiply(a: Integer, b: Integer) -> Integer {
a * b
}
// 静态方法
static function square(x: Integer) -> Integer {
x * x
}
// 最终方法
final function divide(a: Integer, b: Integer) -> Float {
a.toFloat() / b.toFloat()
}
}
方法调用
方法通过对象实例调用,或者对于静态方法,通过类名调用:
let calc = Calculator()
let sum = calc.add(5, 3) // 8
let product = calc.multiply(4, 6) // 24
let squared = Calculator.square(7) // 49
9.4 访问修饰符
X 语言提供了四种访问修饰符,用于控制类成员的可见性:
| 修饰符 | 同类 | 子类 | 同模块 | 外部 |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
internal | ✓ | ✓ | ✓ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
默认情况下,类成员的访问修饰符为 private。
示例
class Account {
private let id: Integer
protected let mutable balance: Float
public let owner: String
internal let createdAt: String
public new(id: Integer, owner: String, initialBalance: Float) {
this.id = id
this.owner = owner
this.balance = initialBalance
this.createdAt = "2023-01-01"
}
public function deposit(amount: Float) {
balance = balance + amount
}
public function withdraw(amount: Float) -> Boolean {
if amount > 0 && balance >= amount {
balance = balance - amount
true
} else {
false
}
}
private function validate() -> Boolean {
balance >= 0.0
}
}
9.5 接口(Trait)
X 语言使用 trait 实现接口功能,定义一组行为契约:
- Trait 可以包含方法签名和默认实现
- 类通过
implement关键字实现 trait - 一个类可以实现多个 trait
trait Printable {
function show() -> String
}
trait Comparable<T> {
function compareTo(other: T) -> Integer
function lessThan(other: T) -> Boolean = compareTo(other) < 0
function greaterThan(other: T) -> Boolean = compareTo(other) > 0
}
class User implement Printable, Comparable<User> {
let name: String
let age: Integer
new(name: String, age: Integer) {
this.name = name
this.age = age
}
function show() -> String = "User({name}, {age})"
function compareTo(other: User) -> Integer = this.age - other.age
}
9.6 多态与动态分发
X 语言支持运行时多态,通过动态分发实现:
- 方法调用根据对象的实际类型选择实现
- 支持向上转型(子类对象赋值给父类类型)
- 遵循最具体实现优先原则
class Shape {
virtual function area() -> Float = 0.0
}
class Circle extends Shape {
let radius: Float
new(radius: Float) {
this.radius = radius
}
override function area() -> Float = 3.14159 * radius * radius
}
class Rectangle extends Shape {
let width: Float
let height: Float
new(width: Float, height: Float) {
this.width = width
this.height = height
}
override function area() -> Float = width * height
}
// 多态示例
let shapes: List<Shape> = [Circle(5.0), Rectangle(3.0, 4.0)]
for shape in shapes {
println(shape.area()) // 运行时分发到对应子类的实现
}
9.7 最佳实践
类设计原则
- 单一职责原则:一个类应该只负责一项职责
- 封装原则:隐藏内部实现细节,只暴露必要的接口
- 继承原则:只有当子类真正是父类的一种特殊类型时才使用继承
- 组合优先:优先使用组合而不是继承来实现功能
代码风格
- 使用 PascalCase 命名类名
- 使用 camelCase 命名方法和字段
- 保持方法简洁,每个方法只做一件事
- 使用访问修饰符明确控制可见性
9.8 总结
X 语言的面向对象编程特性包括:
- 类定义:使用
class关键字定义类,包含字段和方法 - 继承:支持单继承,通过
extends关键字实现 - 方法:支持普通方法、虚方法、抽象方法、静态方法和最终方法
- 访问控制:提供
public、protected、internal和private四种访问修饰符 - 接口:通过 trait 实现接口功能
- 多态:支持运行时多态和动态分发
这些特性使 X 语言能够构建复杂的面向对象系统,同时保持代码的清晰性和可维护性。
类与对象
X 语言支持多种编程范式:函数式、声明式、过程式,以及面向对象编程(OOP)。在本章中,我们将探讨 X 语言中的面向对象特性:类、对象、继承等。
什么是面向对象编程?
面向对象的程序由对象组成。对象将数据和操作该数据的过程包装在一起。这些过程通常被称为方法或操作。
对于这个定义,X 语言是面向对象的:
- 结构体和枚举可以包含数据
impl块提供了可以在结构体和枚举上调用的方法- 类(我们将在本章中讨论)提供了更传统的 OOP 特性
让我们首先看看 X 语言中的类是什么。
定义类
X 语言中的类使用 class 关键字定义。一个类可以包含字段(数据)和方法(函数)。让我们从一个简单的 Animal 类开始:
class Animal {
let name: String
let age: integer
constructor(name: String, age: integer) {
this.name = name
this.age = age
}
function make_sound(self: &Self) {
println("动物发出声音!")
}
function get_name(self: &Self) -> String {
self.name.clone()
}
function get_age(self: &Self) -> integer {
self.age
}
}
让我们分解这个类定义:
- 字段声明:
let name: String和let age: integer声明了类的字段(数据成员)。 - 构造函数:
constructor(name: String, age: integer)是一个特殊方法,用于初始化类的新实例。this关键字指的是正在创建的实例。 - 方法:
make_sound、get_name和get_age是可以在类实例上调用的方法。self: &Self参数指的是方法被调用的实例(类似于其他语言中的this或self)。
创建对象
要创建类的实例(对象),我们像调用函数一样调用构造函数:
let animal = Animal::new(String::from("Buddy"), 5)
println("动物的名字是: ", animal.get_name())
println("动物的年龄是: ", animal.get_age())
animal.make_sound()
这将打印:
动物的名字是: Buddy
动物的年龄是: 5
动物发出声音!
可变对象
默认情况下,对象是不可变的。要修改对象的字段,我们需要声明对象为可变的,并且在类中提供修改字段的方法:
class MutableAnimal {
let mutable name: String
let mutable age: integer
constructor(name: String, age: integer) {
this.name = name
this.age = age
}
function set_name(self: &mut Self, new_name: String) {
self.name = new_name
}
function have_birthday(self: &mut Self) {
self.age = self.age + 1
println("生日快乐!现在 ", self.name, " 是 ", self.age, " 岁了!")
}
function get_name(self: &Self) -> String {
self.name.clone()
}
function get_age(self: &Self) -> integer {
self.age
}
}
现在我们可以创建一个可变对象并修改它:
let mutable mutable_animal = MutableAnimal::new(String::from("Buddy"), 5)
println("原始名字: ", mutable_animal.get_name())
mutable_animal.set_name(String::from("Max"))
println("新名字: ", mutable_animal.get_name())
mutable_animal.have_birthday()
这将打印:
原始名字: Buddy
新名字: Max
生日快乐!现在 Max 是 6 岁了!
封装
面向对象编程的一个关键原则是封装:对象的内部细节对外部代码是隐藏的。只有对象的公共方法是可访问的。
在 X 语言中,默认情况下,类字段和方法是私有的。要使它们公开,我们使用 public 关键字:
class BankAccount {
let mutable balance: integer // 私有字段
constructor(initial_balance: integer) {
this.balance = initial_balance
}
public function deposit(self: &mut Self, amount: integer) {
if amount > 0 {
this.balance = this.balance + amount
}
}
public function withdraw(self: &mut Self, amount: integer) -> boolean {
if amount > 0 && this.balance >= amount {
this.balance = this.balance - amount
true
} else {
false
}
}
public function get_balance(self: &Self) -> integer {
this.balance
}
}
在这个例子中:
balance字段是私有的,不能从类外部直接访问deposit、withdraw和get_balance方法是公共的,可以从外部调用withdraw方法确保在允许提款之前账户有足够的资金
这封装了银行账户的内部状态并强制执行业务规则:
let mutable account = BankAccount::new(1000)
println("初始余额: ", account.get_balance()) // 1000
account.deposit(500)
println("存款后: ", account.get_balance()) // 1500
let success = account.withdraw(300)
println("提款成功: ", success) // true
println("提款后: ", account.get_balance()) // 1200
// account.balance // 错误!无法访问私有字段
类与结构体
你可能想知道什么时候应该使用类,什么时候应该使用结构体。这里有一些指导原则:
使用类的情况:
- 你需要继承
- 你需要封装(私有字段)
- 你正在建模具有身份和行为的对象
- 你需要传统的面向对象特性
使用结构体的情况:
- 你只需要简单的数据容器
- 你不需要继承
- 你想要更轻量级的东西
- 你主要使用函数式风格
总结
X 语言通过以下方式支持面向对象编程:
- 使用
class关键字定义类 - 具有字段(数据)和方法(行为)的对象
- 使用构造函数初始化对象
- 使用
this或self引用当前实例 - 封装与公共/私有可见性
- 可变对象与可变字段和方法
在下一章中,我们将探讨继承,这是另一个关键的面向对象特性!
继承
继承是面向对象编程中的一个机制,其中一个类基于另一个类,从它继承属性和方法。继承的类称为子类或派生类,被继承的类称为超类或基类。
继承促进了代码重用,并允许创建分层的类分类法。让我们看看继承在 X 语言中是如何工作的。
基本继承
要在 X 语言中声明一个继承自另一个类的类,我们在子类名后使用 extends 关键字,后跟超类名。
class Animal {
let name: String
let age: integer
constructor(name: String, age: integer) {
this.name = name
this.age = age
}
function make_sound(self: &Self) {
println("动物发出声音!")
}
function get_name(self: &Self) -> String {
self.name.clone()
}
function get_age(self: &Self) -> integer {
self.age
}
}
class Dog extends Animal {
let breed: String
constructor(name: String, age: integer, breed: String) {
super(name, age) // 调用超类构造函数
this.breed = breed
}
override function make_sound(self: &Self) {
println("汪汪!")
}
function get_breed(self: &Self) -> String {
self.breed.clone()
}
function fetch(self: &Self) {
println(self.name, " 正在去拿球!")
}
}
让我们看看这里发生了什么:
extends Animal:声明Dog继承自Animalsuper(name, age):调用超类的构造函数来初始化继承的字段override:标记make_sound方法为覆盖超类中的方法- 新字段和方法:
Dog添加了breed字段、get_breed方法和fetch方法
使用子类
现在我们可以创建 Dog 的实例并使用继承和新的方法:
let dog = Dog::new(String::from("Fido"), 3, String::from("金毛寻回犬"))
// 继承自 Animal 的方法
println("名字: ", dog.get_name()) // Fido
println("年龄: ", dog.get_age()) // 3
// 覆盖的方法
dog.make_sound() // 汪汪!
// Dog 特有的方法
println("品种: ", dog.get_breed()) // 金毛寻回犬
dog.fetch() // Fido 正在去拿球!
多态
继承的一个强大特性是多态——子类的实例可以被视为超类的实例。
function make_animal_sound(animal: &Animal) {
animal.make_sound()
}
let animal = Animal::new(String::from("Generic"), 5)
let dog = Dog::new(String::from("Fido"), 3, String::from("金毛寻回犬"))
make_animal_sound(&animal) // 动物发出声音!
make_animal_sound(&dog) // 汪汪!
尽管 make_animal_sound 函数接受 &Animal,我们可以传递 &Dog,因为 Dog 继承自 Animal。并且调用的 make_sound 方法是实际类型的方法——这就是多态!
覆盖方法
正如我们看到的,子类可以覆盖超类的方法,以提供特定于子类的行为。override 关键字是必需的,以明确我们有意覆盖一个方法。
class Cat extends Animal {
constructor(name: String, age: integer) {
super(name, age)
}
override function make_sound(self: &Self) {
println("喵喵!")
}
function purr(self: &Self) {
println(self.name, " 正在发出咕噜声!")
}
}
let cat = Cat::new(String::from("Whiskers"), 2)
cat.make_sound() // 喵喵!
cat.purr() // Whiskers 正在发出咕噜声!
调用超类方法
有时你可能想在覆盖的方法中调用超类的实现。你可以使用 super 关键字来做到这一点:
class LoudDog extends Dog {
constructor(name: String, age: integer, breed: String) {
super(name, age, breed)
}
override function make_sound(self: &Self) {
super.make_sound() // 调用 Dog.make_sound()
println("汪汪汪!!!")
}
}
let loud_dog = LoudDog::new(String::from("Buddy"), 4, String::from("比格犬"))
loud_dog.make_sound()
// 输出:
// 汪汪!
// 汪汪汪!!!
保护成员
有时你希望子类可以访问成员,但不能从类外部公开访问。对于这种情况,X 语言有 protected 可见性:
class Vehicle {
protected let mutable speed: integer
let max_speed: integer
constructor(max_speed: integer) {
this.speed = 0
this.max_speed = max_speed
}
public function accelerate(self: &mut Self) {
if this.speed < this.max_speed {
this.speed = this.speed + 10
}
}
public function get_speed(self: &Self) -> integer {
this.speed
}
}
class Car extends Vehicle {
let model: String
constructor(max_speed: integer, model: String) {
super(max_speed)
this.model = model
}
public function honk(self: &Self) {
println("嘟嘟!这是一辆 ", this.model)
}
public function emergency_stop(self: &mut Self) {
this.speed = 0 // 可以访问受保护的字段
}
}
let car = Car::new(120, String::from("轿车"))
car.accelerate()
println("速度: ", car.get_speed()) // 10
// car.speed // 错误!无法从外部访问受保护的字段
car.emergency_stop()
println("紧急停车后: ", car.get_speed()) // 0
继承层级
你可以创建多级继承的继承层级:
class Animal { /* ... */ }
class Mammal extends Animal { /* ... */ }
class Dog extends Mammal { /* ... */ }
class GoldenRetriever extends Dog { /* ... */ }
但要小心——深层继承层级可能变得难以理解和维护。通常更喜欢组合而不是继承。
继承与组合
虽然继承很强大,但通常最好使用组合——在类中包含其他类的实例,而不是继承它们:
// 使用继承
class Vehicle {
let engine: Engine
// ...
}
// 使用组合(通常更好)
class Vehicle {
let engine: Engine
let wheels: List<Wheel>
// ...
}
经验法则是:当存在“is-a“关系时使用继承,当存在“has-a“关系时使用组合。
总结
X 语言中的继承提供:
- 使用
extends进行类继承 - 使用
super调用超类构造函数和方法 - 使用
override覆盖方法 - 多态(子类可以用作超类)
- 用于子类访问的
protected可见性 - 继承层级
但要记住,继承并不总是最好的工具——通常更喜欢组合而不是继承!
在下一章中,我们将讨论抽象类,这是一种不能直接实例化但可以被子类化的特殊类。
抽象类
抽象类是不能直接实例化的类——相反,它们旨在被子类化。抽象类可以包含抽象方法——声明但没有实现的方法。子类必须为所有抽象方法提供实现,或者它们自己也必须是抽象的。
定义抽象类
要在 X 语言中声明一个抽象类,我们使用 abstract 关键字:
abstract class Shape {
let color: String
constructor(color: String) {
this.color = color
}
// 抽象方法:没有实现
abstract function area(self: &Self) -> Float
// 具体方法:有实现
function get_color(self: &Self) -> String {
self.color.clone()
}
function set_color(self: &mut Self, color: String) {
self.color = color
}
}
抽象类可以包含:
- 字段(与普通类相同)
- 构造函数(与普通类相同)
- 抽象方法(使用
abstract关键字,没有实现) - 具体方法(有实现)
注意,我们不能直接实例化抽象类:
// let shape = Shape::new(String::from("red")) // 错误!无法实例化抽象类
实现抽象类
要使用抽象类,我们必须创建一个继承自它的具体子类,并为所有抽象方法提供实现:
class Circle extends Shape {
let radius: Float
constructor(color: String, radius: Float) {
super(color)
this.radius = radius
}
// 必须实现抽象方法 area()
override function area(self: &Self) -> Float {
3.14159 * self.radius * self.radius
}
function get_radius(self: &Self) -> Float {
self.radius
}
}
class Rectangle extends Shape {
let width: Float
let height: Float
constructor(color: String, width: Float, height: Float) {
super(color)
this.width = width
this.height = height
}
// 必须实现抽象方法 area()
override function area(self: &Self) -> Float {
self.width * self.height
}
function get_width(self: &Self) -> Float {
self.width
}
function get_height(self: &Self) -> Float {
self.height
}
}
现在我们可以实例化具体的子类:
let circle = Circle::new(String::from("red"), 5.0)
let rectangle = Rectangle::new(String::from("blue"), 4.0, 6.0)
println("圆的颜色: ", circle.get_color()) // red
println("圆的面积: ", circle.area()) // ~78.53975
println("矩形的颜色: ", rectangle.get_color()) // blue
println("矩形的面积: ", rectangle.area()) // 24.0
多态与抽象类
抽象类对于多态特别有用——我们可以将子类的实例视为抽象类的实例:
function print_shape_info(shape: &Shape) {
println("形状颜色: ", shape.get_color())
println("形状面积: ", shape.area())
}
let circle = Circle::new(String::from("red"), 5.0)
let rectangle = Rectangle::new(String::from("blue"), 4.0, 6.0)
print_shape_info(&circle)
print_shape_info(&rectangle)
抽象类与 Trait
你可能想知道什么时候应该使用抽象类,什么时候应该使用 trait。这里有一些指导原则:
使用抽象类的情况:
- 你想要在相关类之间共享代码
- 你需要在方法之间共享状态(字段)
- 你想要定义一个类层次结构的基础
- 你需要构造函数
使用 trait 的情况:
- 你想要将行为附加到不相关的类
- 你想要指定可以由任何类实现的契约
- 你想要支持多重继承的行为(一个类可以实现多个 trait)
- 你不需要共享状态
另一个抽象类示例:游戏角色
让我们看一个更具体的例子——游戏中的角色:
abstract class GameCharacter {
let name: String
let mutable health: integer
let max_health: integer
constructor(name: String, max_health: integer) {
this.name = name
this.max_health = max_health
this.health = max_health
}
// 所有角色都可以攻击,但攻击方式不同
abstract function attack(self: &Self, target: &mut GameCharacter)
// 所有角色都有相同的受伤方式
function take_damage(self: &mut Self, amount: integer) {
if self.health > amount {
self.health = self.health - amount
println(this.name, " 受到了 ", amount, " 点伤害!")
} else {
self.health = 0
println(this.name, " 被击败了!")
}
}
function is_alive(self: &Self) -> boolean {
self.health > 0
}
function get_name(self: &Self) -> String {
self.name.clone()
}
function get_health(self: &Self) -> integer {
self.health
}
}
class Warrior extends GameCharacter {
let weapon_damage: integer
constructor(name: String) {
super(name, 100)
this.weapon_damage = 25
}
override function attack(self: &Self, target: &mut GameCharacter) {
println(this.name, " 用剑攻击!")
target.take_damage(this.weapon_damage)
}
}
class Mage extends GameCharacter {
let spell_power: integer
let mutable mana: integer
constructor(name: String) {
super(name, 70)
this.spell_power = 30
this.mana = 50
}
override function attack(self: &Self, target: &mut GameCharacter) {
if this.mana >= 10 {
println(this.name, " 施放火球术!")
target.take_damage(this.spell_power)
this.mana = this.mana - 10
} else {
println(this.name, " 法力不足!")
}
}
}
现在我们可以有不同类型的角色进行战斗:
let mutable warrior = Warrior::new(String::from("Conan"))
let mutable mage = Mage::new(String::from("Gandalf"))
warrior.attack(&mut mage)
mage.attack(&mut warrior)
println(warrior.get_name(), " 的生命值: ", warrior.get_health())
println(mage.get_name(), " 的生命值: ", mage.get_health())
总结
抽象类在 X 语言中:
- 使用
abstract class声明 - 不能直接实例化
- 可以包含抽象方法(使用
abstract function) - 可以包含具体方法(有实现)
- 可以有字段和构造函数
- 必须被子类化,并且子类必须实现所有抽象方法
- 对于多态和定义类层次结构非常有用
抽象类与 trait 类似,但它们可以共享状态,并且旨在用于相关类的层次结构。
现在我们已经介绍了 X 语言中的面向对象特性,让我们继续讨论函数式编程特性!
闭包
X 语言支持函数式编程特性,包括闭包——可以捕获其环境中值的匿名函数。在本章中,我们将了解闭包是什么、它们如何工作以及何时使用它们。
什么是闭包?
闭包是可以存储在变量中或作为参数传递给其他函数的匿名函数。与函数不同,闭包可以捕获它们定义的作用域中的值。
让我们从一个简单的闭包示例开始:
let add_one = function(x) { x + 1 }
println(add_one(5)) // 6
这里,add_one 是一个接受一个参数 x 并返回 x + 1 的闭包。闭包的语法类似于函数,但有一些区别:
function关键字(可选的,在某些上下文中)- 参数周围的括号
- 箭头
=>(可选的,取决于语法) - 没有显式类型注解(通常可以推断)
闭包类型推断
与函数不同,闭包通常不需要你注解参数或返回值的类型。类型是从闭包的使用方式推断出来的。
// 闭包不强制类型注解
let add = function(x, y) { x + y }
// 第一次使用确定类型
let result = add(5, 10) // add 现在是 integer -> integer -> integer
println(result) // 15
但是,如果你想显式注解类型,你可以这样做:
let add: function(integer, integer) -> integer = function(x, y) { x + y }
捕获环境
闭包的一个强大特性是它们可以捕获其环境——它们定义的作用域中的变量。
function main() {
let x = 4
let equal_to_x = function(z) { z == x }
let y = 4
println(equal_to_x(y)) // true
}
这里,equal_to_x 闭包从其环境中捕获了变量 x。它将 x 的值与它接受的参数 z 进行比较。
捕获方式:借用与移动
闭包可以通过三种方式捕获它们的环境,对应于函数获取参数的三种方式:不可变借用、可变借用和获取所有权。闭包会自动确定使用哪种方式,具体取决于它如何使用捕获的值。
不可变借用
如果闭包只读取值,它将通过不可变借用捕获它:
let list = [1, 2, 3]
let only_borrows = function() { println("我借用了列表: {:?}", list) }
only_borrows() // 我借用了列表: [1, 2, 3]
可变借用
如果闭包修改值,它将通过可变借用捕获它:
let mutable list = [1, 2, 3]
let mutable borrows_mutably = function() { list = list + [4] }
mut_borrows_mutably()
println(list) // [1, 2, 3, 4]
移动
如果闭包获取值的所有权(例如,如果它将值返回或将其移动到别处),它将通过移动捕获值:
let list = [1, 2, 3]
let takes_ownership = function() {
let moved_list = list
println("我拥有了列表: {:?}", moved_list)
}
takes_ownership()
// println(list) // 错误!list 已经被移动
我们也可以使用 move 关键字强制闭包获取它使用的环境值的所有权,即使它在技术上不需要这样做:
let list = [1, 2, 3]
let owns_list = move function() {
println("我拥有了列表: {:?}", list)
}
owns_list()
// println(list) // 错误!list 已经被移动
当我们将闭包传递给新线程时,move 关键字最常用,因为我们想将数据的所有权从一个线程转移到另一个线程。我们将在关于并发的章节中看到这方面的例子。
将闭包作为参数
闭包作为函数参数非常有用。让我们创建一个接受闭包作为参数的函数:
function apply_twice(f: function(integer) -> integer, x: integer) -> integer {
f(f(x))
}
let add_two = function(x) { x + 2 }
let result = apply_twice(add_two, 5)
println(result) // 5 + 2 + 2 = 9
在这里,apply_twice 接受一个函数 f 和一个整数 x,并将 f 应用于 x 两次。
返回闭包
我们也可以从函数返回闭包。但是,闭包类型是匿名的,所以我们需要使用 impl 语法或 trait 对象:
function create_adder(x: integer) -> impl function(integer) -> integer {
function(y) { x + y }
}
let add_five = create_adder(5)
println(add_five(3)) // 8
create_adder 接受一个整数 x 并返回一个将 x 添加到其参数的闭包。返回的闭包捕获 x。
闭包作为迭代器适配器
闭包在迭代器中特别有用,我们将在下一章讨论。这是一个预览:
let numbers = [1, 2, 3, 4, 5]
let doubled: List<integer> = numbers
.iter()
.map(function(n) { n * 2 })
.collect()
println(doubled) // [2, 4, 6, 8, 10]
闭包与函数指针
你也可以使用普通函数代替闭包,当你想要的逻辑不需要捕获环境中的任何内容时:
function add_one(x: integer) -> integer {
x + 1
}
let result = apply_twice(add_one, 5)
println(result) // 7
闭包的性能
你可能想知道闭包是否有性能成本。好消息是,X 语言中的闭包被编译为高效的代码——通常与手写函数一样高效!每个闭包都有自己独特的类型,即使两个闭包具有相同的签名,因此编译器可以专门化并优化每个闭包的使用。
总结
X 语言中的闭包:
- 是可以捕获其环境的匿名函数
- 可以存储在变量中并作为参数传递
- 可以通过不可变借用、可变借用或移动捕获值
- 可以使用
move关键字强制获取所有权 - 可以作为函数参数和返回值
- 对于迭代器和高阶函数特别有用
- 编译为高效代码,没有运行时开销
闭包是 X 语言函数式编程工具包的重要组成部分。在下一章中,我们将讨论迭代器,它们经常与闭包一起使用!
迭代器
迭代器是处理一系列元素的模式。迭代器本身负责管理序列的逻辑,并确定何时处理完每个元素。当你使用迭代器时,你不必自己重新实现这些逻辑。
在 X 语言中,迭代器是惰性的——这意味着在你调用消耗迭代器的方法之前,它们不会有任何效果。让我们来看看迭代器的实际应用!
Iterator trait
迭代器模式的核心是 Iterator trait,它有一个名为 next 的方法,返回 Option<Self::Item>。让我们看看如何实现 Iterator trait。
但首先,让我们看看如何使用标准库提供的迭代器。
使用迭代器处理列表
我们可以通过调用 iter() 方法从列表创建迭代器:
let v = [1, 2, 3]
let iter = v.iter()
迭代器存储了我们需要处理的序列的所有状态。一旦我们有了迭代器,我们就可以用多种方式使用它。
使用 for 循环与迭代器
我们可以使用 for 循环来遍历迭代器中的元素:
let v = [1, 2, 3]
for element in v {
println("得到: {}", element)
}
这将打印:
得到: 1
得到: 2
得到: 3
调用 next 方法
我们也可以直接在迭代器上调用 next 方法:
let v = [1, 2, 3]
let mutable iter = v.iter()
println(iter.next()) // Some(1)
println(iter.next()) // Some(2)
println(iter.next()) // Some(3)
println(iter.next()) // None
注意,我们需要将 iter 设为可变的:在迭代器上调用 next 方法会更改迭代器用来跟踪它在序列中的位置的内部状态。换句话说,这个代码消耗(使用)了迭代器。每次对 next 的调用都从迭代器中消耗一个元素。
消耗迭代器的方法
定义在 Iterator trait 上的各种方法(我们称之为迭代器适配器)有不同的用途。一些方法调用 next,因此它们被称为消耗适配器,因为调用它们会消耗迭代器。
例如,sum 方法,它消耗迭代器并通过反复调用 next 遍历所有元素,并在遍历过程中将每个元素相加:
let v = [1, 2, 3]
let total: integer = v.iter().sum()
println(total) // 6
sum 方法获取迭代器的所有权并通过遍历元素来消耗它。它将每个元素添加到运行总和中,并在迭代完成时返回总和。
生成其他迭代器的方法
定义在 Iterator trait 上的其他方法(我们称之为迭代器适配器)允许你将迭代器更改为不同类型的迭代器。你可以通过链接多个迭代器适配器调用来执行复杂的操作,并且仍然具有可读性。但请记住,因为所有迭代器都是惰性的,你需要调用一个消耗适配器来从适配器调用中获得结果。
map
map 适配器接受一个闭包,并对每个元素调用该闭包,生成一个新的迭代器:
let v = [1, 2, 3]
let mapped: List<integer> = v.iter()
.map(function(x) { x + 1 })
.collect()
println(mapped) // [2, 3, 4]
这里,map 适配器将闭包应用于每个元素,collect 消耗迭代器并将结果收集到列表中。
filter
filter 适配器接受一个闭包,该闭包对每个元素返回 true 或 false,生成一个只包含闭包返回 true 的元素的新迭代器:
let v = [1, 2, 3, 4, 5, 6]
let evens: List<integer> = v.iter()
.filter(function(x) { x % 2 == 0 })
.collect()
println(evens) // [2, 4, 6]
链接多个适配器
迭代器适配器的强大之处在于你可以将它们链接在一起以可读的方式执行复杂操作:
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let result: List<integer> = numbers.iter()
.filter(function(x) { x % 2 == 0 }) // [2, 4, 6, 8, 10]
.map(function(x) { x * x }) // [4, 16, 36, 64, 100]
.filter(function(x) { x > 10 }) // [16, 36, 64, 100]
.take(3) // [16, 36, 64]
.collect()
println(result) // [16, 36, 64]
这是对偶数进行平方,筛选出大于 10 的,取前 3 个,并收集结果。
其他常见的迭代器适配器
这里有一些你可能会发现有用的其他迭代器适配器:
take
take 适配器从迭代器中获取前 n 个元素:
let v = [1, 2, 3, 4, 5]
let first_three: List<integer> = v.iter().take(3).collect()
println(first_three) // [1, 2, 3]
skip
skip 适配器跳过前 n 个元素:
let v = [1, 2, 3, 4, 5]
let after_two: List<integer> = v.iter().skip(2).collect()
println(after_two) // [3, 4, 5]
enumerate
enumerate 适配器返回元素及其索引:
let v = ["a", "b", "c"]
for (i, element) in v.iter().enumerate() {
println("索引 {}: {}", i, element)
}
这将打印:
索引 0: a
索引 1: b
索引 2: c
fold
fold 适配器使用累加器对迭代器中的所有元素进行归约:
let v = [1, 2, 3, 4, 5]
let sum = v.iter().fold(0, function(acc, x) { acc + x })
println(sum) // 15
let product = v.iter().fold(1, function(acc, x) { acc * x })
println(product) // 120
any 和 all
any 检查是否有任何元素满足条件,而 all 检查是否所有元素都满足条件:
let v = [1, 2, 3, 4, 5]
let has_even = v.iter().any(function(x) { x % 2 == 0 })
let all_positive = v.iter().all(function(x) { x > 0 })
println(has_even) // true
println(all_positive) // true
实现 Iterator trait
你可以实现 Iterator trait 来创建自己的迭代器。让我们创建一个从 1 计数到某个数字的迭代器:
type Counter = {
current: integer,
max: integer
}
function Counter::new(max: integer) -> Counter {
{ current: 1, max: max }
}
impl Iterator for Counter {
type Item = integer
function next(self: &mut Self) -> Option<integer> {
if self.current <= self.max {
let result = Some(self.current)
self.current = self.current + 1
result
} else {
None
}
}
}
// 使用我们的迭代器
let counter = Counter::new(5)
for num in counter {
println(num)
}
这将打印:
1
2
3
4
5
迭代器的性能
你可能会想知道迭代器是否有性能成本。答案是否定的!X 语言中的迭代器被编译为高效的代码——通常与手写循环一样快,甚至更快!迭代器是零成本抽象的一个例子,这意味着使用它们不会增加运行时开销。
总结
X 语言中的迭代器:
- 实现
Iteratortrait,其中有next方法 - 是惰性的——在你调用消耗它们的方法之前不会有效果
- 有消耗适配器,如
sum和collect - 有迭代器适配器,如
map和filter,它们将迭代器转换为其他迭代器 - 可以链接在一起以复杂但可读的方式处理数据
- 你可以通过实现
Iteratortrait 来创建自己的迭代器 - 编译为高效代码,没有运行时开销
迭代器是 X 语言函数式编程工具包的另一个重要组成部分。在下一章中,我们将讨论管道运算符,这是一种将值传递给函数的优雅方式!
管道运算符
X 语言的一个独特特性是管道运算符 |>,它允许你以一种从左到右的方式将值传递给函数,这可以使代码更具可读性。管道运算符是 X 语言函数式编程工具包的重要组成部分。
什么是管道运算符?
管道运算符 |> 接受左侧的值并将其作为参数传递给右侧的函数。
让我们从一个简单的例子开始:
function double(x: integer) -> integer {
x * 2
}
function add_one(x: integer) -> integer {
x + 1
}
// 不使用管道的常规函数调用
let result1 = add_one(double(5))
println(result1) // 11
// 使用管道的相同调用
let result2 = 5 |> double |> add_one
println(result2) // 11
两种方式都做同样的事情,但使用管道,我们从值开始,然后按操作发生的顺序应用操作:先 double,然后 add_one。
为什么使用管道?
管道运算符在以下几个方面使代码更具可读性:
- 从左到右的顺序:操作按视觉上发生的顺序排列
- 减少嵌套:避免了深层嵌套的函数调用
- 关注点分离:每个步骤在自己的行上清晰可见
让我们看一个更复杂的例子,展示了好处:
function process_data(data: List<integer>) -> List<integer> {
data
|> List::filter(function(x) { x > 0 })
|> List::map(function(x) { x * 2 })
|> List::take(5)
}
// 不使用管道的相同代码
function process_data_without_pipe(data: List<integer>) -> List<integer> {
List::take(
List::map(
List::filter(data, function(x) { x > 0 }),
function(x) { x * 2 }
),
5
)
}
管道版本更容易阅读!每个操作都按发生的顺序清晰可见。
带多个参数的管道
如果函数接受多个参数怎么办?管道会将值作为第一个参数传递。让我们看看:
function add(a: integer, b: integer) -> integer {
a + b
}
function multiply(a: integer, b: integer) -> integer {
a * b
}
// 5 作为第一个参数传递给 add
let result = 5 |> add(3) |> multiply(2)
println(result) // (5 + 3) * 2 = 16
这里,5 |> add(3) 等价于 add(5, 3)。
带闭包的管道
管道与闭包配合得非常好。让我们看看:
let result = "hello world"
|> String::to_uppercase
|> function(s) { s + "!" }
|> println
// 等价于:
// println(function(s) { s + "!" }(String::to_uppercase("hello world")))
带方法的管道
你也可以在管道中使用方法。请记住,方法接受 self 作为第一个参数:
type Person = {
name: String,
age: integer
}
function Person::new(name: String, age: integer) -> Person {
{ name: name, age: age }
}
function Person::celebrate_birthday(self: &mut Person) {
self.age = self.age + 1
}
function Person::greet(self: &Person) -> String {
String::format("你好,我是 {},我 {} 岁了", self.name, self.age)
}
let mutable person = Person::new(String::from("Alice"), 30)
|> Person::celebrate_birthday
|> Person::greet
|> println
// 输出: 你好,我是 Alice,我 31 岁了
带 Option 和 Result 的管道
管道与 Option 和 Result 类型配合得特别好,使错误处理代码更具可读性:
function parse_number(s: String) -> Option<integer> {
if s == String::from("42") {
Some(42)
} else {
None
}
}
function double_if_some(opt: Option<integer>) -> Option<integer> {
Option::map(opt, function(x) { x * 2 })
}
let result = String::from("42")
|> parse_number
|> double_if_some
|> Option::unwrap_or(0)
println(result) // 84
链式多个管道
管道运算符的真正威力来自于链接多个操作。让我们看一个数据处理的例子:
type Order = {
id: integer,
product: String,
quantity: integer,
price: Float
}
let orders = [
{ id: 1, product: String::from("Apple"), quantity: 5, price: 0.50 },
{ id: 2, product: String::from("Banana"), quantity: 3, price: 0.30 },
{ id: 3, product: String::from("Orange"), quantity: 10, price: 0.60 },
{ id: 4, product: String::from("Apple"), quantity: 2, price: 0.50 }
]
let total_revenue = orders
|> List::filter(function(o) { o.quantity > 2 })
|> List::map(function(o) { o.quantity * o.price })
|> List::sum()
println("总收入: ${}", total_revenue)
这个管道:
- 筛选出数量大于 2 的订单
- 将每个订单映射到其收入(数量 * 价格)
- 对所有收入求和
最佳实践
这里有一些有效使用管道的最佳实践:
- 保持每个步骤简单:如果步骤变得复杂,请使用命名函数而不是内联闭包
- 每行一个步骤:这使管道易于阅读
- 必要时使用注释:对于不明显的复杂管道,添加解释性注释
- 不要过度使用:对于简单的函数调用,普通调用语法可能更好
总结
X 语言中的管道运算符:
- 使用
|>语法将值从左传递到右 - 使数据转换链更具可读性
- 减少嵌套括号
- 按逻辑发生顺序显示操作
- 与函数、闭包、方法和泛型配合良好
- 对于数据处理和函数式编程特别有用
管道运算符是使 X 语言代码更清晰、更具声明性的强大工具!
现在我们已经介绍了 X 语言的函数式编程特性,让我们继续讨论测试!
如何编写测试
测试是确保代码按预期工作的关键部分。X 语言内置了对测试的支持,因此你可以直接在代码中编写测试并运行它们。
在本章中,我们将介绍如何使用 X 语言内置的测试功能编写测试。我们将讨论用于编写测试的语法,以及用于运行测试和查看测试输出的选项。我们还将讨论如何组织测试以及如何编写单元测试和集成测试。
测试函数的剖析
在最简单的形式中,X 语言中的测试是一个用 test 属性注释的函数,用于验证某些代码是否按预期工作。让我们看一个简单的例子:
test it_works {
let result = 2 + 2
assert_eq!(result, 4)
}
这是一个测试函数,用于验证 2 + 2 是否等于 4。让我们分解一下:
test:声明这是一个测试函数it_works:测试的名称assert_eq!:一个断言宏,用于检查两个值是否相等
如果我们运行这个测试,它会通过,因为 2 + 2 确实等于 4。
断言宏
X 语言提供了几个用于测试的断言宏:
assert!
assert! 宏检查条件是否为 true:
test is_true {
assert!(true)
assert!(2 + 2 == 4)
}
assert_eq! 和 assert_ne!
assert_eq! 检查两个值是否相等,assert_ne! 检查它们是否不相等:
test equality {
assert_eq!(2 + 2, 4)
assert_ne!(2 + 2, 5)
}
自定义消息
你可以向任何断言宏添加自定义消息:
test with_message {
let result = 2 + 2
assert_eq!(result, 4, "加法出了问题:{} + {} != {}", 2, 2, result)
}
使用 panic! 测试
你可以测试代码在某些情况下是否 panic。使用 should_panic 属性:
test this_panics should_panic {
panic!("这个测试应该 panic")
}
你还可以指定 panic 消息:
test panic_with_message should_panic("特定消息") {
panic!("特定消息")
}
测试结果
让我们看看一些测试通过和失败的例子:
test it_passes {
assert_eq!(2 + 2, 4)
}
test it_fails {
assert_eq!(2 + 2, 5)
}
当我们运行这些测试时,我们会看到:
running 2 tests
test it_passes ... ok
test it_fails ... FAILED
failures:
---- it_fails stdout ----
assertion failed: `(left == right)`
left: `4`,
right: `5`
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured
组织测试
测试通常放在与被测试代码相同的文件中,放在一个特殊的测试模块中:
// 我们要测试的代码
function add(a: integer, b: integer) -> integer {
a + b
}
function subtract(a: integer, b: integer) -> integer {
a - b
}
// 测试
test add_works {
assert_eq!(add(2, 3), 5)
}
test subtract_works {
assert_eq!(subtract(5, 3), 2)
}
测试模块
对于较大的项目,你可能希望将测试组织成测试模块:
// src/math.x
module math {
export function add(a: integer, b: integer) -> integer {
a + b
}
export function multiply(a: integer, b: integer) -> integer {
a * b
}
}
// tests/math_tests.x
import math::*
test math_add {
assert_eq!(add(2, 3), 5)
}
test math_multiply {
assert_eq!(multiply(2, 3), 6)
}
单元测试与集成测试
在 X 语言中,通常将测试分为两类:
- 单元测试:测试各个函数和模块的隔离
- 集成测试:测试多个模块协同工作
单元测试
单元测试放在与被测试代码相同的文件中,目的是测试代码的各个部分的隔离。它们可以测试私有接口。
集成测试
集成测试在你的库之外,它们以与任何其他代码相同的方式使用你的代码。它们只能测试公共接口,目的是测试库的多个部分是否协同工作。
测试私有函数
X 语言的测试哲学是,你应该主要通过公共接口测试代码,但如果你需要测试私有函数,你也可以这样做。由于测试只是另一个模块,它们可以访问同一模块中的私有项:
function internal_helper(a: integer) -> integer {
a * 2
}
export function public_function(a: integer) -> integer {
internal_helper(a) + 1
}
test test_internal_helper {
assert_eq!(internal_helper(5), 10)
}
test test_public_function {
assert_eq!(public_function(5), 11)
}
运行测试
要运行测试,你使用 x test 命令:
x test my_file.x
你也可以运行特定测试:
x test my_file.x --test add_works
或者过滤测试:
x test my_file.x --filter add
忽略测试
你可以使用 ignore 属性忽略测试:
test expensive_test ignore {
// 运行时间长的测试
for i in 0..1000000 {
// 做一些事情
}
}
如果你想运行被忽略的测试,你可以使用 --ignored 标志:
x test my_file.x --ignored
测试输出
默认情况下,X 语言只显示失败测试的输出。如果你想查看通过测试的输出,你可以使用 --show-output 标志:
x test my_file.x --show-output
总结
在 X 语言中编写测试:
- 使用
test属性注释测试函数 - 使用
assert!、assert_eq!和assert_ne!宏 - 使用
should_panic测试 panic - 可以测试公共和私有函数
- 组织为单元测试(同一文件)或集成测试(单独文件)
- 可以用
ignore忽略 - 用
x test运行
测试是编写可靠软件的重要组成部分,X 语言使其变得简单!
在下一章中,我们将讨论如何组织测试项目!
测试组织
随着项目的发展,测试套件也会增长,你可能需要组织测试,以便更容易导航和运行。在本章中,我们将讨论如何组织测试,以及如何运行测试的某些子集。
测试目录结构
对于较大的项目,推荐的目录结构如下:
my_project/
├── src/
│ ├── main.x
│ ├── lib.x
│ ├── math.x
│ └── utils/
│ ├── string_utils.x
│ └── num_utils.x
└── tests/
├── integration_tests.x
├── math_tests.x
└── utils/
├── string_tests.x
└── num_tests.x
- src/:包含源代码
- tests/:包含集成测试
- 单元测试与它们测试的代码放在同一个文件中
单元测试
单元测试放在与它们测试的代码相同的文件中。它们旨在测试代码的各个部分的隔离。
// src/math.x
function add(a: integer, b: integer) -> integer {
a + b
}
function subtract(a: integer, b: integer) -> integer {
a - b
}
export function multiply(a: integer, b: integer) -> integer {
a * b
}
// 单元测试
test add {
assert_eq!(add(2, 3), 5)
}
test subtract {
assert_eq!(subtract(5, 3), 2)
}
test multiply {
assert_eq!(multiply(2, 3), 6)
}
集成测试
集成测试放在 tests 目录中,它们像任何其他外部代码一样使用你的代码。它们只能测试公共接口。
// tests/math_tests.x
import math::*
test multiply_integration {
assert_eq!(multiply(2, 3), 6)
assert_eq!(multiply(0, 5), 0)
assert_eq!(multiply(-2, 3), -6)
}
// 集成测试还可以测试多个函数协同工作
test multiple_operations {
let result = multiply(add(2, 3), subtract(10, 5))
assert_eq!(result, 25)
}
注意:集成测试只能访问公共(导出)函数。
测试模块
对于相关测试组,你可以使用测试模块来组织它们:
// tests/math_tests.x
module math_tests {
import math::*
test basic_multiplication {
assert_eq!(multiply(2, 3), 6)
}
test zero_multiplication {
assert_eq!(multiply(0, 5), 0)
assert_eq!(multiply(5, 0), 0)
}
test negative_multiplication {
assert_eq!(multiply(-2, 3), -6)
assert_eq!(multiply(2, -3), -6)
assert_eq!(multiply(-2, -3), 6)
}
}
测试辅助函数
你可以创建辅助函数来帮助测试:
// tests/helpers.x
export function create_test_list() -> List<integer> {
[1, 2, 3, 4, 5]
}
export function assert_list_eq<T: Eq>(a: List<T>, b: List<T>) {
assert_eq!(a.len(), b.len())
for (i, element) in a.iter().enumerate() {
assert_eq!(element, b[i])
}
}
// tests/list_tests.x
import helpers::*
import list_utils::*
test reverse_list {
let original = create_test_list()
let reversed = reverse(original)
let expected = [5, 4, 3, 2, 1]
assert_list_eq(reversed, expected)
}
按名称过滤测试
你可以通过将测试名称的一部分传递给 x test 来运行测试的子集:
# 运行所有名称中包含 "multiply" 的测试
x test --filter multiply
# 运行所有名称中包含 "negative" 的测试
x test --filter negative
测试模块
如果你将测试组织成模块,你可以运行特定模块中的所有测试:
# 运行 math_tests 模块中的所有测试
x test --module math_tests
忽略测试
如前一章所述,你可以使用 ignore 属性忽略测试:
test slow_test ignore {
// 运行时间很长的测试
}
要运行被忽略的测试:
x test --ignored
并行运行测试
默认情况下,X 语言并行运行测试以加快速度。如果你需要按顺序运行测试(例如,如果它们共享资源),你可以使用 --test-threads=1 标志:
x test --test-threads=1
显示输出
默认情况下,X 语言只显示失败测试的输出。要查看所有测试的输出:
x test --show-output
测试设置和拆卸
有时你需要在测试前设置一些东西,然后在测试后清理。虽然 X 语言没有内置的 setup 和 teardown 函数,但你可以使用辅助函数:
function setup_database() -> DatabaseConnection {
let conn = Database::connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
conn
}
function teardown_database(conn: DatabaseConnection) {
conn.close()
}
test database_insert {
let conn = setup_database()
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
let result = conn.query("SELECT name FROM users WHERE id = 1")
assert_eq!(result, "Alice")
teardown_database(conn)
}
属性测试
对于更高级的测试,你可以使用属性测试(也称为基于属性的测试),它生成随机输入来测试你的代码:
// 假设的属性测试库
property addition_commutative forall a: integer, b: integer {
assert_eq!(add(a, b), add(b, a))
}
property addition_associative forall a: integer, b: integer, c: integer {
assert_eq!(add(add(a, b), c), add(a, add(b, c)))
}
基准测试
除了测试正确性之外,你可能还想测试性能。基准测试测量代码运行的速度:
benchmark vector_sort {
let mutable v = [5, 3, 1, 4, 2]
v.sort()
}
benchmark matrix_multiplication(size: integer = 100) {
let a = create_matrix(size, size)
let b = create_matrix(size, size)
multiply_matrices(a, b)
}
总结
组织 X 语言测试:
- 单元测试与被测试的代码放在同一个文件中
- 集成测试放在
tests/目录中 - 使用模块组织相关测试
- 创建辅助函数进行设置/拆卸和共享测试代码
- 使用
--filter按名称运行测试的子集 - 使用
--ignore忽略测试,使用--ignored运行被忽略的测试 - 使用
--test-threads控制并行性 - 使用
--show-output查看所有测试的输出 - 对于更高级的测试,考虑属性测试和基准测试
良好组织的测试使项目更容易维护和自信地更改!
现在我们已经介绍了测试,让我们继续讨论标准库!
X语言标准库概述
X语言标准库提供了一组核心功能,用于处理常见的编程任务。本文档将详细介绍标准库的主要模块及其功能。
标准库版本
标准库当前版本为:
let STDLIB_VERSION: String = "0.1.0"
核心模块
Prelude(自动导入)
Prelude模块包含最常用的函数和类型,会自动导入到所有X程序中,无需显式导入。
断言函数
assert(condition: Bool, message: String = "断言失败")- 断言条件为真,否则 panicassert_eq(a, b, message: String = "值不相等")- 断言两个值相等assert_neq(a, b, message: String = "值相等")- 断言两个值不相等
转换函数
to_string(value): String- 将值转换为字符串表示type_of(value): String- 获取值的类型名称
调试辅助
dbg(value)- 打印调试信息(带前缀)并返回值dbg_label(label: String, value)- 带标签的调试打印并返回值
Option 模块
Option类型用于表示可能存在或不存在的值,替代了null的使用。
Some(value)- 表示存在值None()- 表示不存在值is_some(option)- 检查是否为Someis_none(option)- 检查是否为Noneunwrap(option)- 提取值,如果为None则panicunwrap_or(option, default)- 提取值,如果为None则返回默认值
Result 模块
Result类型用于表示可能成功或失败的操作,替代了异常的使用。
Ok(value)- 表示操作成功Err(error)- 表示操作失败is_ok(result)- 检查是否为Okis_err(result)- 检查是否为Err
集合模块
集合模块提供了列表、映射和集合等数据结构的操作。
列表 (List) 操作
创建和基本属性
list_new<T>(): [T]- 创建一个新的空列表list_of<T>(item: T): [T]- 创建一个包含单个元素的列表list_repeat<T>(item: T, count: Int): [T]- 创建一个包含重复元素的列表list_len<T>(list: [T]): Int- 获取列表长度list_is_empty<T>(list: [T]): Bool- 检查列表是否为空
元素访问
list_get<T>(list: [T], index: Int): Option<T>- 获取指定位置的元素list_first<T>(list: [T]): Option<T>- 获取第一个元素list_last<T>(list: [T]): Option<T>- 获取最后一个元素
修改操作
list_push<T>(list: [T], item: T): [T]- 在列表末尾添加元素list_pop<T>(list: [T]): (Option<T>, [T])- 移除并返回列表末尾的元素list_insert<T>(list: [T], index: Int, item: T): [T]- 在指定位置插入元素list_remove<T>(list: [T], index: Int): (Option<T>, [T])- 移除指定位置的元素list_clear<T>(list: [T]): [T]- 清空列表
连接和分割
list_append<T>(list1: [T], list2: [T]): [T]- 连接两个列表list_concat<T>(lists: [[T]]): [T]- 连接多个列表list_split_at<T>(list: [T], index: Int): ([T], [T])- 分割列表为两部分
变换操作
list_map<T, U>(list: [T], f: (T) -> U): [U]- 对列表中的每个元素应用函数list_filter<T>(list: [T], predicate: (T) -> Bool): [T]- 过滤列表,只保留满足谓词的元素list_filter_map<T, U>(list: [T], f: (T) -> Option<U>): [U]- 过滤并映射列表list_fold<T, U>(list: [T], initial: U, f: (U, T) -> U): U- 左折叠(从左到右累积)list_fold_right<T, U>(list: [T], initial: U, f: (T, U) -> U): U- 右折叠(从右到左累积)
搜索操作
list_contains<T>(list: [T], item: T): Bool- 检查列表是否包含指定元素list_find<T>(list: [T], predicate: (T) -> Bool): Option<T>- 查找第一个满足谓词的元素list_position<T>(list: [T], predicate: (T) -> Bool): Option<Int>- 查找第一个满足谓词的元素的索引list_all<T>(list: [T], predicate: (T) -> Bool): Bool- 检查是否所有元素都满足谓词list_any<T>(list: [T], predicate: (T) -> Bool): Bool- 检查是否有元素满足谓词list_count<T>(list: [T], predicate: (T) -> Bool): Int- 统计满足谓词的元素数量
排序操作
list_reverse<T>(list: [T]): [T]- 反转列表list_sort_int(list: [Int]): [Int]- 排序整数列表(升序)list_sort_with<T>(list: [T], compare: (T, T) -> Int): [T]- 使用比较函数排序
数值操作
list_sum(list: [Int]): Int- 计算整数列表的和list_sum_float(list: [Float]): Float- 计算浮点数列表的和list_product(list: [Int]): Int- 计算整数列表的积list_product_float(list: [Float]): Float- 计算浮点数列表的积list_min_int(list: [Int]): Option<Int>- 查找整数列表的最小值list_max_int(list: [Int]): Option<Int>- 查找整数列表的最大值
范围生成
list_range(start: Int, end: Int): [Int]- 创建整数范围 [start, end)list_range_inclusive(start: Int, end: Int): [Int]- 创建整数范围 [start, end]list_range_step(start: Int, end: Int, step: Int): [Int]- 创建带步长的范围
切片操作
list_slice<T>(list: [T], start: Int, end: Int): [T]- 获取列表切片 [start, end)list_take<T>(list: [T], n: Int): [T]- 获取前 n 个元素list_drop<T>(list: [T], n: Int): [T]- 去掉前 n 个元素
映射 (Map) 操作
map_new<K, V>(): {K: V}- 创建一个新的空映射map_is_empty<K, V>(map: {K: V}): Bool- 检查映射是否为空map_len<K, V>(map: {K: V}): Int- 获取映射的大小map_get<K, V>(map: {K: V}, key: K): Option<V>- 获取键对应的值map_insert<K, V>(map: {K: V}, key: K, value: V): {K: V}- 插入键值对map_remove<K, V>(map: {K: V}, key: K): (Option<V>, {K: V})- 移除键值对map_contains_key<K, V>(map: {K: V}, key: K): Bool- 检查映射是否包含键map_keys<K, V>(map: {K: V}): [K]- 获取所有键map_values<K, V>(map: {K: V}): [V]- 获取所有值map_entries<K, V>(map: {K: V}): [(K, V)]- 获取所有键值对map_from_entries<K, V>(entries: [(K, V)]): {K: V}- 从键值对列表创建映射map_merge<K, V>(map1: {K: V}, map2: {K: V}): {K: V}- 合并两个映射
集合 (Set) 操作
set_new<T>(): [T]- 创建一个新的空集合set_of<T>(items: [T]): [T]- 创建一个包含元素的集合set_contains<T>(set: [T], item: T): Bool- 检查集合是否包含元素set_insert<T>(set: [T], item: T): [T]- 向集合添加元素set_remove<T>(set: [T], item: T): [T]- 从集合移除元素set_len<T>(set: [T]): Int- 获取集合大小set_is_empty<T>(set: [T]): Bool- 检查集合是否为空set_union<T>(set1: [T], set2: [T]): [T]- 集合的并集set_intersection<T>(set1: [T], set2: [T]): [T]- 集合的交集set_difference<T>(set1: [T], set2: [T]): [T]- 集合的差集
I/O 模块
I/O模块提供了输入输出和文件操作功能。
标准输入输出
input(): String- 从标准输入读取一行input_prompt(prompt: String): String- 从标准输入读取一行,带提示print(...values)- 打印到标准输出(不换行)println(...values)- 打印到标准输出(换行)format(template: String, ...args): String- 格式化字符串
文件操作
read_file(path: String): Result<String, String>- 读取文件全部内容为字符串write_file(path: String, content: String): Result<Unit, String>- 写入字符串到文件append_file(path: String, content: String): Result<Unit, String>- 追加内容到文件file_exists(path: String): Bool- 检查文件是否存在delete_file(path: String): Result<Unit, String>- 删除文件copy_file(from: String, to: String): Result<Unit, String>- 复制文件move_file(from: String, to: String): Result<Unit, String>- 移动/重命名文件
目录操作
create_dir(path: String): Result<Unit, String>- 创建目录create_dir_all(path: String): Result<Unit, String>- 创建目录(包括父目录)list_dir(path: String): Result<[String], String>- 列出目录内容dir_exists(path: String): Bool- 检查目录是否存在delete_dir(path: String): Result<Unit, String>- 删除空目录delete_dir_all(path: String): Result<Unit, String>- 删除目录及其内容current_dir(): Result<String, String>- 获取当前工作目录set_current_dir(path: String): Result<Unit, String>- 改变当前工作目录
路径操作
path_join(parts: [String]): String- 连接路径path_dirname(path: String): String- 获取路径的目录部分path_basename(path: String): String- 获取路径的文件名部分path_extension(path: String): Option<String>- 获取文件扩展名path_without_extension(path: String): String- 去除文件扩展名path_is_absolute(path: String): Bool- 检查路径是否是绝对路径path_is_relative(path: String): Bool- 检查路径是否是相对路径
文件元数据
file_size(path: String): Option<Int>- 获取文件大小(字节)is_file(path: String): Bool- 检查是否是文件is_dir(path: String): Bool- 检查是否是目录
逐行读取
read_lines(path: String): Result<[String], String>- 读取文件行write_lines(path: String, lines: [String]): Result<Unit, String>- 写入行到文件append_lines(path: String, lines: [String]): Result<Unit, String>- 追加行到文件
临时文件
temp_file(): Result<String, String>- 创建临时文件temp_dir(): Result<String, String>- 创建临时目录
环境变量
env_var(name: String): Option<String>- 获取环境变量set_env_var(name: String, value: String): Result<Unit, String>- 设置环境变量env_vars(): Result<{String: String}, String>- 获取所有环境变量
进程操作
exit(code: Int): Unit- 退出程序args(): [String]- 获取命令行参数program_name(): String- 获取程序名
调试和日志
eprint(...values)- 打印错误信息到标准错误eprintln(...values)- 打印错误信息到标准错误(带换行)dbg_fmt(template: String, ...args)- 格式化并打印调试信息
网络模块
X语言标准库计划提供网络功能,目前正在开发中。以下是计划中的网络模块功能:
HTTP 客户端
http_get(url: String): Result<String, String>- 发送HTTP GET请求http_post(url: String, body: String): Result<String, String>- 发送HTTP POST请求http_request(method: String, url: String, headers: {String: String}, body: String): Result<HttpResponse, String>- 发送自定义HTTP请求
TCP 客户端和服务器
tcp_connect(host: String, port: Int): Result<TcpStream, String>- 连接到TCP服务器tcp_listen(host: String, port: Int): Result<TcpListener, String>- 监听TCP连接tcp_accept(listener: TcpListener): Result<(TcpStream, SocketAddr), String>- 接受TCP连接
套接字操作
socket_read(stream: TcpStream, buffer_size: Int): Result<(String, TcpStream), String>- 从套接字读取数据socket_write(stream: TcpStream, data: String): Result<TcpStream, String>- 向套接字写入数据socket_close(stream: TcpStream): Result<Unit, String>- 关闭套接字
URL 处理
url_parse(url: String): Result<Url, String>- 解析URLurl_build(scheme: String, host: String, port: Int, path: String, query: {String: String}): String- 构建URL
时间模块
时间模块提供了时间获取、格式化、睡眠等操作。
时间类型
Time- 时间点(自 Unix 纪元以来的秒数和纳秒数)Duration- 持续时间DateTime- 日历日期时间
时间常量
NANOS_PER_SECOND: Int = 1_000_000_000- 1秒 = 1_000_000_000 纳秒NANOS_PER_MILLISECOND: Int = 1_000_000- 1毫秒 = 1_000_000 纳秒NANOS_PER_MICROSECOND: Int = 1_000- 1微秒 = 1_000 纳秒SECONDS_PER_MINUTE: Int = 60- 1分钟 = 60秒SECONDS_PER_HOUR: Int = 3600- 1小时 = 3600秒SECONDS_PER_DAY: Int = 86400- 1天 = 86400秒
当前时间
timestamp(): Int- 获取当前时间(自 Unix 纪元以来的秒数)timestamp_millis(): Int- 获取当前时间(自 Unix 纪元以来的毫秒数)timestamp_micros(): Int- 获取当前时间(自 Unix 纪元以来的微秒数)timestamp_nanos(): Int- 获取当前时间(自 Unix 纪元以来的纳秒数)now(): Time- 获取当前时间点
睡眠
sleep(seconds: Float)- 睡眠指定秒数sleep_ms(milliseconds: Int)- 睡眠指定毫秒数sleep_us(microseconds: Int)- 睡眠指定微秒数sleep_ns(nanoseconds: Int)- 睡眠指定纳秒数sleep_duration(duration: Duration)- 睡眠指定持续时间
Duration 构造函数
duration_seconds(seconds: Int): Duration- 创建持续时间(秒)duration_millis(milliseconds: Int): Duration- 创建持续时间(毫秒)duration_micros(microseconds: Int): Duration- 创建持续时间(微秒)duration_nanos(nanoseconds: Int): Duration- 创建持续时间(纳秒)duration_minutes(minutes: Int): Duration- 创建持续时间(分钟)duration_hours(hours: Int): Duration- 创建持续时间(小时)duration_days(days: Int): Duration- 创建持续时间(天)
Duration 操作
duration_as_seconds(d: Duration): Float- 获取持续时间的总秒数duration_as_millis(d: Duration): Int- 获取持续时间的总毫秒数duration_as_micros(d: Duration): Int- 获取持续时间的总微秒数duration_as_nanos(d: Duration): Int- 获取持续时间的总纳秒数duration_add(a: Duration, b: Duration): Duration- 两个持续时间相加duration_sub(a: Duration, b: Duration): Duration- 两个持续时间相减duration_compare(a: Duration, b: Duration): Int- 比较两个持续时间
Time 操作
time_diff(a: Time, b: Time): Duration- 计算两个时间点的差time_add(t: Time, d: Duration): Time- 给时间点加上持续时间time_sub(t: Time, d: Duration): Time- 给时间点减去持续时间time_compare(a: Time, b: Time): Int- 比较两个时间点
日历时间
to_local_datetime(seconds: Int): DateTime- 将时间戳转换为本地日历时间to_utc_datetime(seconds: Int): DateTime- 将时间戳转换为 UTC 日历时间from_datetime(dt: DateTime): Int- 将日历时间转换为时间戳local_now(): DateTime- 获取当前本地时间utc_now(): DateTime- 获取当前 UTC 时间
时间格式化
format_datetime(dt: DateTime, format: String): String- 格式化日期时间为字符串format_iso8601(dt: DateTime): String- 格式化为 ISO 8601 格式datetime_to_string(dt: DateTime): String- 简单的日期时间字符串表示
工作日和月份
weekday_name(weekday: Int): String- 获取工作日名称weekday_abbr(weekday: Int): String- 获取工作日缩写month_name(month: Int): String- 获取月份名称month_abbr(month: Int): String- 获取月份缩写
性能测量
time_it<T>(f: () -> T): (T, Float)- 测量函数执行时间(秒)time_it_print<T>(label: String, f: () -> T): T- 测量函数执行时间并打印
字符串模块
字符串模块提供了丰富的字符串操作功能。
字符串基本属性
str_len(s: String): Int- 获取字符串长度(字符数)str_is_empty(s: String): Bool- 检查字符串是否为空str_byte_len(s: String): Int- 获取字符串的字节长度
字符访问
str_chars(s: String): [Char]- 获取字符串的所有字符str_get(s: String, index: Int): Option<Char>- 获取指定位置的字符str_first(s: String): Option<Char>- 获取第一个字符str_last(s: String): Option<Char>- 获取最后一个字符
字符串比较
str_compare(a: String, b: String): Int- 比较两个字符串(字典序)str_eq(a: String, b: String): Bool- 检查字符串是否相等
字符串拼接
str_concat(a: String, b: String): String- 拼接两个字符串str_join(strings: [String], separator: String): String- 拼接多个字符串str_repeat(s: String, n: Int): String- 重复字符串 n 次
字符串包含检查
str_contains(s: String, substr: String): Bool- 检查字符串是否包含子串str_starts_with(s: String, prefix: String): Bool- 检查字符串是否以指定前缀开头str_ends_with(s: String, suffix: String): Bool- 检查字符串是否以指定后缀结尾
字符串提取
str_substring(s: String, start: Int, end: Int): String- 提取子字符串str_slice(s: String, start: Int): String- 提取从 start 到末尾的子串str_take(s: String, n: Int): String- 获取前 n 个字符str_drop(s: String, n: Int): String- 去掉前 n 个字符
字符串替换
str_replace(s: String, from: String, to: String): String- 替换子字符串str_replace_first(s: String, from: String, to: String): String- 替换第一个匹配的子字符串
字符串大小写转换
str_to_lowercase(s: String): String- 转换为小写str_to_uppercase(s: String): String- 转换为大写str_capitalize(s: String): String- 首字母大写
字符串修剪
str_trim(s: String): String- 去除首尾空白str_trim_start(s: String): String- 去除开头空白str_trim_end(s: String): String- 去除结尾空白str_trim_chars(s: String, chars: String): String- 去除首尾指定字符str_trim_start_chars(s: String, chars: String): String- 去除开头指定字符str_trim_end_chars(s: String, chars: String): String- 去除结尾指定字符
字符串填充
str_pad_left(s: String, width: Int, pad_char: Char): String- 左侧填充到指定长度str_pad_right(s: String, width: Int, pad_char: Char): String- 右侧填充到指定长度str_center(s: String, width: Int, pad_char: Char): String- 居中填充到指定长度
字符串分割
str_split(s: String, separator: String): [String]- 按分隔符分割字符串str_split_whitespace(s: String): [String]- 按空白分割字符串str_lines(s: String): [String]- 按行分割字符串
字符串解析
str_parse_int(s: String): Option<Int>- 解析为整数str_parse_float(s: String): Option<Float>- 解析为浮点数str_parse_bool(s: String): Option<Bool>- 解析为布尔值
字符和字符串转换
char_to_string(c: Char): String- 字符转换为字符串char_code(c: Char): Int- 数字转换为字符char_from_code(code: Int): Option<Char>- 从字符码创建字符
字符分类
char_is_alpha(c: Char): Bool- 检查字符是否是字母char_is_digit(c: Char): Bool- 检查字符是否是数字char_is_alphanumeric(c: Char): Bool- 检查字符是否是字母或数字char_is_whitespace(c: Char): Bool- 检查字符是否是空白char_is_lowercase(c: Char): Bool- 检查字符是否是小写字母char_is_uppercase(c: Char): Bool- 检查字符是否是大写字母
字符串反转
str_reverse(s: String): String- 反转字符串
字符串检查
str_is_alpha(s: String): Bool- 检查字符串是否只包含字母str_is_digit(s: String): Bool- 检查字符串是否只包含数字str_is_alphanumeric(s: String): Bool- 检查字符串是否只包含字母或数字str_is_whitespace(s: String): Bool- 检查字符串是否只包含空白
格式化辅助
format_int(n: Int, width: Int): String- 将整数格式化为指定宽度的字符串format_float(n: Float, decimals: Int): String- 将浮点数格式化为指定小数位数的字符串
数学模块
数学模块提供了常用的数学常量和函数。
数学常量
pi: Float = 3.141592653589793- 圆周率 πe: Float = 2.718281828459045- 自然对数的底 esqrt2: Float = 1.4142135623730951- 2 的平方根sqrt1_2: Float = 0.7071067811865476- 1/2 的平方根log2e: Float = 1.4426950408889634- 以 2 为底 e 的对数log10e: Float = 0.4342944819032518- 以 10 为底 e 的对数ln2: Float = 0.6931471805599453- 以 e 为底 2 的对数ln10: Float = 2.302585092994046- 以 e 为底 10 的对数infinity: Float = 1.0 / 0.0- 正无穷大neg_infinity: Float = -1.0 / 0.0- 负无穷大
基础函数
abs(x: Float): Float- 绝对值abs_int(x: Int): Int- 整数绝对值signum(x: Float): Float- 符号函数:返回 -1, 0, 或 1signum_int(x: Int): Int- 整数符号函数
幂函数和平方根
sqrt(x: Float): Float- 平方根square(x: Float): Float- 平方square_int(x: Int): Int- 整数平方pow(x: Float, y: Float): Float- 幂运算:x^ypow_int(x: Int, y: Int): Int- 整数幂运算(只支持非负指数)cbrt(x: Float): Float- 立方根rsqrt(x: Float): Float- 平方根的倒数
指数和对数函数
exp(x: Float): Float- e 的 x 次幂exp2(x: Float): Float- 2 的 x 次幂ln(x: Float): Float- 自然对数(以 e 为底)log2(x: Float): Float- 以 2 为底的对数log10(x: Float): Float- 以 10 为底的对数log(base: Float, x: Float): Float- 以任意数为底的对数
三角函数
sin(x: Float): Float- 正弦函数(弧度)cos(x: Float): Float- 余弦函数(弧度)tan(x: Float): Float- 正切函数(弧度)asin(x: Float): Float- 反正弦函数(返回弧度)acos(x: Float): Float- 反余弦函数(返回弧度)atan(x: Float): Float- 反正切函数(返回弧度)atan2(y: Float, x: Float): Float- 反正切函数,返回 (x, y) 的角度
双曲函数
sinh(x: Float): Float- 双曲正弦cosh(x: Float): Float- 双曲余弦tanh(x: Float): Float- 双曲正切
取整函数
floor(x: Float): Float- 向下取整ceil(x: Float): Float- 向上取整round(x: Float): Float- 四舍五入trunc(x: Float): Float- 截断小数部分fract(x: Float): Float- 小数部分
极值函数
min(a: Float, b: Float): Float- 两个数中的较小值min_int(a: Int, b: Int): Int- 两个整数中的较小值max(a: Float, b: Float): Float- 两个数中的较大值max_int(a: Int, b: Int): Int- 两个整数中的较大值clamp(x: Float, min_val: Float, max_val: Float): Float- 限制值在 [min, max] 范围内clamp_int(x: Int, min_val: Int, max_val: Int): Int- 限制整数在 [min, max] 范围内
角度转换
radians(degrees: Float): Float- 将角度转换为弧度degrees(radians: Float): Float- 将弧度转换为角度
距离和插值
lerp(a: Float, b: Float, t: Float): Float- 线性插值:在 a 和 b 之间按 t 插值distance(x1: Float, y1: Float, x2: Float, y2: Float): Float- 计算两个点之间的欧几里得距离manhattan_distance(x1: Float, y1: Float, x2: Float, y2: Float): Float- 曼哈顿距离
随机数
srand(seed: Int)- 设置随机种子rand(): Float- 生成 0 到 1 之间的随机浮点数rand_int(min: Int, max: Int): Int- 生成指定范围内的随机整数 [min, max)rand_float(min: Float, max: Float): Float- 生成指定范围内的随机浮点数 [min, max)
除法和余数
div_euclid(a: Int, b: Int): Int- 欧几里得除法(总是向负无穷方向舍入)rem_euclid(a: Int, b: Int): Int- 欧几里得余数(总是非负)
最大公约数和最小公倍数
gcd(a: Int, b: Int): Int- 最大公约数lcm(a: Int, b: Int): Int- 最小公倍数
因数和质数检查
is_even(n: Int): Bool- 检查是否是偶数is_odd(n: Int): Bool- 检查是否是奇数is_prime(n: Int): Bool- 检查是否是质数
阶乘和斐波那契
factorial(n: Int): Int- 阶乘fibonacci(n: Int): Int- 斐波那契数列
系统模块
系统模块提供了与操作系统交互的功能。
exit(code: Int): Unit- 退出程序env_var(name: String): Option<String>- 获取环境变量set_env_var(name: String, value: String): Result<Unit, String>- 设置环境变量args(): [String]- 获取命令行参数program_name(): String- 获取程序名
迭代器模块
迭代器模块提供了迭代器相关的功能。
iter_range(start: Int, end: Int): [Int]- 创建整数范围迭代器iter_map<T, U>(iter: [T], f: (T) -> U): [U]- 映射迭代器iter_filter<T>(iter: [T], predicate: (T) -> Bool): [T]- 过滤迭代器iter_fold<T, U>(iter: [T], initial: U, f: (U, T) -> U): U- 折叠迭代器
标准库初始化
标准库会在程序启动时自动初始化:
/// 初始化标准库
fun init_stdlib() {
// 这里可以进行标准库的初始化工作
// 例如设置随机种子、初始化日志等
srand(timestamp())
}
// 自动初始化
init_stdlib()
导入标准库
要使用完整的标准库,只需导入主模块:
import "stdlib"
或者导入特定模块:
import "stdlib/collections"
import "stdlib/io"
import "stdlib/time"
总结
X语言标准库提供了丰富的功能,涵盖了从基本数据结构到高级I/O操作的各个方面。通过这些模块,开发者可以更高效地编写各种类型的应用程序。
标准库的设计理念是简洁、实用和高效,提供了与现代编程语言相匹配的功能集,同时保持了X语言的特色和优势。
Prelude
如前一章所述,X 语言的 prelude 是自动导入到每个 X 程序中的模块。这意味着你可以直接使用其中定义的类型和函数,而不必显式导入它们。
在本章中,我们将更详细地查看 prelude 中的内容。
Prelude 中的内容
Prelude 包含 X 编程中最常用的类型、函数和 trait。让我们将它们分类来看。
基本类型
这些是 X 语言的基本构建块:
integer- 任意精度整数类型Float- 双精度浮点类型(IEEE 754)boolean- 布尔类型(true或false)character- 单个 Unicode 标量值String- UTF-8 编码的字符串Unit或()- Unit 类型,只有一个值()Never- Never 类型,没有值
这些类型在每个 X 程序中都是可用的,无需导入。
集合类型
这些是最常用的数据结构:
List<T>或[T]- 可增长的数组Map<K, V>- 键值对的哈希映射Set<T>- 唯一值的集合
这些在 prelude 中,因此你可以直接使用它们而无需导入 collections 模块。
Option 和 Result
这些是 X 语言错误处理的核心:
-
Option<T>- 表示可选值Some(T)- 存在一个值None- 没有值
-
Result<T, E>- 表示操作的结果Ok(T)- 操作成功,有一个值Err(E)- 操作失败,有一个错误
这些类型及其变体都在 prelude 中,因此你可以直接使用 Some、None、Ok 和 Err 而无需导入它们。
打印函数
这些是用于输出到标准输出的函数:
print(...)- 打印到标准输出,不带换行符println(...)- 打印到标准输出,带换行符eprint(...)- 打印到标准错误,不带换行符eprintln(...)- 打印到标准错误,带换行符
这些函数接受任意数量的参数,并会智能地格式化它们。
Panic 宏
panic!(...)- 使用给定消息终止程序assert!(...)- 断言条件为 trueassert_eq!(...)- 断言两个值相等assert_ne!(...)- 断言两个值不相等
这些主要用于测试,但有时在生产代码中也用于不变量检查。
常用 Trait
这些是最常用的 trait,因此它们在 prelude 中:
Eq- 相等比较(==和!=)Ord- 排序比较(<、>、<=、>=)Show- 格式化显示(用于print和println)Clone- 显式复制值Copy- 隐式复制值(标记 trait)Drop- 析构函数(当值超出作用域时调用)Iterator- 迭代器Sized- 编译时已知大小的类型(标记 trait)
这些 trait 是自动导入的,因此你可以在自己的类型上实现它们或在泛型函数中使用它们作为约束。
类型转换
as- 类型转换运算符(不是函数,但在语言中可用)- 各种转换方法,如
to_string()、to_integer()等(取决于类型)
其他实用工具
todo!()- 标记未完成的代码(编译但会 panic)unimplemented!()- 类似todo!(),但用于尚未实现的功能unreachable!()- 标记不应可达的代码
为什么 Prelude 存在?
你可能想知道为什么我们有 prelude。为什么不直接让人们导入他们需要的东西呢?有几个很好的理由:
1. 便利性
基本类型和函数如 integer、String、print 和 println 在几乎每个程序中都使用。如果每次都必须导入它们,那会很烦人。
2. 一致性
通过在每个程序中自动导入相同的 prelude,所有 X 代码都使用相同的基本类型和函数集。这使得阅读和理解其他人的代码更容易。
3. 简单性
对于新用户,prelude 意味着你可以开始编写 X 代码而不必学习导入系统。你可以只写 println("Hello") 并使其工作。
4. 稳定性
Prelude 是稳定的——它很少变化。这意味着依赖 prelude 的代码不太可能因 X 语言的新版本而中断。
Prelude 中不包含什么
了解 prelude 中不包含的内容也很重要。这些内容你需要显式导入:
- 不太常见的集合,如
VecDeque、LinkedList等 - 文件系统操作(
fs模块) - 网络(
net模块) - 线程(
thread模块) - 大多数数学函数(
math模块) - 时间和日期(
time模块) - 正则表达式(如果有单独的模块)
- 序列化(如果有单独的模块)
- 随机数生成(
rand模块,如果单独的话)
这些功能不太常用,因此将它们保留在 prelude 之外可以保持 prelude 较小并避免命名冲突。
如何查看 Prelude
如果你想确切地看到 prelude 中有什么,你可以查看标准库源代码中的 prelude 模块。它通常被定义为导出所有包含项的模块。
最佳实践
关于使用 prelude 的一些最佳实践:
1. 不要重新导出 Prelude
除非你正在创建自己的替代 prelude(这很少见),否则你不应该重新导出 prelude 中的项。这样做会令人困惑并且没有必要。
2. 在文档中显式提及
在库文档中,显式提及你正在使用 prelude 中的哪些类型是有帮助的,即使它们是自动导入的。这使得阅读文档的人更容易理解。
3. 知道何时导入其他内容
虽然 prelude 包含很多内容,但它不包含所有内容。知道何时导入其他模块是成为高效 X 程序员的重要部分。
自定义 Prelude
虽然不推荐用于大多数项目,但可以创建自己的自定义 prelude 供项目内部使用。这对于具有很多常用类型和函数的大型项目很有用。
要创建自定义 prelude:
// src/prelude.x
export use crate::types::*
export use crate::utils::*
export use crate::errors::*
// 在其他文件中
import crate::prelude::*
但在大多数情况下,使用标准库 prelude 更好——它是每个人都理解的共同点。
总结
X 语言的 prelude:
- 自动导入到每个程序中
- 包含基本类型(integer、String 等)
- 包含常用集合(List、Map、Set)
- 包含 Option 和 Result 及其变体
- 包含打印函数(print、println 等)
- 包含常用的 trait(Eq、Ord、Show 等)
- 包含 panic 和 assert 宏
- 存在是为了便利、一致、简单和稳定
- 不包含不太常用的功能(fs、net、thread 等)
了解 prelude 中的内容将帮助你编写更简洁的 X 代码!
在下一章中,我们将查看标准库中的一些常用模块!
常用模块
在前面的章节中,我们看到了标准库的概述和 prelude。在本章中,我们将更详细地查看一些最常用的标准库模块。
这些是你在 X 编程中可能经常使用的模块。虽然我们无法涵盖所有内容,但我们将提供足够的信息来帮助你入门。
fmt:格式化和打印
fmt 模块处理格式化和打印值。我们之前已经见过 print 和 println,但这里有更多功能。
格式化占位符
格式化字符串可以包含各种占位符:
// 默认格式
println("Hello, {}", "world") // Hello, world
// 多个占位符
println("{} + {} = {}", 2, 3, 5) // 2 + 3 = 5
// 命名占位符
println("{greeting}, {name}", greeting="Hello", name="Alice")
// 调试格式(用于调试)
println("{:?}", [1, 2, 3]) // [1, 2, 3]
// 数字格式
println("{:x}", 255) // ff (hexadecimal)
println("{:b}", 5) // 101 (binary)
println("{:o}", 9) // 11 (octal)
// 宽度和对齐
println("{:>10}", "right") // right
println("{:<10}", "left") // left
println("{:^10}", "center") // center
// 精度
println("{:.2}", 3.14159) // 3.14
自定义格式化
你可以通过实现 Show trait 为自己的类型自定义格式化:
type Point = { x: integer, y: integer }
impl Show for Point {
function show(self: &Self) -> String {
String::format("({}, {})", self.x, self.y)
}
}
let p = { x: 3, y: 5 }
println(p) // (3, 5)
io:输入/输出
io 模块处理输入和输出操作。
从标准输入读取
// 读取一行
print("输入你的名字: ")
let name = io::stdin().read_line()?
println("你好, {}!", name)
// 读取整个输入
let input = io::stdin().read_to_string()?
写入标准输出/错误
// 我们已经知道 print 和 println
print("这个")
println("那个")
// 写入标准错误
eprintln("这是一个错误消息")
I/O 错误
I/O 操作返回 Result,因为它们可能失败:
when io::stdin().read_line() is {
Ok(line) => println("读取: {}", line),
Err(e) => eprintln!("读取行失败: {}", e)
}
fs:文件系统
fs 模块处理文件系统操作。
读取文件
// 将整个文件读取为字符串
let contents = fs::read_to_string("hello.txt")?
println("文件内容:\n{}", contents)
// 读取为字节
let bytes = fs::read("data.bin")?
写入文件
// 写入字符串
fs::write("output.txt", "Hello, file!")?
// 写入字节
let data = [0, 1, 2, 3]
fs::write("data.bin", data)?
文件元数据
let metadata = fs::metadata("file.txt")?
println("大小: {} 字节", metadata.len())
println("是文件吗? {}", metadata.is_file())
println("是目录吗? {}", metadata.is_dir())
目录操作
// 创建目录
fs::create_dir("new_dir")?
// 创建目录及其所有父目录
fs::create_dir_all("path/to/nested/dir")?
// 删除文件
fs::remove_file("unwanted.txt")?
// 删除空目录
fs::remove_dir("empty_dir")?
// 删除目录及其所有内容(小心使用!)
fs::remove_dir_all("dir_to_delete")?
path:路径处理
path 模块处理文件系统路径。
// 从字符串创建路径
let p = path::Path::new("/home/user/file.txt")
// 连接路径
let full_path = path::Path::new("/home/user") + "file.txt"
// 获取组件
println("父目录: {:?}", p.parent())
println("文件名: {:?}", p.file_name())
println("扩展名: {:?}", p.extension())
// 检查路径
println("存在吗? {}", p.exists())
println("是文件吗? {}", p.is_file())
println("是目录吗? {}", p.is_dir())
time:时间和日期
time 模块处理时间和日期。
当前时间
// 获取当前时间
let now = time::now()
println("当前时间: {:?}", now)
// 自 UNIX 纪元以来的持续时间
let duration = now.duration_since(time::UNIX_EPOCH)?
println("自 1970 年以来的秒数: {}", duration.as_seconds())
持续时间
// 创建持续时间
let five_seconds = time::Duration::from_seconds(5)
let hundred_millis = time::Duration::from_millis(100)
// 持续时间运算
let total = five_seconds + hundred_millis
let difference = five_seconds - hundred_millis
// 睡眠一段时间
thread::sleep(five_seconds)
thread:线程
thread 模块处理多线程。
生成线程
// 生成新线程
let handle = thread::spawn(function() {
for i in 1..10 {
println("线程数字: {}", i)
thread::sleep(time::Duration::from_millis(1))
}
})
// 主线程中做一些事情
for i in 1..5 {
println("主线程数字: {}", i)
thread::sleep(time::Duration::from_millis(1))
}
// 等待线程完成
handle.join()?
线程本地存储
// 每个线程都有自己的计数器副本
thread_local! {
static COUNTER: integer = 0
}
thread::spawn(function() {
COUNTER.with(|c| {
println("线程中的计数器: {}", c)
})
})
我们将在关于并发的章节中更详细地讨论线程。
sync:同步
sync 模块提供同步原语。
互斥锁(Mutex)
let counter = sync::Mutex::new(0)
// 在多线程中使用...
let guard = counter.lock()?
*guard = *guard + 1
// guard 被丢弃时自动解锁
原子引用计数(Arc)
// Arc 是原子的、线程安全的引用计数指针
let data = sync::Arc::new([1, 2, 3])
// 跨线程共享...
let data_clone = data.clone() // 增加引用计数
thread::spawn(function() {
println("线程中的数据: {:?}", data_clone)
})
通道(Channel)
// 创建通道
let (sender, receiver) = sync::mpsc::channel()
// 在线程中发送
thread::spawn(function() {
sender.send("你好来自线程!")?
})
// 在主线程中接收
let message = receiver.recv()?
println("收到: {}", message)
同样,我们将在关于并发的章节中更详细地讨论这些内容。
math:数学函数
math 模块提供数学函数和常量。
基本函数
println("绝对值: {}", math::abs(-5)) // 5
println("符号: {}", math::signum(-3)) // -1
幂运算和根
println("平方: {}", math::pow(2, 3)) // 8
println("平方根: {}", math::sqrt(16.0)) // 4.0
println("立方根: {}", math::cbrt(8.0)) // 2.0
三角函数
let angle = math::PI / 4.0 // 45度
println("sin: {}", math::sin(angle))
println("cos: {}", math::cos(angle))
println("tan: {}", math::tan(angle))
指数和对数
println("e^2: {}", math::exp(2.0))
println("ln(10): {}", math::ln(10.0))
println("log2(8): {}", math::log2(8.0))
println("log10(100): {}", math::log10(100.0))
舍入
println("floor: {}", math::floor(3.7)) // 3.0
println("ceil: {}", math::ceil(3.2)) // 4.0
println("round: {}", math::round(3.5)) // 4.0
println("trunc: {}", math::trunc(3.9)) // 3.0
最小值和最大值
println("min: {}", math::min(5, 3)) // 3
println("max: {}", math::max(5, 3)) // 5
常量
println("π: {}", math::PI)
println("e: {}", math::E)
convert:类型转换
convert 模块帮助在类型之间进行转换。
// 字符串转数字
let num: integer = "42".parse()?
let float: Float = "3.14".parse()?
// 数字转字符串
let s1 = 42.to_string()
let s2 = 3.14.to_string()
// 整数转换
let i: integer = 5
let f: Float = i as Float
// 浮点数转换(小心!)
let f: Float = 3.9
let i: integer = f as integer // 3(截断)
rand:随机数
(如果 rand 是单独的模块或在 std 中)
// 生成随机整数
let n = rand::random<integer>()
let n_in_range = rand::range(0, 10) // 0 <= n < 10
// 生成随机浮点数
let f = rand::random<Float>() // 0.0 <= f < 1.0
// 从列表中选择
let items = ["a", "b", "c"]
let chosen = rand::choice(items)
总结
常用标准库模块:
- fmt - 格式化和打印,有很多占位符选项
- io - 标准输入/输出
- fs - 文件系统操作
- path - 路径处理
- time - 时间和日期,持续时间
- thread - 多线程
- sync - 同步原语(Mutex、Arc、通道)
- math - 数学函数和常量
- convert - 类型转换
- rand - 随机数生成(如果可用)
这些模块涵盖了你在日常 X 编程中需要的大部分功能。与往常一样,官方文档是完整 API 的最佳资源!
现在我们已经涵盖了标准库,让我们继续讨论高级特性!
I/O 项目:构建命令行程序
这是一个有趣的章节!我们将同时使用你已经学过的很多知识,并展示一些常见的标准库功能。我们将通过构建一个与文件和命令行输入/输出交互的命令行工具来实践,以继续练习你现在熟悉的一些 X 语言概念。
我们将要构建的命令行工具是经典的命令行工具 grep 的一个版本,简称“全局正则表达式打印“。grep 最简单的用例是在文件中搜索包含指定字符串的行。为此,grep 接受一个文件名和一个字符串作为参数。然后它读取该文件,查找包含该字符串的行,并打印这些行。
在本章中,我们将逐步构建这个项目。我们将使用的关键概念包括:
- 组织代码(使用你在第 5 章中学到的模块)
- 使用向量和字符串(集合,第 6 章)
- 处理错误(第 7 章)
- 适当使用 trait 和生命周期(第 8 章)
- 编写测试(第 11 章)
事不宜迟,让我们开始吧!
接受命令行参数
首先,我们将让我们的 minigrep 工具接受两个命令行参数:一个查询字符串和一个文件名。也就是说,我们希望能够使用 x run 运行我们的程序,并传入查询字符串和要搜索的文件,如下所示:
x run -- searchstring example-filename.txt
但现在,我们使用 x new 生成的程序不能接受任何参数。让我们确保我们的程序可以通过查看如何让 X 语言程序接受命令行参数来接受它们。
读取参数值
为了确保我们的 minigrep 能够接受传递给它的命令行参数值,我们需要使用 X 语言标准库中提供的 std::env::args 函数。这个函数返回传递给程序的命令行参数的迭代器。我们将在第 13 章全面讨论迭代器。对于我们的目的,我们不需要了解迭代器的所有细节,只需了解两个细节:
- 迭代器产生一系列值
- 我们可以在迭代器上调用
collect方法将其变成一个向量
让我们创建一个程序,该程序只会打印出所有传递给它的命令行参数,如示例所示。首先,我们会将参数收集到一个向量中,然后打印该向量。
import std::env
function main() {
let args: List<String> = env::args().collect()
println!("{:?}", args)
}
注意,env::args 将产生的第一个值是二进制文件的名称。这与其他语言的行为匹配。让我们尝试使用 x run 首先运行这段代码,不传入任何参数:
x run
输出应该看起来像这样:
["target/debug/minigrep"]
第一个参数 "target/debug/minigrep" 是我们二进制文件的名称。这符合 C 族语言中参数列表的行为,让程序在执行时使用调用它的名称。如果需要,能够访问程序名称会很方便,这样我们就可以在消息中打印它,或者更改程序应有的行为,具体取决于命令行中调用程序时使用的别名。但就本章而言,我们将忽略第一个参数,只获取我们需要的两个参数。
现在让我们尝试使用两个参数运行程序:
x run -- needle haystack
注意我们在 x run 的参数和我们的程序参数之间添加了 --:这告诉 x 后面的参数是给我们的程序的,而不是给 x 的。输出应该看起来像这样:
["target/debug/minigrep", "needle", "haystack"]
太好了!程序正在接收它作为参数的查询和文件名。现在我们将第二个参数保存在变量中,第三个参数保存在另一个变量中。
将参数值保存到变量中
现在我们的程序能够访问作为命令行参数指定的值,让我们将这两个参数的值保存到变量中,以便我们可以在程序的其余部分使用这些值。我们将把查询参数称为 query,将文件名参数称为 filename,如示例所示。
import std::env
function main() {
let args: List<String> = env::args().collect()
let query = args[1].clone()
let filename = args[2].clone()
println!("搜索 {}", query)
println("在文件 {} 中", filename)
}
我们再次使用 clone 在这里简化示例。我们可以直接使用引用,但这需要生命周期注解,我们还没有准备好深入研究。现在使用 clone 是一个可以接受的折衷方案,让我们可以继续编写这个程序的第一部分。
让我们尝试运行这段代码:
x run -- test sample.txt
输出应该看起来像这样:
搜索 test
在文件 sample.txt 中
太好了!程序正在工作;查询和文件名正在被保存到变量中。稍后我们会添加一些错误处理,以防有人在没有提供任何参数的情况下运行我们的程序,但现在我们将忽略这种情况,而是处理添加读取文件功能。
读取文件
现在我们要添加读取作为 filename 命令行参数指定的文件的功能。首先,我们需要一个示例文件来测试,这是使用 grep 时的最佳用例。我们将使用一个包含多行文本的文件,其中一些行包含“duct“。这就是示例。创建一个名为 poem.txt 的文件,其中包含 Emily Dickinson 的诗《我没告诉你的那个早晨》:
我没告诉你的那个早晨,
那最初的早晨,太阳升起来了——
从那之后,我数不清多少个日子,
但它们都不如那第一个日子重要。
夏天来得太早了,
我忘记了夏天也会离开。
我记得的唯一一件事,
是那第一个早晨,以及它如何照亮我。
很好,现在我们有了一个文件,让我们编辑 src/main.x 以添加读取文件的代码,如示例所示:
import std::env
import std::fs
function main() {
// --snip--
println!("搜索 {}", query)
println!("在文件 {} 中", filename)
let contents = fs::read_to_string(filename)
.expect("读取文件出错了")
println("带有文本的文件:\n{}", contents)
}
首先,我们添加了另一个 import 语句以从标准库引入 std::fs,它处理文件相关的事情。
在 main 中,我们添加了新行:fs::read_to_string 接受 filename,打开该文件,然后返回一个 Result<String>,其中包含该文件的内容。
在那之后,我们再次添加了一个临时的 println! 语句,该语句在读取文件后打印 contents 的值,以便我们可以检查程序是否正常工作。
让我们尝试运行这段代码,首先将任何字符串作为第一个命令行参数,将 poem.txt 作为第二个参数:
x run -- the poem.txt
我们应该看到打印出的诗的内容!很好。我们的程序正在工作。
现在让我们重构并改进它。
第13章 效果系统
效果系统是 X 语言的核心特性之一,它使函数的副作用在类型签名中显式可见,确保所有副作用都被正确追踪和处理。通过效果系统,X 语言实现了类型安全的副作用管理,同时保持了代码的清晰性和可维护性。
13.1 效果声明
效果声明是效果系统的基础,它定义了函数可能产生的副作用类型。在 X 语言中,效果通过函数签名中的 with 关键字进行声明。
内置效果
X 语言提供了以下内置效果:
| 效果 | 含义 | 语义 |
|---|---|---|
IO | 输入输出操作 | 文件系统、网络、控制台交互 |
Async | 异步执行 | 函数可能挂起并稍后恢复 |
State<S> | 可变状态 | 读写类型为 S 的状态 |
Throws<E> | 可能失败 | 返回 Result<T, E>,用 ? 传播错误 |
NonDet | 非确定性 | 可能产生多个结果 |
函数效果注解
函数签名中使用 with 关键字分隔返回类型和效果列表。无效果的函数是纯函数,不需要效果注解。
// 纯函数——无副作用
function add(a: Integer, b: Integer) -> Integer = a + b
// 单效果
function readLine() -> String with IO {
Console.readLine()
}
// 多效果
function fetchUser(id: Integer) -> User with Async, IO, Throws<NetworkError> {
let response = await http.get("/users/{id}")?
parseUser(response.body)?
}
// 效果推断——编译器可自动推断效果集,签名中可省略
function helper(x: Integer) { // 编译器推断效果
print(x) // 推断出 IO
}
用户自定义效果
除了内置效果外,X 语言还允许用户定义自己的效果,以捕获特定领域的副作用。
effect Logger {
function log(level: String, message: String) -> ()
function getLevel() -> String
}
effect Random {
function nextInteger(bound: Integer) -> Integer
function nextFloat() -> Float
}
效果类型规则
效果系统遵循以下类型规则:
- 函数调用传播效果:调用具有效果的函数会将这些效果传播到调用点。
- 效果集合的并:多个效果的组合会形成一个效果集合。
- 效果子类型:效果集越小的函数越纯,可以替代效果集更大的函数。
13.2 效果处理
效果处理是 X 语言中处理副作用的核心机制,它允许开发者拦截、转换和处理效果。通过效果处理器,开发者可以为效果提供具体实现,从而消除函数的效果依赖。
效果处理器语法
效果处理器使用 handle 表达式来定义:
handle {
// 可能产生效果的代码块
} with {
// 效果处理规则
}
效果处理示例
基本效果处理
effect Ask<T> {
function ask() -> T
}
// 使用效果
function greet() -> String with Ask<String> {
let name = Ask.ask()
"Hello, {name}!"
}
// 处理效果——提供具体实现
let result = handle {
greet()
} with {
Ask.ask() => "World"
}
// result = "Hello, World!"
用处理器实现纯测试
效果处理器的一个重要应用是实现纯测试,通过模拟依赖来避免实际的副作用:
function testGetUser() {
let result = handle {
getUser(42)
} with {
Database.query(sql) => [Row { id = 42, name = "Alice" }]
Logger.info(msg) => ()
}
assert(result == Ok(User { id = 42, name = "Alice" }))
}
效果多态
函数可以对效果进行参数化,实现效果多态:
function map<A, B, E>(list: List<A>, f: (A) -> B with E) -> List<B> with E {
match list {
[] => []
[head, ...tail] => [f(head), ...map(tail, f)]
}
}
效果处理的类型规则
效果处理遵循以下类型规则:
- 效果消除:处理效果后,被处理的效果会从函数的效果集合中移除。
- 类型兼容性:效果处理器必须为效果中的所有操作提供实现。
13.3 依赖注入
X 语言的依赖注入机制是通过效果系统实现的,使用 needs 和 given 关键字来声明和提供依赖。
需求声明(needs)
在函数签名中使用 needs 关键字声明函数所需的依赖:
trait Database {
function query(sql: String) -> List<Row> with Throws<DbError>
function execute(sql: String) -> Integer with Throws<DbError>
}
trait Logger {
function info(message: String) -> () with IO
function error(message: String) -> () with IO
}
function getUser(id: Integer) -> User with Throws<NotFound>
needs Database, Logger {
Logger.info("Fetching user {id}")
let rows = Database.query("SELECT * FROM users WHERE id = {id}")?
match rows.first() {
Some(row) => User.fromRow(row)
None => Err(NotFound { entity = "User", id })
}
}
给定依赖(given)
在调用函数时,使用 given 块提供依赖的具体实现:
function main() with IO {
let result = getUser(42) given {
Database = PostgresDatabase.connect("localhost:5432")
Logger = ConsoleLogger.new()
}
match result {
Ok(user) => print("Found: {user.name}")
Err(NotFound { id, .. }) => print("User {id} not found")
}
}
依赖作用域
given 块内提供的依赖对所有嵌套调用可见。例如,deleteUser 内部调用 getUser 时,会自动继承外层的 Database 和 Logger 实例:
function deleteUser(id: Integer) -> () with Throws<NotFound>
needs Database, Logger {
let user = getUser(id)?
Database.execute("DELETE FROM users WHERE id = {id}")?
Logger.info("Deleted user {user.name}")
}
依赖注入的类型规则
依赖注入遵循以下类型规则:
- 依赖传播:函数声明的依赖会成为其效果集合的一部分。
- 依赖消除:提供依赖后,依赖会从函数的效果集合中移除。
- 类型匹配:提供的依赖必须与声明的依赖类型匹配。
13.4 最佳实践
效果使用建议
- 保持函数纯性:尽量编写纯函数,只在必要时使用效果。
- 效果最小化:函数应只声明其实际需要的效果,避免过度声明。
- 效果组合:使用效果多态来编写可以处理多种效果的通用函数。
依赖注入建议
- 依赖抽象:通过接口(trait)声明依赖,而不是具体实现。
- 依赖隔离:每个函数只声明其直接需要的依赖。
- 测试友好:使用效果处理器来模拟依赖,实现纯测试。
错误处理建议
- 使用 Throws 效果:对于可能失败的操作,使用
Throws<E>效果而不是异常。 - 明确错误类型:为不同类型的错误定义明确的错误类型。
- 错误传播:使用
?运算符来简洁地传播错误。
13.5 总结
X 语言的效果系统是一个强大的工具,它通过以下方式提升代码质量:
- 副作用显式化:所有副作用在类型签名中明确可见,提高代码的可读性和可维护性。
- 类型安全:效果系统在编译时确保所有副作用都被正确处理。
- 依赖管理:通过
needs和given实现类型安全的依赖注入。 - 测试友好:通过效果处理器可以轻松模拟依赖,实现纯测试。
- 无异常设计:使用
Result<T, E>和?运算符替代传统的异常机制,使错误处理更加显式和可控。
效果系统是 X 语言的核心特性之一,它为开发者提供了一种优雅的方式来管理副作用,同时保持代码的清晰性和类型安全性。通过合理使用效果系统,开发者可以编写更加可靠、可测试和可维护的代码。
异步编程
异步编程是一种并发编程风格,其中程序可以在等待操作(如 I/O)完成时执行其他工作。这与同步编程不同,在同步编程中,程序会等待每个操作完成后再继续下一个。
X 语言内置了对异步编程的支持,使用 async 和 wait 关键字。在本章中,我们将探讨如何使用这些功能编写高效的异步代码。
什么是异步编程?
在同步编程中,当你调用一个执行 I/O 的函数(如读取文件或进行网络请求)时,程序会等待(阻塞)直到该操作完成。在等待期间,程序除了等待之外什么都不做。
在异步编程中,当你调用一个异步函数时,它会立即返回一个“未来“或“承诺“对象,而不是等待操作完成。然后,你可以继续做其他工作,当操作最终完成时,你可以“等待“未来以获取结果。
这对于具有大量 I/O 的程序特别有用,如 Web 服务器、数据库客户端或任何需要同时处理多个连接的东西。
Async 和 Wait 关键字
X 语言使用两个关键字进行异步编程:
async- 声明一个函数是异步的wait- 等待异步操作完成
让我们看一个简单的例子:
async function fetch_data(url: String) needs Async -> Result<String, String> {
// 模拟异步网络请求
wait thread::sleep(time::Duration::from_seconds(2))
Ok(String::from("来自 ") + url + " 的数据")
}
async function main() needs Async, IO {
println("开始获取数据...")
let result = wait fetch_data(String::from("https://example.com"))
when result is {
Ok(data) => println("得到: {}", data),
Err(e) => eprintln("错误: {}", e)
}
}
让我们分解一下:
async function- 声明fetch_data是一个异步函数。异步函数总是返回一个 future。wait- 在fetch_data调用上暂停执行,直到 future 完成。needs Async- 声明这些函数具有异步效果。
运行时多个异步操作
异步编程的真正威力来自于同时运行多个异步操作。让我们看看如何做到这一点:
async function fetch_url(url: String) needs Async -> String {
wait thread::sleep(time::Duration::from_seconds(1))
String::from("来自 ") + url + " 的数据"
}
async function main() needs Async, IO {
println("开始获取...")
// 开始两个 fetch 操作——它们同时运行
let future1 = fetch_url(String::from("https://example.com"))
let future2 = fetch_url(String::from("https://rust-lang.org"))
// 等待两者都完成
let result1 = wait future1
let result2 = wait future2
println("结果 1: {}", result1)
println("结果 2: {}", result2)
}
在这个例子中,两个 fetch 操作同时运行,所以整个程序大约需要 1 秒,而不是 2 秒(如果我们按顺序等待它们的话)。
Join 和 Select
X 语言提供了组合多个 future 的实用程序:
Join
join 等待所有 future 完成并返回它们的结果:
async function main() needs Async, IO {
let (result1, result2) = wait join(
fetch_url(String::from("https://example.com")),
fetch_url(String::from("https://rust-lang.org"))
)
println("两者都完成了!")
}
Select
select 等待任何一个 future 完成并返回第一个结果:
async function main() needs Async, IO {
let result = wait select(
fetch_url(String::from("https://example.com")),
fetch_url(String::from("https://rust-lang.org"))
)
println("第一个完成的: {}", result)
}
这对于实现超时很有用:
async function fetch_with_timeout(url: String, timeout: Duration) needs Async -> Result<String, String> {
wait select(
fetch_url(url).then(function(r) { Ok(r) }),
(async function() {
wait thread::sleep(timeout)
Err(String::from("超时"))
})()
)
}
异步 I/O
异步编程对于 I/O 绑定操作最有用。X 语言的标准库提供了许多异步版本的常用 I/O 操作:
async function read_file_async(path: String) needs Async, FileIO -> Result<String, String> {
// 异步读取文件——不会阻塞
fs::read_to_string_async(path)
}
async function write_file_async(path: String, contents: String) needs Async, FileIO -> Result<(), String> {
// 异步写入文件——不会阻塞
fs::write_async(path, contents)
}
异步 Trait
你也可以定义具有异步方法的 trait:
trait Database {
async function connect(url: String) needs Async -> Result<Self, String>
async function query(self: &Self, sql: String) needs Async -> Result<List<Row>, String>
}
错误处理
异步函数中的错误处理与同步函数中的工作方式相同——你使用 Result:
async function fetch_with_retry(url: String, max_retries: integer) needs Async -> Result<String, String> {
let mut attempt = 0
while attempt < max_retries {
when wait fetch_url(url) is {
Ok(result) => return Ok(result),
Err(e) => {
attempt = attempt + 1
if attempt == max_retries {
return Err(e)
}
wait thread::sleep(time::Duration::from_seconds(1))
}
}
}
Err(String::from("意外错误"))
}
实际例子:Web 服务器
让我们看一个更实际的例子——一个简单的异步 Web 服务器:
type Request = {
method: String,
path: String,
body: String
}
type Response = {
status: integer,
body: String
}
async function handle_request(req: Request) needs Async -> Response {
// 模拟一些异步工作,如数据库查询
wait thread::sleep(time::Duration::from_millis(10))
when req.path is {
"/" => {
{ status: 200, body: String::from("你好,世界!") }
},
"/about" => {
{ status: 200, body: String::from("关于我们") }
},
_ => {
{ status: 404, body: String::from("未找到") }
}
}
}
async function main() needs Async, IO, Net {
println("服务器在 :8080 上启动...")
let listener = net::TcpListener::bind("127.0.0.1:8080")?
// 异步接受连接
while let Some(stream) = wait listener.accept() {
// 生成新任务来处理每个连接
spawn async {
let request = wait read_request(stream)?
let response = wait handle_request(request)
wait send_response(stream, response)?
}
}
}
这个服务器可以同时处理多个连接,因为每个连接都在自己的异步任务中处理。
最佳实践
关于异步编程的一些最佳实践:
-
不要在异步代码中阻塞:避免在异步函数中进行阻塞操作——它们会阻塞整个执行器。如果你需要做阻塞的事情,将其生成到单独的线程中。
-
使用适当的抽象:在可用时使用
join、select和其他 future 组合器。 -
限制并发性:同时运行太多任务会导致问题——使用信号量或其他机制来限制并发性。
-
处理取消:异步任务可以被取消——确保你的代码通过清理资源来正确处理这个问题。
-
测试异步代码:测试异步代码可能很棘手——使用专门为异步测试设计的测试工具。
总结
X 语言中的异步编程:
- 使用
async声明异步函数 - 使用
wait等待异步操作 - 允许同时运行多个操作
- 对 I/O 绑定工作最有用
- 使用
join等待所有操作 - 使用
select等待第一个操作 - 与
Result集成用于错误处理
异步编程是构建高效、可扩展应用程序的强大工具!
在下一章中,我们将探讨元编程!
元编程
元编程是编写编写或操作其他程序的程序的做法。在 X 语言中,这主要通过宏、编译时代码生成和反射来实现。
在本章中,我们将探讨 X 语言的元编程功能——它们是什么、如何工作以及何时使用它们。
什么是元编程?
元编程意味着“在程序之上编程“。它是编写将其他程序作为数据处理的代码的技术——在编译时或运行时生成或修改代码。
元编程的常见用途包括:
- 减少样板代码
- 特定于领域的语言(DSL)
- 代码生成
- 序列化/反序列化
- 调试工具
- 性能优化
宏
宏是 X 语言中最常见的元编程形式。宏允许你在编译时扩展为其他代码的语法。
你之前已经见过一些宏:
// print 宏
println("Hello, {}!", "world")
// 断言宏
assert!(condition)
assert_eq!(a, b)
// Panic 宏
panic!("出了问题")
// Todo 宏
todo!()
声明宏
X 语言使用类似函数的语法声明宏:
macro vec {
() => {
List::new()
},
($($x:expr),*) => {
{
let mut temp_list = List::new()
$(
temp_list = temp_list + [$x]
)*
temp_list
}
}
}
// 使用我们的宏
let v = vec![1, 2, 3]
这个 vec! 宏接受一个逗号分隔的表达式列表并创建一个包含这些元素的列表。
宏语法
让我们分解 vec! 宏中发生的事情:
macro vec- 声明一个名为vec的宏() => { ... }- 第一个匹配规则——匹配空参数列表($($x:expr),*) => { ... }- 第二个匹配规则——匹配逗号分隔的表达式$x:expr- 匹配一个表达式并将其绑定到$x$(...)*- 重复匹配内部的内容零次或多次,- 匹配文字逗号
在宏体中,我们使用 $(...)* 再次重复,为我们捕获的每个 $x 生成代码。
属性宏
属性宏适用于项(函数、结构体、枚举等)并可以修改它们:
attribute derive(Debug) {
// 自动为类型生成 Debug 实现
}
// 使用属性宏
[derive(Debug)]
type Point = {
x: integer,
y: integer
}
// 现在我们可以打印 Point 进行调试
let p = { x: 1, y: 2 }
println("{:?}", p) // Point { x: 1, y: 2 }
derive 属性是一个属性宏,它自动为类型生成 trait 实现。
自定义 Derive
你可以创建自己的自定义 derive 宏:
attribute derive(Clone) for type T {
function clone(self: &T) -> T {
// 生成克隆每个字段的代码
{
$(field: self.field.clone(),)*
}
}
}
这个自定义 derive(Clone) 会自动为具有可克隆字段的任何类型生成 clone 方法。
编译时代码生成
除了宏之外,X 语言还支持更强大的编译时代码生成形式。
Build Scripts
你可以编写在主构建之前运行的构建脚本,并可以生成额外的源代码:
// build.x - 在编译主代码之前运行
function main() {
// 生成一些代码
let code = String::from("
function generated_function() -> integer {
42
}
")
// 把它写到一个文件中
fs::write("src/generated.x", code)?
}
然后你的主代码可以使用生成的代码:
import "generated.x"
function main() {
println("{}", generated_function()) // 42
}
过程宏
过程宏是更强大的宏形式,它们在编译时运行并可以任意操作 AST:
// 这是一个简单的过程宏,它将函数名改为大写
proc_macro uppercase_function(item: Item) -> Item {
when item is {
Item::Function(mut f) => {
f.name = f.name.to_uppercase()
Item::Function(f)
},
_ => item
}
}
// 使用它
[uppercase_function]
function hello() {
println("你好")
}
// 现在这个函数叫做 HELLO()
function main() {
HELLO()
}
过程宏比声明宏更强大,但也更复杂。
反射
反射是程序在运行时检查和操作自身结构的能力。
类型信息
X 语言提供了在运行时检查类型的方法:
function print_type_info<T>(_: T) {
let type_name = type_name_of<T>()
println("类型名称: {}", type_name)
let type_id = type_id_of<T>()
println("类型 ID: {:?}", type_id)
}
print_type_info(42)
// 输出:
// 类型名称: integer
// 类型 ID: ...
字段反射
你可以在运行时检查结构体或记录的字段:
type Person = {
name: String,
age: integer
}
function print_fields<T>(value: &T) {
for field in fields_of(value) {
println("字段: {} = {:?}", field.name, field.value)
}
}
let p = { name: String::from("Alice"), age: 30 }
print_fields(&p)
// 输出:
// 字段: name = "Alice"
// 字段: age = 30
动态调用
你可以使用反射在运行时动态调用方法:
type Greeter = {
name: String
}
function Greeter::greet(self: &Greeter) {
println("你好,我是 {}", self.name)
}
let g = { name: String::from("Bob") }
// 动态调用 greet 方法
call_method(&g, "greet", [])
// 输出: 你好,我是 Bob
实际例子:序列化
让我们看一个使用元编程进行序列化的实际例子——将数据结构转换为可以存储或传输的格式。
// 首先,定义我们的 Serialize trait
trait Serialize {
function serialize(self: &Self) -> String
}
// 现在,一个用于自动生成 Serialize 实现的 derive 宏
attribute derive(Serialize) for type T {
function serialize(self: &T) -> String {
let mut result = String::from("{")
let mut first = true
for field in fields_of(self) {
if !first {
result = result + ", "
}
first = false
result = result + "\"" + field.name + "\": "
result = result + serialize_field(field.value)
}
result = result + "}"
result
}
}
// 使用我们的宏
[derive(Serialize)]
type User = {
id: integer,
name: String,
email: String
}
// 现在我们可以序列化 User
let user = {
id: 1,
name: String::from("Alice"),
email: String::from("alice@example.com")
}
let json = user.serialize()
println(json)
// 输出: {"id": 1, "name": "Alice", "email": "alice@example.com"}
这个例子展示了元编程如何通过自动生成样板代码来节省大量的手动工作。
最佳实践
关于元编程的一些最佳实践:
-
仅在必要时使用:元编程可能很强大,但也会使代码更难理解。首先考虑普通的抽象(函数、trait 等)。
-
保持宏简单:复杂的宏难以编写、测试和维护。保持小而专注。
-
提供良好的错误消息:宏错误可能令人困惑——尽可能提供清晰、有用的错误消息。
-
记录宏:宏可能不透明——彻底记录它们的作用、它们接受什么以及它们扩展为什么。
-
测试宏:彻底测试宏——编写测试用例,覆盖成功和失败案例。
-
考虑编译时间:元编程会增加编译时间——要注意这一点,特别是对于大型项目。
-
使用 hygienic macros(卫生宏):确保宏不会意外捕获或与周围代码中的变量名冲突。
总结
X 语言中的元编程:
- 宏 - 声明宏、属性宏、过程宏
- 编译时代码生成 - 构建脚本、代码生成
- 反射 - 运行时类型检查和操作
- 对于减少样板、DSL、序列化等很有用
- 强大但应谨慎使用
- 会增加编译时间和复杂性
元编程是一个强大的工具,但它不是万能的。明智地使用它,更喜欢简单的抽象而不是复杂的元编程!
这结束了我们关于高级特性的章节。你现在已经涵盖了 X 语言的所有主要特性!
关于 Cargo 和 Crates.io 的更多内容
到目前为止,我们只使用了最基本的 Cargo 功能来构建、运行和测试我们的代码,但它可以做更多的事情。在本章中,我们将讨论它的一些更高级的功能,向你展示如何做到以下几点:
- 使用发布配置文件自定义构建
- 将库发布到 Crates.io
- 使用工作空间组织大型项目
- 从 Crates.io 安装二进制文件
- 使用自定义命令扩展 Cargo
我们在本书前面介绍的功能只是 Cargo 能力的一小部分;虽然我们在这里没有完整的文档,但官方文档是关于其功能的最佳文档。
使用发布配置文件自定义构建
在 X 语言中,发布配置文件是预定义的、自定义的配置,允许程序员对编译选项进行更多控制。每个配置都独立于其他配置。
Cargo 有两个主要配置文件:dev 配置文件,Cargo 在运行 x build 时使用,以及 release 配置文件,Cargo 在运行 x build --release 时使用。
dev 配置文件定义为适合开发的良好默认值,release 配置文件定义为适合发布构建的良好默认值。
这些配置名称可能看起来很熟悉;它们出现在你的构建输出中:
$ x build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ x build --release
Finished release [optimized] target(s) in 0.0s
这里显示的 dev 和 release 表明编译器正在使用不同的配置文件。
当你的项目的 x.toml 中没有任何 [profile.*] 部分时,Cargo 对每个配置文件都有默认设置。通过将任何非默认设置添加到你想要自定义的配置文件的部分,你可以覆盖默认设置的任何子集。例如,这里是 dev 和 release 配置文件的 opt-level 设置的默认值:
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level 设置控制 X 语言应该对代码应用多少优化,范围从 0 到 3。应用更多优化会延长编译时间,所以如果你在开发过程中并希望编译得快,你不希望优化太多,即使生成的代码运行得更慢。这就是为什么 dev 的 opt-level 默认是 0 的原因。当你准备发布时,最好花更多时间编译。你只会在发布模式下编译一次,但你会多次运行编译后的程序,所以发布模式会以更长的编译时间换取更快的代码运行时间。这就是为什么 release 的 opt-level 默认是 3 的原因。
你可以通过在 x.toml 中添加不同的值来覆盖任何默认设置。例如,假设我们想在开发配置文件中使用优化级别 1。我们可以将这两行添加到项目的 x.toml 中:
[profile.dev]
opt-level = 1
这将覆盖默认设置 0。现在,当我们运行 x build 时,Cargo 将使用 dev 的默认设置,除了我们覆盖的 opt-level。因为我们将 opt-level 设置为 1,Cargo 会比默认值多应用一些优化,但不如发布构建那么多。
有关每个配置文件的配置选项和默认值的更多信息,请参阅 Cargo 文档。
将 Crate 发布到 Crates.io
你可以将自己的包发布到 Crates.io,与世界其他地方分享!在发布之前,你需要先创建一个帐户并获取 API 令牌。为此,请访问 crates.io 并使用 GitHub 帐户登录。(目前 GitHub 帐户是必需的,但将来可能会支持其他创建帐户的方式。)一旦你登录,请查看你的帐户设置并获取你的 API 密钥。然后,像这样使用该密钥登录:
$ x login abcdefghijklmnopqrstuvwxyz012345
Login for abcdefghijklmnopqrstuvwxyz012345 stored in /Users/you/.x/credentials
此命令会通知 X 语言你的秘密 API 令牌,并将其本地存储在 ~/.x/credentials 中。请确保不要与任何人共享此令牌;将其保密!如果你的令牌因任何原因被泄露,你应该立即在 Crates.io 上撤销并重新生成它。
现在你已登录,让我们看看发布需要什么!在发布之前,你需要在 crate 的 x.toml 文件中添加一些元数据。你需要确保你的 crate 有一个唯一的名称,设置一个描述,说明它的作用,选择一个许可证,并提供一个分类器(如果适用)。将这些内容添加到你的 x.toml 文件中:
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2024"
description = "一个有趣的猜数字游戏"
license = "MIT OR Apache-2.0"
# 这里可以添加更多键(查看文档!)
[dependencies]
authors 字段可能因你使用的 X 语言版本而异;请查看文档以获取详细信息。description 是对你的 crate 作用的简短描述。license 是一个许可证标识符值;你可以在 SPDX 许可证列表 中找到你可以使用的标识符。如果你想在多个许可证下发布,请用 OR 分隔这些标识符。
现在我们已经配置好了所有内容,让我们发布!发布 crate 是永久性的;特定版本永远无法被覆盖,但可以发布更多版本。让我们运行 x publish:
$ x publish
Updating registry `https://github.com/x-lang/crates.io-index`
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0 (file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
注意,当你运行 x publish 时,x publish 会先打包你的 crate,然后将其上传到 crates.io。如果其他人想使用你的 crate,他们可以像我们使用 rand crate 那样使用它:将其添加为依赖项并同意他们选择的任何许可证。
Cargo 工作空间
随着项目的发展,你可能想将包拆分为多个包,以便它们更容易维护。为此,Cargo 有一个称为工作空间的功能,允许我们管理多个相关的包,这些包是一起开发的。
让我们创建一个包含二进制文件和两个库的工作空间。首先,我们将创建工作空间并添加一个二进制文件:
$ mkdir add
$ cd add
接下来,在 add 目录中,我们将创建包含工作空间配置的 x.toml 文件。此文件不会有 [package] 部分,也不会有我们在其他 x.toml 文件中看到的元数据。相反,它将以 [workspace] 开头,这将允许我们通过指定工作空间成员的路径来将成员添加到工作空间:
[workspace]
members = [
"adder",
]
接下来,让我们使用 x new 在 add 目录中创建 adder 二进制包:
$ x new adder
Created binary (application) `adder` package
此时,我们可以使用 x build 构建工作空间。add 目录中的文件应该如下所示:
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.x
└── target
工作空间在顶层有一个 target 目录;adder 包没有自己的 target 目录。即使我们从 adder 目录内部运行 x build,编译后的工件仍会在 add/target 中而不是 add/adder/target 中。Cargo 以这种方式在工作空间中构建包,因为工作空间中的包应该相互依赖。如果每个包都有自己的 target 目录,那么它们必须在每次想要使用另一个包时重新编译。通过共享一个 target 目录,包可以避免不必要的重新构建。
在工作空间中创建第二个包
让我们创建另一个工作空间成员包,称为 add_one。让我们在顶层 x.toml 中调整 members 以包含 add-one 路径:
[workspace]
members = [
"adder",
"add-one",
]
然后生成一个包含函数的新库包 add-one:
$ x new add-one --lib
Created library `add-one` package
现在我们的目录应该如下所示:
├── Cargo.toml
├── add-one
│ ├── Cargo.toml
│ └── src
│ └── lib.x
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.x
└── target
让我们在 add-one/src/lib.x 中添加一个函数:
export function add_one(x: integer) -> integer {
x + 1
}
现在我们的 adder 包可以依赖我们的 add-one 包了。首先,我们需要通过在 adder/Cargo.toml 中添加路径依赖项来告诉 Cargo:
[dependencies]
add-one = { path = "../add-one" }
Cargo 不假设工作空间中的包会相互依赖,所以我们需要明确依赖关系。
接下来,让我们在 adder 包中使用 add_one 包中的 add_one 函数。打开 adder/src/main.x 并在顶部添加一个 import 行,将新的 add-one 库包引入作用域。然后修改 main 函数调用 add_one 函数。
import add_one::add_one
function main() {
let num = 10
println("Hello, world! {} plus one is {}!", num, add_one(num))
}
让我们通过在顶层 add 目录中运行 x build 来构建工作空间!
$ x build
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
要从顶层 add 目录运行 adder 包,我们可以使用 -p 参数和包名来指定我们要在工作空间中运行哪个包:
$ x run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
这运行了 adder/src/main.x 中的代码,它依赖于 add-one 包。
工作空间的外部依赖
注意,工作空间在顶层只有一个 Cargo.lock,而不是在每个包中都有。这确保所有包都使用相同版本的所有依赖项。如果我们将 rand 包同时添加到 adder/Cargo.toml 和 add-one/Cargo.toml 中,Cargo 会将这两个包解析为同一个版本的 rand 并将其记录在一个 Cargo.lock 中。让工作空间中的所有包使用相同的依赖项确保工作空间中的包相互兼容。
在工作空间中添加测试
让我们在 add_one 包中添加 add_two 函数的测试:
export function add_one(x: integer) -> integer {
x + 1
}
export function add_two(x: integer) -> integer {
x + 2
}
test it_works {
assert_eq!(4, add_two(2))
}
要运行工作空间中特定包的测试,我们可以使用 -p 参数并指定我们要测试的包的名称:
$ x test -p add-one
Finished test [unoptimized + debuginfo] target(s) in 0.0s
Running unittests (target/debug/deps/add_one-abcabcabc)
running 1 test
test it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
如果我们在没有指定包的情况下在顶层目录运行 x test,所有测试都会运行!
如你所见,工作空间是一种组织包的便捷方式。它们可以帮助保持相关包在一起,同时允许它们保持独立。你可以选择是否在一个工作空间中拥有所有包。
从 Crates.io 安装二进制文件
x install 命令允许你在本地安装和使用二进制 crate。这不打算取代你的系统包管理器;它旨在作为一种方便的工具,供 X 语言开发者安装他们在 crates.io 上共享的工具。注意你只能安装具有二进制目标的包。二进制目标是 crate 在其 src/main.x 或其他被指定为二进制的文件中有一个可执行程序,而不是仅打算作为库使用的库目标。
默认情况下,x install 会将已安装的二进制文件存储在你的系统的配置目录下的 bin 文件夹中。如果你使用 xup 安装了 X 语言并且没有任何非默认配置,这个目录将是 $HOME/.x/bin。确保该目录在你的 $PATH 中,以便能够运行 x install 安装的程序!
例如,在第 12 章中我们提到有一个 X 语言版本的 grep 工具,叫做 ripgrep,用于搜索文件。要安装 ripgrep,我们可以运行:
$ x install ripgrep
Updating crates.io index
Downloaded ripgrep v11.0.2
Downloaded ...
Compiling ...
Finished release [optimized] target(s) in ...
Installing /Users/you/.x/bin/rg
Installed package `ripgrep v11.0.2` (executable `rg`)
安装输出显示了安装的位置和命令的名称,在这种情况下是 rg。然后,只要你的 $PATH 配置正确,就可以运行 rg --help 并开始使用更快的、用 X 语言编写的搜索文件工具!
使用自定义命令扩展 Cargo
Cargo 的设计使得你可以在不修改 Cargo 本身的情况下用新的子命令扩展它。如果 $PATH 中有名为 x-something 的二进制文件,你可以像 Cargo 子命令一样运行它,就像 x something 一样。这些自定义命令也会在你运行 x --list 时显示出来。能够使用 x install 安装插件,然后像内置的 Cargo 工具一样运行它们,这是让 Cargo 非常好扩展的一个不错的便利功能!
总结
在本章中,我们讨论了一些更高级的 Cargo 功能,包括:
- 发布配置文件,让你自定义构建选项
- 在 crates.io 上发布包
- 使用工作空间管理多个相关包
- 用
x install安装二进制 crate - 用自定义命令扩展 Cargo
这些只是 Cargo 能力的一小部分;有关其所有功能的更多信息,请查看 Cargo 文档。
智能指针
指针是一个包含内存地址的变量;这个地址指向,或“指向“,一些其他数据。在 X 语言中,最常见的指针类型是我们在第 4 章中看到的引用。引用由 & 字符表示,并借用它们指向的值。除了引用数据之外,它们没有任何特殊功能。它们也没有任何开销,因此是最常用的指针类型。
相比之下,智能指针是数据结构,它们像指针一样行动,但也有额外的元数据和功能。智能指针的概念不是 X 语言独有的:它起源于 C++,并存在于其他语言中。X 语言在标准库中定义了各种智能指针,它们提供的功能超出了引用所提供的功能。为了探索一般概念,我们将看看一些不同的智能指针示例,最值得注意的是引用计数智能指针类型。这种指针允许你拥有多个数据所有者,通过跟踪所有者的数量,当没有所有者剩余时清理数据。
在 X 语言中,使用 struct 自定义类型是常见的,并且有一个关键的区别使智能指针与普通结构体不同:智能指针实现了 Deref 和 Drop trait。Deref trait 允许智能指针实例的行为类似于引用,因此你可以编写适用于引用或智能指针的代码。Drop trait 允许你自定义当智能指针实例超出作用域时运行的代码。在本章中,我们将讨论这两个 trait,并演示为什么它们对智能指针很重要。
鉴于智能指针模式是 X 语言中经常使用的设计模式,本章不会涵盖标准库中可用的每一个智能指针。许多库都有自己的智能指针,你甚至可以自己编写一些。我们将介绍标准库中最常见的智能指针:
Box<T>,用于在堆上分配值Rc<T>,一个引用计数类型,允许多重所有权Ref<T>和RefMut<T>,通过RefCell<T>访问,它在运行时而不是编译时强制借用规则
此外,我们将介绍内部可变性模式,其中不可变类型公开了一个用于改变内部值的 API。我们还将讨论引用循环:它们如何泄漏内存以及如何防止它们。
让我们开始吧!
使用 Box 指向堆上的数据
最直接的智能指针是 box,其类型写为 Box<T>。Box 允许你将数据存储在堆上,而不是栈上。栈上剩下的是指向堆上数据的指针。
让我们看看 Box<T> 是什么样的:
let b = Box::new(5)
println("b = {}", b)
我们使用 Box::new 定义了变量 b,它包含值 5。这段代码会打印 b = 5;在这种情况下,我们可以像访问栈上的数据一样访问 box 中的数据。与任何拥有的值一样,当 box 超出作用域时(就像在 main 结束时 b 一样),它将被释放。释放发生在 box(存储在栈上)和它指向的数据(存储在堆上)上。
将单个值放在堆上不是很有用,所以你不会经常像刚才那样单独使用 box。box 有用的情况是,当你有一个类型,其大小在编译时无法知道,并且你想在需要精确大小的上下文中使用该类型值时。让我们探索这种情况。
使用 Box 允许递归类型
box 的主要用例是当你有一个可能是递归的类型时——一个可以将自身的另一个实例作为自己的一部分的类型。因为 box 有一个已知的大小,我们可以通过在递归类型定义中插入 box 来创建递归类型。
让我们探索 cons list,这是一种主要在函数式编程语言中发现的数据结构,作为递归类型的示例。除了递归情况外,我们将定义的 cons list 是一个简单的结构;我们将用它来练习使用 box。
更多关于 cons list 的信息
cons list(或“construct list“)是一个源自 Lisp 编程语言及其方言的数据结构。一个 cons list 是一个嵌套对列表;它的构造函数的传统名称(因此得名 cons list)是 cons。cons 函数接受两个参数:一个值和另一个 cons 函数,并创建一个新的 cons 函数,该函数又包含该值和另一个 cons 函数,以此类推,直到我们到达 nil 或 Nil,这是结束条件,它不包含下一个值。cons 大致翻译为“构造函数“。
这是一个包含 1、2 和 3 的 cons list 的伪代码表示,每个 cons 函数都在括号中:
(1, (2, (3, Nil)))
cons list 中的每个项目包含两个元素:当前项目的值和下一个项目。最后一个项目只包含一个名为 Nil 的值,没有下一个项目。我们通过递归调用 cons 函数来定义 cons list。我们使用特殊的 Nil 名称来指定 cons list 的结束条件。注意,这与我们在第 6 章中讨论的列表类型不同!cons list 不是现代 X 语言代码中常用的数据结构,但它是一个概念上清晰的例子,展示了 box 如何让我们定义递归类型。
让我们在示例中更详细地探索 cons list。
定义 cons list
首先,让我们定义一个包含第一个项目和下一个项目的枚举,我们称之为 List。枚举将有两个变体:Cons,它包含一个整数和一个 List,以及 Nil,表示 cons list 的结束。代码如下:
type List = Cons(integer, List) | Nil
// 这还不能编译!
等等,这段代码有一个问题:它无法编译!让我们看看为什么。如果我们尝试编译这段代码,我们会得到一个错误,说有一个“递归类型没有无限大小“。
为什么会这样?让我们想想为什么。要弄清楚 List 类型需要多少空间,X 语言会查看每个变体,看看哪个变体需要最多空间。X 语言看到 Cons 变体包含一个 integer 和一个 List,这意味着 Cons 需要 integer 的大小加上 List 的大小。为了弄清楚 List 需要多少空间,它查看它的变体,从 Cons 变体开始,依此类推,无限循环。
为了修复这个错误,我们需要给 X 语言一个关于 List 有多大的提示,通过打破递归,在其中一个变体中插入一个 box。因为 box 是一个指针,我们总是知道它需要多少空间:指针的大小不会根据它指向的数据量而变化。这意味着我们可以在 Cons 变体中放一个 Box,它指向的是下一个 List 值,而不是直接放另一个 List 值。从概念上讲,我们仍然有一个“包含“其他列表的列表,但现在这种表示是通过将 Box 放在下一个 List 前面,而不是直接包含它来实现的。
这是我们的修改:
type List = Cons(integer, Box<List>) | Nil
现在 Cons 变体将需要 integer 的大小加上 box 的大小。Nil 变体不存储任何值,所以它需要的空间比 Cons 变体少。现在我们知道 List 枚举最大需要 integer 的大小加上 box 的大小。通过使用 box,我们打破了无限递归链,因此 X 语言可以计算出存储 List 值需要多大的大小。
box 只是间接和堆分配;它们没有任何其他特殊功能,比如我们将在本章后面看到的其他智能指针。它们也没有 Deref 或 Drop trait 提供的功能,所以我们将通过查看这些 trait 以及我们如何在自定义智能指针上使用它们来继续!
无畏并发
并发编程(Concurrent programming),其中程序的不同部分独立执行,以及并行编程(parallel programming),其中程序的不同部分同时执行,在计算机可以更轻松地利用多个处理器的时代变得越来越重要。历史上,这两种编程范式都很困难且容易出错:X 语言希望改变这一点。
最初,X 语言团队认为确保内存安全和防止并发问题是两个需要用不同方法解决的不同挑战。随着时间的推移,团队发现所有权和类型系统实际上是一组强大的工具,可以帮助同时管理内存安全和并发问题!通过利用所有权和类型检查,X 语言中的许多并发错误在编译时是错误,而不是运行时错误。因此,你可以花更多时间让程序的并发部分按预期工作,而不是花时间追踪错误。
我们将在本章中讨论的概念是:
- 如何使用线程同时运行多段代码
- 如何在线程之间使用消息传递并发,使用通道发送数据
- 如何使用状态共享并发,其中多个线程可以访问同一块数据
Sync和Sendtrait,它们使 X 语言的并发保证扩展到用户定义的类型以及标准库提供的类型
让我们开始吧!
使用线程同时运行代码
在大多数现代操作系统中,执行的程序代码在进程中运行,操作系统一次管理多个进程。在你的程序中,你可以有同时运行的独立部分。运行这些独立部分的功能称为线程(thread)。例如,Web 服务器可以有多个线程,这样它可以同时响应多个请求。
将代码拆分为程序中的多个线程可以同时完成更多工作,但这也增加了复杂性。因为线程是同时运行的,所以无法保证线程中的代码部分的运行顺序。这可能会导致问题,例如:
- 竞争条件(Race conditions),其中线程以不一致的顺序访问数据或资源
- 死锁(Deadlocks),其中两个线程都在等待对方释放它们需要的资源,阻止两个线程继续
- 只在特定情况下发生的错误,很难可靠地复现和修复
X 语言试图减轻使用线程的负面影响,但在多线程上下文中编程仍然需要仔细思考,并且代码结构需要与单线程程序不同。
编程语言以几种不同的方式实现线程。许多操作系统提供一个 API 用于创建新线程。这种由编程语言使用操作系统 API 创建线程的模型通常称为 1:1,意思是一个操作系统线程对应一个语言线程。
还有绿色线程(green threads)模型,其中语言创建自己的线程。这些绿色线程在被称为 M:N 模型的一定数量的操作系统线程上运行,其中 M 个绿色线程对应 N 个操作系统线程,其中 M 和 N 不一定相同。
每个模型都有其优点和缺点,对 X 语言最重要的优点是运行时大小。运行时(Runtime)指的是语言包含在每个二进制文件中的代码;对于某些语言,这段代码可能很大,对于其他语言可能很小。通常,较小的运行时大小的权衡是更少的功能。X 语言团队需要一个几乎没有运行时的语言,同时仍然尽可能保留功能。
早期,X 语言团队尝试实现绿色线程,但发现很难使它们在所有目标平台上都能很好地工作。因此,X 语言标准库仅提供 1:1 线程的实现。X 语言的设计使得你可以在这个基础上实现 M:N 线程,如果你愿意牺牲一些性能以获得诸如更好的调度控制和更低的操作系统线程开销等功能。
让我们通过看看标准库提供的线程 API 来探索 X 语言中的线程!
使用 spawn 创建新线程
要创建新线程,我们调用 thread::spawn 函数并向其传递一个闭包(我们在第 13 章中讨论过),其中包含我们要在新线程中运行的代码。示例展示了如何从主线程打印一些文本,并在新线程中打印其他一些文本:
import std::thread
import std::time::Duration
function main() {
thread::spawn(function() {
for i in 1..10 {
println("线程中的数字 {}", i)
thread::sleep(Duration::from_millis(1))
}
})
for i in 1..5 {
println("主线程中的数字 {}", i)
thread::sleep(Duration::from_millis(1))
}
}
注意,使用此代码,新线程将在主线程完成时立即停止,无论新线程是否完成打印其所有数字!输出可能每次看起来都有点不同,但它应该类似于:
主线程中的数字 1
线程中的数字 1
主线程中的数字 2
线程中的数字 2
主线程中的数字 3
线程中的数字 3
主线程中的数字 4
线程中的数字 4
主线程中的数字 5
线程中的数字 5
线程可能会交替,但这不能保证:这取决于你的操作系统如何调度线程。在这里,主线程先打印,即使新线程的打印语句在代码中首先出现。即使我们告诉新线程一直打印到 i 是 9,它只在主线程关闭之前走到 5。
请注意,你从 thread::spawn 获得的值类型是 JoinHandle,它是一个拥有值的类型,当你在其上调用 join 方法时,它将等待线程完成。示例修改了示例中的代码,以使用 JoinHandle 的 join 方法,并确保新线程在主线程退出之前完成:
import std::thread
import std::time::Duration
function main() {
let handle = thread::spawn(function() {
for i in 1..10 {
println("线程中的数字 {}", i)
thread::sleep(Duration::from_millis(1))
}
})
for i in 1..5 {
println("主线程中的数字 {}", i)
thread::sleep(Duration::from_millis(1))
}
handle.join().unwrap()
}
通过在句柄上调用 join,我们可以阻塞当前线程,直到句柄表示的线程完成。阻塞(Blocking)一个线程意味着阻止该线程继续工作或退出。根据我们是否将调用 join 放在主线程的 for 循环之前或之后,我们的输出将不再交替!尝试看看;这就是线程调度工作的方式。
这就是使用线程的基本原理!让我们继续使用线程,通过使用 move 关键字与闭包一起从一个线程向另一个线程移动数据。
使用 move 闭包和线程
我们经常使用 thread::spawn 中的 move 关键字与闭包一起,以允许闭包从环境中获取数据的所有权,从而允许我们在一个线程中使用来自另一个线程的数据。
在第 13 章中,我们提到在闭包参数列表之前可以使用 move 关键字来强制闭包获取它使用的环境中值的所有权。这种技术在创建新线程时特别有用,以便我们将数据从一个线程移动到另一个线程。
请注意,在示例中,我们传递给 thread::spawn 的闭包没有任何来自环境的参数:我们没有在新线程运行的代码中使用来自主线程的任何数据。要在新线程中使用来自主线程的数据,闭包需要捕获它需要的值。示例显示了一个尝试在新线程中创建并使用来自主线程的向量引用的闭包。但是,这还不能工作,原因我们马上会解释。
import std::thread
function main() {
let v = [1, 2, 3]
let handle = thread::spawn(function() {
println("这是一个向量: {:?}", v)
})
handle.join().unwrap()
}
闭包使用了 v,所以闭包会捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在新线程中运行这个闭包,所以我们需要在新线程中访问 v。但是,当我们编译这个示例时,我们会得到以下错误:
error: 闭包可能比借用值活得更久
X 语言告诉我们,闭包不能借用 v,因为它不知道新线程会活多久,所以它不知道对 v 的引用是否总是有效。
为了修复这个编译错误,让我们使用我们在第 13 章中学到的 move 关键字,强制闭包获取它正在使用的值的所有权,而不是让 X 语言推断它应该借用值。示例展示了如何修改示例以添加 move 关键字到闭包,这将解决这个问题:
import std::thread
function main() {
let v = [1, 2, 3]
let handle = thread::spawn(move function() {
println("这是一个向量: {:?}", v)
})
handle.join().unwrap()
}
通过在闭包前面添加 move 关键字,我们强制闭包获取它正在使用的值的所有权,而不是让 X 语言推断它应该借用。这修复了我们的问题!
既然我们已经看到了 thread::spawn 和 JoinHandle 的基本原理,并且我们已经看到了如何将数据从一个线程移动到另一个线程,让我们看看在线程之间通信的一些方法。
模式和模式匹配
模式是 X 语言中一个特殊的语法,用于匹配类型的结构,无论是简单的还是复杂的。与 when/is 表达式和其他构造一起使用模式使我们能够更好地控制程序的控制流。模式由以下部分的某种组合组成:
- 字面量
- 解构的数组、枚举、结构体或元组
- 变量
- 通配符
- 占位符
这些部分描述了我们要处理的数据的形状,然后我们将其与模式匹配,以确定我们的程序是否具有继续执行特定代码路径所需的正确形状数据。
在本章中,我们将查看所有可以使用模式的地方,所有有效模式类型,以及如何使用它们。
所有可以使用模式的地方
到目前为止,我们已经在几个上下文中非正式地使用了模式,但我们还没有讨论所有可以使用它们的地方。让我们开始一个全面的列表,列出所有模式有效的位置!
when/is 表达式
正如我们在第 6 章中讨论的,我们可以在 when/is 表达式中使用模式。作为提醒,when/is 表达式的一般形式是:
when VALUE is {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
_ => EXPRESSION,
}
when/is 表达式必须是穷尽的,这意味着 VALUE 的每个可能的可能性都必须被覆盖。特定的模式 _ 是一个很好的包罗万象的模式,它匹配任何内容而不绑定到变量,因此它对于最后一个 arm 很有用。
if let 条件表达式
正如我们在第 6 章中讨论的,if let 表达式主要是作为编写等效于仅匹配一个情况的 when/is 表达式的简写而构造的。可选地,if let 可以有一个相应的 else,其中包含如果 if let 中的模式不匹配则运行的代码。
示例显示了可以混合和匹配 if let、else if 和 else if let 表达式。这比 when/is 表达式更灵活,因为 when/is 只能有一个值和一组针对其第一个 arm 进行检查的模式。此外,X 语言不要求 if let、else if、else if let arm 中的条件相互关联。
让我们看一下:
let favorite_color: Option<String> = None
let is_tuesday = false
let age: Result<integer, String> = Ok(34)
if let Some(color) = favorite_color {
println("使用你喜欢的颜色 {}, 作为背景", color)
} else if is_tuesday {
println("星期二是绿色的日子!")
} else if let Ok(age) = age {
if age > 30 {
println("使用橙色作为背景色")
} else {
println("使用紫色作为背景色")
}
} else {
println("使用蓝色作为背景色")
}
这段代码可以运行,但不一定有意义(我们选择的颜色决策不一定基于任何真实标准)。重要的是要看看 if let 的灵活性允许我们表达什么。
while let 条件循环
while let 条件循环与 if let 类似,只要模式继续匹配,它就会允许 while 循环运行。示例显示了一个使用 while let 循环的示例,只要 pop() 调用返回 Some,该循环就会打印向量中的每个值。当它返回 None 时,循环停止。
let mut stack = List::new()
stack = stack + [1]
stack = stack + [2]
stack = stack + [3]
while let Some(top) = stack.pop() {
println("{}", top)
}
这段代码将打印 3、2,然后是 1。pop 方法获取列表的最后一个元素并返回 Some(value)。如果列表为空,pop 返回 None。while let 循环继续运行循环体,只要 pop 返回 Some。当它返回 None 时,循环停止。我们可以使用 while let 来弹出栈中的每个元素。
for 循环
正如我们在第 3 章中讨论的,在 for 循环中,直接在 for 关键字后面的是模式。例如,在 for x in y 中,x 是模式。示例显示了如何在 for 循环中使用模式来解构元组,因为 for 循环是 for 循环的一部分:
let v = [(1, 'a'), (2, 'b'), (3, 'c')]
for (index, value) in v.iter().enumerate() {
println("{} 在索引 {}", value, index)
}
因为我们使用 enumerate 方法适配了迭代器,所以它每次迭代都会产生一个元组,其中包含索引和该索引处的值。第一次迭代将产生元组 (0, (1, 'a'))。当我们将此值与模式 (index, value) 匹配时,index 将为 0,value 将为 (1, 'a')。
这段代码的输出如下:
(1, 'a') 在索引 0
(2, 'b') 在索引 1
(3, 'c') 在索引 2
let 语句
在本章之前,我们只讨论了使用模式与 when/is 和 if let,但我们也可以在 let 语句中使用模式!例如,考虑这个带有 let 的简单变量赋值:
let x = 5
x 在这里是一个模式!每次我们使用 let 语句时,我们都在使用模式,即使我们没有意识到这一点!让我们看一个更复杂的 let 示例:使用模式来解构元组。
let (x, y, z) = (1, 2, 3)
这里我们用一个元组匹配一个模式。如果模式中的元素数量与元组中的元素数量匹配,X 语言会将元组中的每个值绑定到模式中的相应变量。在这种情况下,1 将绑定到 x,2 将绑定到 y,3 将绑定到 z。
如果模式中的元素数量与元组中的元素数量不匹配,整个类型将不匹配,我们将得到编译错误。
函数参数
与 let 类似,函数参数也可以是模式!让我们看一个例子:
function print_coordinates(&(x, y): &(integer, integer)) {
println("当前位置: ({}, {})", x, y)
}
function main() {
let point = (3, 5)
print_coordinates(&point)
}
这段代码将打印 当前位置: (3, 5)。值 &(3, 5) 匹配模式 &(x, y),所以 x 是值 3,y 是值 5。
我们也可以在闭包参数列表中使用模式,就像我们在函数参数列表中使用它们一样,因为闭包类似于函数,正如我们在第 13 章中讨论的那样。
好的,所以我们已经看到了很多使用模式的地方,但模式在我们使用它们的所有地方的行为并不相同;在某些地方,模式必须是不可反驳的;在其他地方,它们可能是可反驳的。让我们接下来讨论这个区别。
不安全 X
到目前为止,我们讨论的所有代码都在编译时强制执行 X 语言的安全保证。但是 X 语言有第二个语言隐藏在表面之下,它不强制执行这些安全保证:这被称为不安全 X(Unsafe X),它的工作方式与常规 X 语言一样,但给了我们额外的超能力。
不安全 X 存在是因为静态分析本质上是保守的。当编译器试图确定代码是否持有某些保证时,它拒绝一些有效的程序,同时接受一些无效的程序。在这种情况下,我们可以使用不安全代码,因为我们知道程序是正确的。
另一个原因是底层计算机硬件本身就是不安全的。如果 X 语言不允许我们做不安全的操作,我们根本无法完成某些任务。X 语言需要能够进行系统级编程,例如直接与操作系统交互,甚至编写你自己的操作系统!这也是不安全 X 语言存在的原因之一。
让我们看看不安全 X 是什么以及如何使用它。在本章中,我们将介绍:
- 什么是不安全操作
- 如何使用不安全操作
- 为什么不安全操作存在
- 何时使用不安全操作
让我们开始!
不安全超能力
你可以通过使用 unsafe 关键字切换到不安全 X,并开始一个新的不安全代码块。不安全 X 给你的超能力是:
- 解引用原始指针
- 调用不安全函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
这些就是不安全超能力!让我们看看每一个。
在我们深入研究之前,让我们先记住这一点:不安全代码不会关闭借用检查器或禁用 X 语言的任何其他安全检查:如果你在不安全代码中使用引用,它仍然会被检查。不安全关键字只给你访问这四个功能,然后不被编译器检查。你仍然会在不安全代码中获得一定程度的安全性。
此外,不安全不一定意味着代码是危险的或必然会有内存安全问题:其意图是作为程序员,你将确保不安全块内的代码以有效的方式访问内存。
人们会犯错误,错误会发生,但通过要求这四个操作被包含在一个标有 unsafe 的块中,你将知道任何内存安全错误都在该块内。保持 unsafe 块尽可能小;稍后,当你调查内存错误时,你会很高兴这样做。
为了尽可能隔离不安全代码,最好将不安全代码包含在一个小的不安全块中,然后提供一个安全的 API。我们稍后会看到这一点,因为在我们了解了四个不安全超能力之后,我们会看一些例子,其中不安全代码被包装在一个安全的抽象后面。
解引用原始指针
在第 4 章中,我们看到了智能指针,并简要提到了原始指针(raw pointer)。类似于引用,原始指针是不可变的或可变的,分别写为 *const T 和 *mut T。星号不是解引用运算符;它是类型名称的一部分。在原始指针的上下文中,不可变意味着指针在被赋值后不能直接改变。
原始指针与引用和智能指针的区别在于:
- 允许忽略借用规则,同时拥有不可变和可变指针,或多个指向同一位置的可变指针
- 不保证指向有效内存
- 允许为空
- 不实现任何自动清理
通过选择不拥有 X 语言给你的任何保证,你以放弃安全保证为代价获得了更好的性能或与其他语言或硬件接口的能力。
让我们看一下如何从引用创建原始指针,这是一个安全操作:
let mut x = 5
let r1 = &x as *const integer
let r2 = &mut x as *mut integer
我们正在从引用创建 *const integer 和 *mut integer 原始指针。我们可以这样做,因为我们使用 as 将不可变和可变引用强制转换为它们相应的原始指针类型。直接从引用创建原始指针是安全的;我们还没有对原始指针做任何危险的事情。
注意,我们不需要在这里用 unsafe 标记这个代码。我们可以在安全代码中创建原始指针;我们只是不能在不安全块外解引用原始指针,正如我们稍后会看到的那样。
我们也可以直接从字面量创建原始指针,而不是从引用创建它们,但这很危险,所以让我们等到展示了 unsafe 块后再这样做。
我们刚刚展示了你可以在安全代码中创建原始指针。让我们看看当我们有一个原始指针时,我们可以用它做什么:解引用它。解引用原始指针需要 unsafe,如下所示:
let mut x = 5
let r1 = &x as *const integer
let r2 = &mut x as *mut integer
unsafe {
println("r1 指向: {}", *r1)
println("r2 指向: {}", *r2)
}
这段代码将打印出 r1 指向: 5 和 r2 指向: 5。通过在 unsafe 块内解引用原始指针,我们选择承担责任,如果有问题,那是我们的错,不是 X 语言的错。
因为原始指针可以为空、无效或别名,所以使用它们时很容易导致内存安全问题。如果可能,请尽量避免使用原始指针!
调用不安全函数或方法
第二个要求 unsafe 块的操作是调用不安全函数。不安全函数和方法看起来就像普通函数和方法,只是它们在定义的其余部分之前有一个额外的 unsafe。这里的 unsafe 关键字表示该函数在被调用时有我们需要遵守的要求,因为 X 语言不能保证我们已经满足了这些要求。
让我们看看一个不安全函数:
unsafe function dangerous() {
// 一些危险的代码
}
unsafe {
dangerous()
}
不安全函数必须在 unsafe 块内调用。如果我们尝试在没有 unsafe 块的情况下调用 dangerous,我们会得到一个错误。
不安全函数体实际上是一个不安全块,因此我们可以在不安全函数中执行其他不安全操作,而无需添加另一个 unsafe 块。
创建一个不安全函数的安全抽象是一个常见的模式。让我们看一下标准库中的一个例子:split_at_mut。这个函数在列表上工作,它是安全的。让我们看看为什么它是安全的,但它使用了不安全代码。这个方法接受一个列表和一个索引,并将列表分成两部分:该索引之前的所有内容和该索引之后的所有内容。
let mut v = [1, 2, 3, 4, 5, 6]
let (left, right) = v.split_at_mut(3)
println!("{:?}, {:?}", left, right)
我们不能用安全 X 实现这个函数。让我们尝试思考一下:
// 这不会编译!
function split_at_mut(slice: &mut [integer], mid: integer) -> (&mut [integer], &mut [integer]) {
let len = slice.len()
assert!(mid <= len)
(&mut slice[..mid], &mut slice[mid..])
}
这无法编译,因为借用检查器不允许我们对同一个切片进行两次可变借用。然而,我们知道我们正在做的事情是安全的;我们正在借用切片的两个独立部分。为了告诉 X 语言这一点,我们需要使用不安全代码。让我们看看 split_at_mut 的实际实现:
use core::slice;
function split_at_mut(slice: &mut [integer], mid: integer) -> (&mut [integer], &mut [integer]) {
let len = slice.len()
let ptr = slice.as_mut_ptr()
assert!(mid <= len)
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid)
)
}
}
我们正在使用我们知道的一些不安全操作,并且我们通过将不安全代码包装在安全函数中来安全地抽象它。
好的,所以调用不安全函数是第二个不安全操作。让我们继续第三个:访问可变静态变量。
访问或修改可变静态变量
我们还没有谈论过全局变量(global variables),X 语言确实支持它们,但它们由于借用规则而有问题。如果两个线程正在访问同一个可变全局变量,可能会导致数据竞争。
在 X 语言中,全局变量称为静态(static)变量。让我们看一个例子:
static HELLO_WORLD: &str = "Hello, world!"
function main() {
println("名称是: {}", HELLO_WORLD)
}
静态变量类似于常量。我们用 static 而不是 const 来声明它们,并且它们的类型总是被注解(所以我们不能省略 &'static str)。静态变量的名称是 SCREAMING_SNAKE_CASE,按照惯例。
静态变量和常量之间的区别在于静态变量在内存中有一个固定的地址;使用该值将始终访问相同的地址。另一方面,常量在每次使用时都可以复制它们的数据。另一个区别是静态变量可以是可变的。访问和修改可变静态变量是不安全的。让我们看看:
static mut COUNTER: integer = 0
function add_to_count(inc: integer) {
unsafe {
COUNTER = COUNTER + inc
}
}
function main() {
add_to_count(3)
unsafe {
println("COUNTER: {}", COUNTER)
}
}
这段代码将打印 COUNTER: 3。这工作正常,但访问 COUNTER 需要 unsafe。使用可变静态变量很难实现数据竞争安全,因此 X 语言认为它们是不安全的。
实现不安全 trait
最后一个我们可以在 unsafe 块内执行的操作是实现不安全 trait。当至少有一个方法具有编译器无法验证的不变量时,trait 就是不安全的。我们可以通过在 trait 之前添加 unsafe 并在实现该 trait 时也添加 unsafe 来将 trait 声明为不安全:
unsafe trait Foo {
// 方法
}
unsafe impl Foo for integer {
// 方法实现
}
通过使用 unsafe impl,我们承诺我们将维护编译器无法验证的不变量。
这就是四个不安全超能力!让我们回顾一下:
- 解引用原始指针
- 调用不安全函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
好的,关于不安全 X 就讲到这里!我们不会在本书中进一步使用不安全 X,但现在你知道它是什么以及如何使用它了。
高级 Trait
我们在第 8 章中第一次讨论了 trait,但和我们生命周期一样,我们没有讨论一些更高级的细节。现在我们对 X 语言有了更多了解,我们可以深入研究。
关联类型在 trait 定义中指定占位符类型
关联类型(Associated types)将类型占位符与 trait 连接起来,以便 trait 的方法签名可以在其签名中使用这些占位符类型。trait 的实现者将为他们正在实现的 trait 的特定用例指定要用于占位符类型的具体类型。这样,我们就可以定义一个使用某些类型的 trait,而不必确切知道这些类型是什么,直到 trait 被实现。
我们在本书前面提到的一个具有关联类型的 trait 是标准库中的 Iterator trait。它有一个名为 Item 的关联类型,代表迭代器正在迭代的值的类型。Iterator trait 的定义如下所示:
trait Iterator {
type Item
function next(self: &mut Self) -> Option<Self::Item>
}
Item 类型是一个占位符类型,而 next 方法的定义表明它将返回 Option<Self::Item> 类型的值。Iterator trait 的实现者将为 Item 指定一个具体类型,并且 next 方法将返回一个包含该具体类型值的 Option。
关联类型与泛型的不同之处在于,使用关联类型时,我们不需要为每个实现注解类型,因为我们不能为一个类型多次实现该 trait。让我们看看这看起来像什么。
让我们看一下 Iterator trait 在一个名为 Counter 的类型上的实现,我们在第 13 章中定义了该类型,它迭代 1 到 5 的值:
type Counter = {
count: integer
}
impl Iterator for Counter {
type Item = integer
function next(self: &mut Self) -> Option<integer> {
if self.count < 6 {
let current = self.count
self.count = self.count + 1
Some(current)
} else {
None
}
}
}
我们为 Item 关联类型指定了 integer 类型,并实现了 next 方法,使其返回 Option<integer>。
如果 trait 是泛型的,会是什么样子?Iterator trait 可能已经用泛型定义了,如下所示:
trait Iterator<T> {
function next(self: &mut Self) -> Option<T>
}
那么我们需要这样实现:
impl Iterator<integer> for Counter {
function next(self: &mut Self) -> Option<integer> {
// --snip--
}
}
然后我们也可以为 Counter 实现 Iterator<&str>、Iterator<Float> 等等,这样 Counter 就有多个 next 方法的实现,每个都有自己的类型。
使用关联类型而不是泛型 trait 的好处是,我们不需要为每个实现注解类型,因为我们不能为一个类型多次实现该 trait。使用关联类型,一旦我们在 impl Iterator for Counter 中选择了 Item 的类型,我们就不必再次指定我们正在迭代 integer 值。
默认泛型类型参数和运算符重载
我们可以为泛型类型参数指定默认类型。如果默认类型足够,这消除了 trait 用户为泛型类型指定具体类型的需要。指定泛型类型的默认类型的语法是在声明泛型类型时使用 <PlaceholderType=ConcreteType>。
这种技术的一个很好的例子是一个用于运算符重载的 trait。运算符重载允许我们在某些情况下自定义运算符(如 +)的行为。
X 语言不允许你创建自己的运算符或重载任意运算符。但是你可以通过实现与该运算符对应的 trait 来重载 std::ops 中列出的操作和相应的 trait。例如,在示例中,我们通过实现 Add trait 来重载 + 运算符,以将两个 Point 实例加在一起。
use std::ops::Add
type Point = {
x: integer,
y: integer
}
impl Add for Point {
type Output = Point
function add(self: Self, other: Self) -> Point {
{
x: self.x + other.x,
y: self.y + other.y
}
}
}
function main() {
assert_eq!({x: 1, y: 0} + {x: 2, y: 3}, {x: 3, y: 3})
}
add 方法将两个 Point 实例的 x 值和 y 值分别相加,以创建一个新的 Point。Add trait 有一个名为 Output 的关联类型,它确定从 add 方法返回的类型。
让我们更仔细地看看 Add trait 是如何工作的:
trait Add<Rhs=Self> {
type Output
function add(self: Self, rhs: Rhs) -> Self::Output
}
这段代码看起来应该很熟悉:它是一个带有一个方法和一个关联类型的 trait。新的部分是 Rhs=Self:这个语法称为默认类型参数。Rhs 泛型类型参数(用于“右手边“)定义了 add 方法中 rhs 参数的类型。如果我们在实现 Add trait 时没有为 Rhs 指定具体类型,Rhs 的类型将默认为 Self,这将是我们正在实现 Add 的类型。
当我们为 Point 实现 Add 时,我们使用了默认的 Rhs,因为我们想将两个 Point 实例加在一起。让我们看一个实现 Add trait 的例子,在这个例子中,我们想要自定义 Rhs 类型而不是使用默认类型。
我们有两个结构,Millimeters 和 Meters,分别持有以毫米和米为单位的值。我们想通过实现 Add 来将毫米加到米上,其中 Add 在 Millimeters 上的实现将 Meters 作为 Rhs。
use std::ops::Add
type Millimeters = { value: integer }
type Meters = { value: integer }
impl Add<Meters> for Millimeters {
type Output = Millimeters
function add(self: Self, other: Meters) -> Millimeters {
{ value: self.value + (other.value * 1000) }
}
}
要将毫米加到米上,我们需要设置 impl Add<Meters> 以赋予 Rhs 类型参数一个值,而不是使用默认的 Self。
默认类型参数主要用于两种方式:
- 扩展一个类型而不破坏现有代码
- 允许在大多数用户不需要的特定情况下进行自定义
标准库的 Add trait 是第二种方式的一个例子:通常你会将两个相似的类型加在一起,但 Add trait 提供了自定义该功能的能力。使用 Add trait 的默认类型参数意味着大多数时候你不必指定额外的参数,从而减少了必须指定的样板代码。
好的,关于关联类型和默认类型参数就讲到这里!让我们继续关于 trait 的另一个高级特性:完全限定语法以消除歧义。
高级类型
在本节中,我们将探索一些关于类型的更高级功能:类型别名、never 类型和动态大小类型。让我们开始!
使用类型别名创建类型同义词
除了使用泛型,我们还可以讨论类型别名(type alias)——这是一种为现有类型赋予另一个名称的方法,称为同义词。我们将使用 type 关键字来实现。例如,我们可以像这样别名 integer:
type Kilometers = integer
let x: integer = 5
let y: Kilometers = 5
println("x + y = {}", x + y)
因为 Kilometers 是 integer 的同义词,它们是同一类型。所以我们可以将 integer 和 Kilometers 相加,并且我们可以将 Kilometers 类型的值传递给接受 integer 类型参数的函数。但是通过使用这种技术,我们不会获得我们在第 4 章中讨论的类型检查的好处。换句话说,如果我们不小心将 Kilometers 和 integer 的值混合在一起,编译器不会给我们错误。
类型别名的主要用例是减少重复。例如,我们可能有一个如下所示的长类型:
let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi"))
在每个函数签名中写出这个长长的类型会很累人且容易出错。想象一下,必须在函数定义中多次写出这个。谢天谢地,我们可以使用类型别名来缩短它:
type Thunk = Box<Fn() + Send + 'static>
let f: Thunk = Box::new(|| println!("hi"))
function takes_long_type(f: Thunk) {
// --snip--
}
function returns_long_type() -> Thunk {
// --snip--
}
好多了!类型别名允许我们编写更简洁、更清晰的代码。类型别名通常也与 Result<T, E> 一起使用,以减少重复。考虑一下标准库中的 std::io 模块。I/O 操作通常返回 Result<T, E> 来处理操作失败的情况。std::io 定义了 Error 结构体,表示所有可能的 I/O 错误。std::io 中的许多函数返回 Result<T, E>,其中 E 是 std::io::Error,例如 Write trait 中的这些函数:
use std::io::Error
use std::fmt
trait Write {
function write(self: &mut Self, buf: &[u8]) -> Result<integer, Error>
function flush(self: &mut Self) -> Result<(), Error>
}
因为 std::io::Error 经常被使用,std::io 提供了这个类型别名 Result<T> 作为 Result<T, std::io::Error> 的简写!所以 std::io::Result<T> 只是 Result<T, E> 的别名,其中 E 填充了 std::io::Error。这最终意味着我们需要输入更少,并且我们可以拥有更一致的接口。因为它在 std::io 模块中,可用的类型别名是 std::io::Result<T>,这正是我们想要的!
这就是类型别名!它们并不复杂,但很方便。
高级函数和闭包
接下来,我们将探索一些与函数和闭包相关的高级特性:函数指针以及返回闭包。
函数指针
我们已经讨论了如何将闭包传递给函数;你也可以将普通函数传递给函数!当你想传递一个已经定义的函数而不是定义一个新的闭包时,这很有用。函数强制转换为类型 fn(带有小写的 f),不要与 Fn 闭包 trait 混淆。fn 被称为函数指针(function pointer)。使用函数指针的语法与使用闭包作为函数参数的语法类似:
function add_one(x: integer) -> integer {
x + 1
}
function do_twice(f: function(integer) -> integer, arg: integer) -> integer {
f(arg) + f(arg)
}
function main() {
let answer = do_twice(add_one, 5)
println("答案是: {}", answer)
}
这会打印出 答案是: 12。我们指定 do_twice 的参数 f 是一个 fn,它接受一个 integer 类型的参数并返回一个 integer。然后我们可以在 do_twice 的主体中调用 f。在 main 中,我们可以将函数名 add_one 作为第一个参数传递给 do_twice。
与闭包不同,fn 是一个类型而不是一个 trait,所以我们直接将 fn 指定为参数类型,而不是声明一个泛型类型参数,并将 Fn 作为 trait 约束之一。
函数指针实现了所有三个闭包 trait(Fn、FnMut 和 FnOnce),这意味着你总是可以将函数指针作为参数传递给期望闭包的函数。最好编写使用泛型类型和闭包 trait 之一的函数,这样你的函数就可以接受函数或闭包。
一个你只想接受 fn 而不接受闭包的例子是与不需要闭包的外部代码接口时:C 函数可以接受函数作为参数,但 C 没有闭包。
让我们看一个使用 Option 的 map 方法的例子,你可以选择传递闭包或命名函数:
let list_of_numbers = [1, 2, 3]
let list_of_strings: List<String> = list_of_numbers
.iter()
.map(integer::to_string)
.collect()
在这里,我们使用 integer::to_string,它是我们之前见过的 to_string 函数,通过使用完全限定语法。我们也可以在这里使用闭包,如下所示:
let list_of_numbers = [1, 2, 3]
let list_of_strings: List<String> = list_of_numbers
.iter()
.map(|i| i.to_string())
.collect()
无论哪种方式都有效,所以选择你喜欢的风格,或者在代码中已经有很多闭包的情况下使用函数指针来避免添加更多闭包语法。
好的,这就是函数指针!让我们继续讨论返回闭包!
返回闭包
闭包由 trait 表示,这意味着你不能直接返回闭包。在大多数情况下,当你想返回一个 trait 时,你可以使用实现该 trait 的具体类型代替函数的返回值。但是你不能对闭包这样做,因为它们没有可返回的具体类型;例如,你不允许使用 Fn trait 作为返回类型;编译器会抱怨:
// 这不会编译!
function returns_closure() -> Fn(integer) -> integer {
|x| x + 1
}
编译器给我们的错误是:
error: the `Fn` trait cannot be made into an object
我们在第 17 章中讨论了这个问题!我们需要使用 trait 对象。下面是我们如何重写返回闭包的函数:
function returns_closure() -> Box<Fn(integer) -> integer> {
Box::new(|x| x + 1)
}
这个代码编译得很好!我们使用 trait 对象 Box<Fn(integer) -> integer> 作为返回类型。我们创建了一个闭包并将其装箱,然后返回它。
好的,这就是返回闭包!这有点复杂,但非常有用。
宏
我们在本书中已经使用过宏,比如 println!,但我们还没有完全探讨宏是什么以及它们是如何工作的。宏(Macro)指的是 X 语言中的一系列功能:声明宏(declarative macros)与 macro! 和三种程序宏(procedural macros):
- 自定义
#[derive]宏,用于在结构体和枚举上使用derive属性指定代码 - 类属性宏,用于定义可用于任意项目的自定义属性
- 类函数宏,看起来像函数调用,但对作为其参数指定的标记进行操作
让我们按顺序讨论每一个,但首先,让我们看看为什么我们需要宏,而我们已经有了函数。
宏和函数的区别
从根本上说,宏是一种编写其他代码的代码方式,这被称为元编程(metaprogramming)。在附录 C 中,我们探讨了 derive 属性,它会自动为你生成各种 trait 的实现。我们在本书中也使用了 println! 和 format!。所有这些宏都通过展开(expand)来生成比你手动编写的代码更多的代码。
元编程对于减少你必须编写和维护的代码量很有用,这也是函数的作用之一。然而,宏有一些函数没有的额外能力。
函数签名必须声明它们有多少个参数以及这些参数的类型。另一方面,宏可以接受可变数量的参数:我们可以用一个参数调用 println!("hello") 或用两个参数调用 println!("hello {}", name)!此外,宏可以在编译时展开,因此宏可以,例如,在编译时实现一个类型的 trait。函数不能这样做,因为它们在运行时被调用,而 trait 需要在编译时实现。
宏的一个缺点是,与函数定义相比,宏定义更难阅读,因为你正在编写编写 X 语言代码的 X 语言代码。由于这种间接性,宏定义可能比函数定义更难阅读、理解和维护。
宏和函数之间的另一个重要区别是,你必须在调用宏之前定义宏或将它们带入作用域,而你可以在任何地方定义函数并在任何地方调用它们。让我们开始探索声明宏!
声明宏与 macro! 用于通用元编程
X 语言中最常用的宏形式是声明宏(declarative macros)。它们有时也被称为“示例宏“(macros by example)、macro_rules! 或简称为“macros“。在它们的核心,声明宏允许你编写类似于 match 表达式的东西。正如我们在第 6 章中讨论的,match 表达式是控制流结构,它接受一个值,将该值与模式进行比较,然后运行与匹配模式相关联的代码。宏也将一个值与具有相关代码的模式进行比较:在这种情况下,该值是字面 X 语言源代码,模式与源代码的结构进行比较,并且与每个模式相关联的代码在匹配时替换传递给宏的代码。这一切都发生在编译时!
要定义宏,你使用 macro_rules!。让我们看看 vec! 宏是如何定义的,作为一个例子。第 8 章讨论了我们如何使用 vec! 宏来创建一个包含特定值的新列表。例如,下面的宏创建一个包含三个整数的新列表:
let v: List<integer> = vec![1, 2, 3]
我们可以使用这个宏来创建一个包含任意数量任意类型值的列表。我们可以使用 macro_rules! 来定义 vec! 宏。让我们看看 vec! 的简化定义是什么样子的。示例显示了 vec! 宏的稍微简化的定义。
macro vec {
($($x:expr),*) => {
{
let mut temp_vec = List::new()
$(
temp_vec = temp_vec + [$x]
)*
temp_vec
}
}
}
注意:vec! 的实际标准库定义包括预分配正确数量内存的代码,作为优化。我们没有在这里包含该代码,以保持示例简单。
macro vec! 声明开始了宏定义。然后我们有一组大括号,表示宏定义的主体。vec! 宏的结构类似于 match 表达式的结构。我们有一个臂,其中包含模式 ($($x:expr),*),后跟 => 和与该模式相关联的代码块。如果模式匹配,这个代码块将被执行。鉴于这是这个宏中唯一的模式,只有一种有效的匹配方式;任何其他模式都会导致错误。更复杂的宏会有多个臂。
宏模式中的语法与 match 模式中的语法不同,因为我们正在匹配 X 语言代码结构而不是值。让我们逐步了解示例中模式的部分。对于完整的宏语法,请参阅 X 语言参考。
首先,一组括号包围了整个模式。我们在 $() 内部使用了 $x:expr,它匹配任何 X 语言表达式,并将该表达式绑定到元变量 $x。$() 后面的逗号表示字面逗号字符可以可选地出现在匹配 $() 中的代码的代码之后。* 指定模式匹配零次或多次前面的任何内容。
当我们用 vec![1, 2, 3] 调用这个宏时,$x 模式与三个表达式 1、2 和 3 匹配了三次。
现在让我们看看与这个臂相关联的代码块中的模式:temp_vec 中 $()* 内的 temp_vec = temp_vec + [$x] 部分为每次匹配零次或多次的部分在展开时生成;每次生成与该迭代匹配的 $x。当我们用 vec![1, 2, 3] 调用这个宏时,替换此宏的代码变成了这个:
{
let mut temp_vec = List::new()
temp_vec = temp_vec + [1]
temp_vec = temp_vec + [2]
temp_vec = temp_vec + [3]
temp_vec
}
我们已经添加了一个简单的宏,它可以用任意数量的任意类型表达式创建一个列表。macro_rules! 有一些奇怪的边缘情况。在某个时候,X 语言可能会引入一种用于声明宏的第二个宏语法,它的工作方式类似但修复了这些边缘情况。届时,macro_rules! 将被有效地弃用。考虑到这一点,以及大多数 X 语言程序员使用宏而不是编写宏这一事实,我们不会在这个主题上花更多时间。有关更多信息,请参阅文档或其他来源,如《X 语言宏小手册》。
好的,现在我们已经了解了声明宏,让我们继续讨论三种程序宏!
X 语言最佳实践
本章节提供 X 语言的最佳实践指南,涵盖代码风格、性能优化、测试策略和项目组织等方面。遵循这些实践可以帮助你编写更清晰、更高效、更可维护的 X 语言代码。
1. 代码风格
X 语言的设计理念是「可读性第一」,因此代码风格应优先考虑可读性和一致性。
1.1 命名约定
-
变量和函数:使用
snake_case(小写字母加下划线)let user_name: string = "Alice" function calculate_total_price(items: List<Product>) -> float { ... } -
类型:使用
PascalCase(首字母大写)record User { name: string age: integer } enum Result<T, E> { Ok(T) Err(E) } -
常量:使用
SCREAMING_SNAKE_CASE(全大写加下划线)const MAX_CONNECTIONS: integer = 100 const DEFAULT_TIMEOUT: float = 5.0 -
模块:使用
snake_case,与文件系统路径保持一致// 文件: src/utils/http_client.x module utils.http_client
1.2 格式与缩进
-
缩进:使用 4 个空格(而非制表符)
-
行宽:建议不超过 100 个字符
-
花括号:使用 K&R 风格(与代码同一行)
function process_data(data: List<integer>) { for item in data { if item > 0 { println(item) } } } -
空行:
- 在函数定义之间使用空行
- 在逻辑块之间使用空行
- 保持文件顶部和底部的空行
1.3 注释
-
单行注释:使用
//进行简短注释// 计算用户年龄 let age: integer = current_year - birth_year -
文档注释:使用
///为函数、类型和模块添加文档/// 计算两个数的和 /// /// # 参数 /// - `a`: 第一个加数 /// - `b`: 第二个加数 /// /// # 返回值 /// 两数之和 function add(a: integer, b: integer) -> integer { a + b } -
注释风格:
- 注释应使用完整句子
- 避免冗余注释(代码本身已清晰表达的内容)
- 注释应解释「为什么」而不是「是什么」
1.4 代码组织
- 函数长度:单个函数不应过长(建议不超过 50 行)
- 函数职责:每个函数应只做一件事
- 代码分组:相关代码应放在一起
- 导入语句:按模块层次排序,使用空白行分隔不同来源的导入
import std.io import std.collections import myapp.models import myapp.utils
1.5 表达式与语句
-
优先使用表达式:X 是表达式导向的语言,优先使用表达式而非语句
// 推荐 let result = if condition { value1 } else { value2 } // 不推荐 let result if condition { result = value1 } else { result = value2 } -
管道运算符:使用
|>提高代码可读性// 推荐 data |> filter(|x| x > 0) |> map(|x| x * 2) |> sum() // 不推荐 sum(map(filter(data, |x| x > 0), |x| x * 2)) -
模式匹配:充分利用模式匹配的威力
match result { Ok(value) => println("成功: {value}"), Err(error) => println("错误: {error}") }
2. 性能优化
X 语言通过 Perceus 内存管理和多后端架构提供了良好的性能基础,但仍有一些优化策略可以进一步提升代码性能。
2.1 内存管理优化
-
默认不可变:优先使用不可变绑定(
let),这有助于 Perceus 的重用分析// 推荐 let immutable_value = calculate_value() // 仅在必要时使用可变绑定 let mutable counter = 0 -
避免不必要的复制:当处理大型数据结构时,考虑使用引用类型
// 对于大型数据,使用引用类型避免复制 let large_data: List<integer> = generate_large_list() process_data(large_data) // 传递引用,避免复制 -
理解 Perceus 优化:Perceus 会在编译时分析引用计数,当引用计数为 1 时会进行原地更新
// Perceus 会将此优化为原地更新 let updated_list = old_list |> push(new_item)
2.2 算法与数据结构
-
选择合适的数据结构:
- 频繁查找:使用
HashMap - 有序数据:使用
SortedList - 唯一性要求:使用
Set
- 频繁查找:使用
-
算法复杂度:了解常见算法的时间复杂度,选择合适的算法
// 避免 O(n²) 复杂度的嵌套循环 // 考虑使用更高效的算法或数据结构 -
惰性计算:对于大型数据集,使用惰性计算避免一次性加载所有数据
// 使用惰性迭代器 let lazy_items = large_list |> filter(|x| x > 0) |> map(|x| x * 2) // 只有在需要时才会计算
2.3 并发优化
-
选择合适的并发模型:
- 轻量级任务:使用协程
- 消息传递:使用 Actor 模型
- I/O 密集型:使用 async/await
-
避免共享状态:优先使用消息传递而非共享内存
// 推荐:消息传递 actor Counter { let count = 0 receive(Increment) { count += 1 } receive(GetCount) { reply(count) } } -
合理使用锁:如果必须使用共享状态,最小化锁的范围
// 只在必要时加锁 lock(shared_resource) { // 最小化临界区 update_shared_data() }
2.4 编译优化
-
使用适当的后端:根据目标平台选择合适的编译后端
- 系统编程:C 或 LLVM 后端
- Web:JavaScript 后端
- Java 生态:JVM 后端
- .NET 生态:.NET 后端
-
启用优化:在发布构建时启用优化
x build --release -
分析性能瓶颈:使用性能分析工具找出瓶颈
x bench
3. 测试策略
X 语言提供了强大的测试工具和框架,合理的测试策略可以确保代码质量和可靠性。
3.1 测试类型
- 单元测试:测试单个函数或模块
- 集成测试:测试多个模块的交互
- 端到端测试:测试整个应用的流程
- 性能测试:测试代码的性能特性
3.2 测试组织
-
测试文件:将测试文件放在与被测试代码相同的目录中,使用
_test.x后缀src/ ├── utils.x └── utils_test.x -
测试模块:在测试文件中使用
test模块module utils_test import utils import std.test test "add function" { assert_eq(utils.add(2, 3), 5) } -
测试分组:使用描述性的测试名称,按功能分组
test "math operations - addition" { // 测试加法 } test "math operations - subtraction" { // 测试减法 }
3.3 测试实践
-
测试覆盖率:目标是 80% 以上的代码覆盖率
-
边界情况:测试边界条件和异常情况
test "divide by zero" { let result = divide(10, 0) assert(result is Err) } -
测试隔离:确保测试之间相互独立
test "user creation" { // 每次测试创建新的测试环境 let test_db = setup_test_database() // 测试代码 teardown_test_database(test_db) } -
属性测试:使用属性测试生成随机输入,发现边缘情况
test "sort is idempotent" { property { (list: List<integer>) in let sorted = sort(list) assert_eq(sort(sorted), sorted) } }
3.4 测试工具
-
运行测试:
# 运行所有测试 x test # 运行特定测试 x test test_name # 运行特定文件的测试 x test path/to/test_file.x -
测试覆盖率:
x test --coverage -
性能测试:
x bench
4. 项目组织
良好的项目组织可以提高代码的可维护性和可扩展性。
4.1 目录结构
-
标准项目结构:
project_name/ ├── x.toml # 项目配置 ├── x.lock # 依赖锁文件 ├── src/ # 源代码 │ ├── main.x # 主入口 │ ├── lib.x # 库入口(如果是库项目) │ ├── models/ # 数据模型 │ ├── utils/ # 工具函数 │ └── api/ # API 接口 ├── tests/ # 集成测试 ├── examples/ # 示例代码 └── docs/ # 文档 -
模块组织:
- 按功能组织模块
- 每个模块应有明确的职责
- 使用
module声明与文件路径一致的模块结构
4.2 依赖管理
-
依赖声明:在
x.toml中声明依赖[dependencies] serde = "1.0" http = "0.5" -
版本控制:
- 使用语义化版本号
- 锁定依赖版本以确保构建可重现
-
依赖分组:
[dependencies] # 运行时依赖 [dev-dependencies] # 开发时依赖(测试、基准测试等) [build-dependencies] # 构建时依赖
4.3 构建与部署
-
构建配置:
[package] name = "my-project" version = "0.1.0" authors = ["Your Name <your.email@example.com>"] [build] target = "c" # 目标后端 optimization = "speed" # 优化级别 -
多目标构建:
# 构建多个目标 x build --target c x build --target js x build --target jvm -
持续集成:
- 设置 CI/CD 管道
- 自动运行测试和构建
- 自动部署到目标环境
4.4 文档
-
项目文档:
README.md:项目概述、安装说明、使用示例CHANGELOG.md:版本变更记录CONTRIBUTING.md:贡献指南
-
API 文档:
- 使用文档注释(
///) - 生成 API 文档
x doc - 使用文档注释(
-
用户指南:
- 教程和示例
- 常见问题解答
- 最佳实践指南
5. 总结
遵循这些最佳实践可以帮助你编写高质量的 X 语言代码:
- 代码风格:优先考虑可读性,保持一致性
- 性能优化:了解并利用 X 语言的内存管理和优化特性
- 测试策略:编写全面的测试,确保代码质量
- 项目组织:合理组织代码结构,便于维护和扩展
记住,最佳实践不是一成不变的规则,而是根据具体项目和团队情况可以调整的指导原则。最重要的是保持代码的可读性和可维护性,让代码能够清晰地表达其意图。
Happy coding with X!
附录 A - 关键字
以下列表包含 X 语言中保留的关键字,因此它们不能用作标识符(如变量名、函数名等),除非在原始标识符形式中。
本附录基于 X 语言的官方设定文件(x-keywords.md 和 README.md)。
关键字速览
X 语言的关键字设计遵循以下原则:
- 使用完整英文单词(
function、not、and、or),避免缩写 - 优先选用“行业共识“的单词(如
if/else、class、async/await) - 尽量避免符号化操作符,用自然语言单词提高可读性
- 关键字语义“顾名思义“,不需要记忆额外规则
当前使用的关键字
以下关键字具有当前描述的功能。
1. 变量与绑定关键字
| 关键字 | 说明 |
|---|---|
let | 声明不可变变量绑定(默认不可变) |
mutable | 声明可变绑定(与 let 配合使用,如 let mutable) |
const | 声明编译期常量(值在编译期确定) |
2. 类型与抽象关键字
| 关键字 | 说明 |
|---|---|
type | 声明类型别名、记录类型或代数数据类型(ADT) |
trait | 声明 trait(接口),定义可被类型实现的一组行为 |
3. 函数关键字
| 关键字 | 说明 |
|---|---|
function | 声明函数(使用完整单词,不使用缩写 fn 或 func) |
return | 从函数提前返回值 |
4. 控制流关键字
| 关键字 | 说明 |
|---|---|
if | 基于条件表达式的分支 |
else | if 控制流结构的回退分支 |
when | 条件表达式(类似三元运算符,但更可读) |
then | when 表达式的一部分 |
match | 模式匹配(穷尽匹配多个分支) |
where | 模式匹配的守卫条件,或声明式查询的过滤条件 |
while | 基于条件的循环 |
for | 遍历迭代器的循环 |
in | for 循环语法的一部分,用于指定范围 |
break | 退出循环 |
continue | 跳到循环的下一次迭代 |
5. 布尔逻辑关键字
| 关键字 | 说明 |
|---|---|
not | 逻辑非(替代符号 !) |
and | 逻辑与(替代符号 &&) |
or | 逻辑或(替代符号 ` |
true | 布尔字面量 true |
false | 布尔字面量 false |
6. 类型检查与转换关键字
| 关键字 | 说明 |
|---|---|
is | 类型检查(判断值是否属于某个类型) |
as | 类型转换(将值转换为另一个类型) |
7. 类与对象关键字
| 关键字 | 说明 |
|---|---|
class | 声明类 |
extends | 类继承 |
new | 创建新实例(构造函数) |
virtual | 虚方法声明(可被子类重写) |
override | 重写继承的方法 |
abstract | 抽象类或方法 |
extension | 扩展方法或属性(为现有类型添加功能) |
8. 模块系统关键字
| 关键字 | 说明 |
|---|---|
module | 声明模块 |
import | 从模块导入符号 |
export | 从模块导出符号 |
9. 选项和结果类型构造器
| 关键字 | 说明 |
|---|---|
Some | Option 枚举的变体,表示存在值 |
None | Option 枚举的变体,表示不存在值 |
Ok | Result 枚举的变体,表示成功 |
Err | Result 枚举的变体,表示错误 |
10. 异步与并发关键字
| 关键字 | 说明 |
|---|---|
async | 异步函数或块 |
await | 等待异步操作完成 |
together | 并发执行多个操作(等待所有完成) |
race | 竞态执行(取第一个完成的结果) |
go | 启动轻量级协程 |
actor | Actor 模型声明 |
11. 其他关键字
| 关键字 | 说明 |
|---|---|
_ | 通配符模式(忽略值或占位符) |
this | 方法接收器(当前实例) |
super | 父类或父模块 |
关键字和类型设计原则
X 语言的关键字和类型设计遵循以下原则(来自 x-keywords.md):
- 控制流关键字高度收敛:几乎所有语言都使用
if/else、for/while,X 直接沿用这些“行业标准“。 - 命名更偏自然语言:与大量使用缩写的语言(
def、fn、sub)相比,X 选择function、not/and/or、match/where等完整单词,使代码接近英文散文。 - 异步与模块关键字与现代生态对齐:
async/await、import/export、module与 C#、JavaScript、TypeScript 等现代语言保持一致,方便开发者迁移。 - 并发与 OOP 使用领域通用术语:
actor、trait、extension等直接采用在多门语言中已经广泛使用的专业术语,避免发明新的名词。
设计准则:代码应该像散文一样可读。关键字是语言的词汇表——每个词都必须是真正的英语单词,且含义精确。
原始标识符
原始标识符是一种语法,它允许你使用通常是关键字的词作为标识符。这对于与旧版代码的互操作性很有用,该代码是在某些词成为关键字之前编写的,或者与使用不是 X 语言关键字但另一种语言中的关键字的库进行交互。
原始标识符的语法使用反引号 ` 来转义关键字:
// `let` 是原始标识符,表示变量名 "let"
let `let` = 5
重要符号(非关键字)
虽然不是关键字,但以下符号在 X 语言中非常重要:
| 符号 | 说明 |
|---|---|
-> | 函数返回类型或 Lambda 表达式 |
=> | 模式匹配分支 |
| ` | >` |
.. | 左闭右开范围(不包含末尾) |
..= | 包含范围(包含末尾) |
?. | 可选链访问 |
?? | 默认值运算符 |
附录 B - 操作符与符号
本附录包含 X 语言语法中使用的所有操作符和符号的目录。
本附录基于 X 语言的宪法性文件(DESIGN_GOALS.md 和 README.md)。
操作符
表 B-1 包含按优先级从高到低排序的操作符。
表 B-1: 操作符
| 操作符 | 示例 | 名称 | 说明 | 优先级 |
|---|---|---|---|---|
:: | Module::name | 路径分隔符 | 命名空间或模块路径 | 1 |
. | value.method() | 方法调用 | 方法调用或字段访问 | 1 |
. | value.field | 字段访问 | 记录或结构体字段访问 | 1 |
() | func(args) | 函数调用 | 函数或方法调用 | 1 |
[] | list[index] | 索引 | 列表或数组索引 | 1 |
? | value? | 错误传播 | 传播 Result 或 Option 错误 | 2 |
- | -value | 一元负号 | 数字取反 | 3 |
not | not value | 逻辑非 | 布尔值取反 | 3 |
~ | ~value | 按位非 | 按位取反 | 3 |
* | value * value | 乘法 | 整数或浮点数乘法 | 4 |
/ | value / value | 除法 | 整数或浮点数除法 | 4 |
% | value % value | 取模 | 整数取模 | 4 |
+ | value + value | 加法 | 整数或浮点数加法 | 5 |
+ | string + string | 字符串连接 | 连接两个字符串 | 5 |
- | value - value | 减法 | 整数或浮点数减法 | 5 |
< | value < value | 小于 | 小于比较 | 6 |
> | value > value | 大于 | 大于比较 | 6 |
<= | value <= value | 小于等于 | 小于等于比较 | 6 |
>= | value >= value | 大于等于 | 大于等于比较 | 6 |
== | value == value | 等于 | 相等比较 | 7 |
!= | value != value | 不等于 | 不相等比较 | 7 |
and | value and value | 逻辑与 | 布尔逻辑与 | 8 |
or | value or value | 逻辑或 | 布尔逻辑或 | 9 |
= | name = value | 赋值 | 将值赋给变量 | 10 |
+= | name += value | 加法赋值 | 加法后赋值 | 10 |
-= | name -= value | 减法赋值 | 减法后赋值 | 10 |
*= | name *= value | 乘法赋值 | 乘法后赋值 | 10 |
/= | name /= value | 除法赋值 | 除法后赋值 | 10 |
%= | name %= value | 取模赋值 | 取模后赋值 | 10 |
^= | name ^= value | 按位异或赋值 | 按位异或后赋值 | 10 |
-> | -> Type | 返回类型箭头 | 函数返回类型 | 11 |
=> | pattern => expr | 胖箭头 | 模式匹配分支或闭包 | 11 |
| ` | >` | `value | > func` | 管道 |
.. | start..end | 范围 | 左闭右开范围 | 13 |
..= | start..=end | 包含范围 | 包含两端的范围 | 13 |
非操作符符号
表 B-2 包含不作为操作符出现但具有各种功能的所有符号。
表 B-2: 符号
| 符号 | 示例 | 名称 | 说明 |
|---|---|---|---|
// | // 注释 | 单行注释 | 注释掉直到行尾 |
/// | /// 文档注释 | 文档注释 | Markdown 文档注释 |
_ | _ | 通配符模式 | 忽略值或占位符 |
_ | let _ = x | 通配符绑定 | 显式忽略值 |
' | 'a' | 字符字面量 | 单个 Unicode 字符 |
" | "string" | 字符串字面量 | 字符串 |
` | `identifier` | 原始标识符 | 转义关键字 |
{} | { ... } | 块表达式 | 表达式块 |
{} | { x: 5 } | 记录字面量 | 记录或结构体实例化 |
() | () | Unit 字面量 | Unit 类型的唯一值 |
() | (expr) | 括号表达式 | 明确优先级 |
() | (Type, Type) | 元组类型 | 元组类型声明 |
() | (value, value) | 元组字面量 | 元组实例化 |
[] | [Type] | 列表类型简写 | List<Type> 的简写 |
[] | [value, value] | 列表字面量 | 列表实例化 |
: | name: Type | 类型注解 | 变量或参数的类型 |
: | key: value | 字段初始化 | 记录或结构体字段 |
, | a, b | 逗号 | 分隔项 |
; | expr; | 分号 | 语句终止符(可选) |
| ` | ` | `Variant1 | Variant2` |
| ` | ` | ` | param |
& | &Type | 引用/按位与 | 引用类型或按位与 |
@ | name @ pattern | 模式绑定 | 绑定到完整值的名称 |
# | #attribute | 属性 | 编译器属性或注解 |
符号和类型设计原则
X 语言的符号和类型设计遵循以下原则(来自 DESIGN_GOALS.md 和 x-keywords.md / x-types.md):
- 键盘上能直接打出来:不使用需要特殊输入法或 Unicode 查表才能输入的符号
- 看到就知道什么意思:每个符号的含义对大多数程序员来说应该是显而易见的
- 宁可用关键字也不用符号:当符号的含义不够清晰时,用英文单词代替
- 使用
not/and/or代替!/&&/|| - 使用完整单词
function代替缩写
- 使用
- 基础类型小写,引用类型大写:
- 值类型(小写):
integer、float、boolean、string、character - 引用类型(大写):
Integer、Float、Boolean、String、Character
- 值类型(小写):
- 固定大小整数类型使用完整短语:如
signed 8bit integer、unsigned 32bit integer,不使用i8/u32等缩写 - main 函数可选:可以像 Swift 一样直接在文件顶层编写代码,不需要
main函数 - 常量使用连字符(kebab-case):如
MAX-RETRY-COUNT,不使用下划线
设计准则:符号应该是“看一眼就懂“的。如果需要解释,就用英文单词代替。键盘上的每个符号都很宝贵——只用最常见、最直观的那些。
符号组合
一些符号组合在一起形成特殊语法:
| 符号组合 | 示例 | 名称 | 说明 |
|---|---|---|---|
when/is | when expr is { ... } | 模式匹配 | 完整的模式匹配表达式 |
function | function name() { ... } | 函数定义 | 函数声明语法 |
type | type Name = ... | 类型定义 | 类型声明语法 |
let | let name = value | 变量声明 | 变量绑定语法 |
let mutable | let mutable name = value | 可变变量 | 可变变量绑定语法 |
if/else | if cond { ... } else { ... } | 条件表达式 | 完整的 if-else 表达式 |
| ` | >` | `value | > func |
附录 C - 可派生的 Trait
在整个书中,我们使用了 derive 属性,它可以在类型上生成 trait 的默认实现。在本附录中,我们提供了所有可用于 derive 的所有标准库 trait 的参考。每个部分涵盖:
- 该 trait 提供了哪些操作符和方法
- derive 生成什么
- trait 做什么
- 为什么你可能想要或不想要实现该 trait
Debug
Debug trait 支持使用 {:?} 格式说明符进行调试格式化。
使用 derive 时,Debug trait 使你能够打印类型的实例以进行调试。
示例
[derive(Debug)]
type Point = {
x: integer,
y: integer
}
let p = { x: 3, y: 5 }
println("{:?}", p) // Point { x: 3, y: 5 }
手动实现
如果你想自定义调试输出,你可以手动实现 Debug trait 而不是使用 derive:
type Point = {
x: integer,
y: integer
}
impl Debug for Point {
function fmt(self: &Self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "({}, {})", self.x, self.y)
}
}
let p = { x: 3, y: 5 }
println("{:?}", p) // (3, 5)
何时使用
- 当你想要能够使用
println!("{:?}", value)打印你的类型进行调试时,使用derive(Debug)。几乎所有类型都应该派生这个 trait。
Display
Display trait 支持使用 {} 格式说明符进行用户友好的格式化。
注意:Display 不能使用 derive;你必须手动实现它。
示例
type Point = {
x: integer,
y: integer
}
impl Display for Point {
function fmt(self: &Self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "({}, {})", self.x, self.y)
}
}
let p = { x: 3, y: 5 }
println("{}", p) // (3, 5)
何时实现
当你想要能够使用 println!("{}", value) 以面向用户的方式打印你的类型时,实现 Display。
Clone
Clone trait 明确创建值的深拷贝。
使用 derive 时,Clone trait 使你能够克隆一个值。
示例
[derive(Clone)]
type Point = {
x: integer,
y: integer
}
let p1 = { x: 3, y: 5 }
let p2 = p1.clone()
println("p1 = {:?}, p2 = {:?}", p1, p2)
何时使用
- 当你想要能够显式复制你的类型的值时,使用
derive(Clone)。
Copy
Copy trait 允许值被隐式复制,而不是移动。
使用 derive 时,Copy trait 使你的类型可以被复制而不是移动。
示例
[derive(Copy, Clone)]
type Point = {
x: integer,
y: integer
}
let p1 = { x: 3, y: 5 }
let p2 = p1 // p1 被复制,而不是移动!
println("p1 仍然有效!")
何时使用
- 当你的类型简单且足够小,可以通过复制而不是移动时,使用
derive(Copy)。 - 注意:如果一个类型实现了
Copy,它也必须实现Clone。 - 通常,只有完全由
Copy类型组成的类型才可以是Copy。 - 拥有堆分配数据的类型(如
String或List)不应是Copy。
PartialEq 和 Eq
PartialEq trait 允许你使用 == 和 != 操作符比较值的相等性。
Eq trait 是一个标记 trait,表示对于类型的所有值,相等性是自反的(a == a)、对称的(a == b 意味着 b == a)和传递的(a == b 和 b == c 意味着 a == c)。
使用 derive 时,PartialEq trait 允许你比较你的类型的值相等或不相等。
示例
[derive(PartialEq, Eq)]
type Point = {
x: integer,
y: integer
}
let p1 = { x: 3, y: 5 }
let p2 = { x: 3, y: 5 }
let p3 = { x: 1, y: 2 }
println("p1 == p2: {}", p1 == p2) // true
println("p1 == p3: {}", p1 == p3) // false
何时使用
- 当你想要能够比较你的类型的值相等性时,使用
derive(PartialEq)。 - 当相等性对于你的类型是自反的、对称的和传递的时,也使用
derive(Eq)。 - 大多数具有相等性概念的类型都应该派生这些 trait。
PartialOrd 和 Ord
PartialOrd trait 允许你使用 <、>、<= 和 >= 比较符比较值的排序。
Ord trait 是一个标记 trait,表示对于类型的所有值,排序是完全的(对于任意两个值,a < b、a == b 或 a > b 中恰好有一个为真)。
使用 derive 时,PartialOrd trait 允许你比较你的类型的值的排序。
示例
[derive(PartialOrd, Ord, PartialEq, Eq)]
type Point = {
x: integer,
y: integer
}
let p1 = { x: 1, y: 2 }
let p2 = { x: 3, y: 4 }
println("p1 < p2: {}", p1 < p2) // true
println("p1 > p2: {}", p1 > p2) // false
何时使用
- 当你想要能够排序或比较你的类型的值的排序时,使用
derive(PartialOrd)。 - 当排序对于你的类型是完全的时,也使用
derive(Ord)。
Hash
Hash trait 允许你将类型的值哈希为整数。
使用 derive 时,Hash trait 允许你哈希你的类型的值。
示例
[derive(Hash, PartialEq, Eq)]
type Point = {
x: integer,
y: integer
}
let p = { x: 3, y: 5 }
let h = hash(&p)
println("p 的哈希: {}", h)
何时使用
- 当你想要能够将你的类型的值作为
Map中的键或Set中的值时,使用derive(Hash)。 - 如果派生
Hash,还必须同时派生PartialEq和Eq。
Default
Default trait 为类型提供默认值。
使用 derive 时,Default trait 为你的类型提供默认值。
示例
[derive(Default)]
type Point = {
x: integer,
y: integer
}
let p: Point = Default::default()
println("默认点: {:?}", p) // Point { x: 0, y: 0 }
何时使用
- 当你的类型有一个合理的默认值时,使用
derive(Default)。 - 例如,数值类型默认为 0,可选类型默认为
None,等等。
总结
这是所有标准库 trait 的总结,你可以使用 derive 属性派生它们:
- Debug - 调试格式化
- Display - 用户友好的格式化(不能派生,必须手动实现)
- Clone - 显式复制
- Copy - 隐式复制
- PartialEq - 相等性比较
- Eq - 完全相等性
- PartialOrd - 排序比较
- Ord - 完全排序
- Hash - 哈希值
- Default - 默认值
大多数这些 trait 都可以使用 derive 自动派生,但有些(如 Display)需要手动实现。
附录 D - 有用的开发工具
在本附录中,我们将讨论 X 语言官方项目提供的一些有用的开发工具,以及它们如何帮助你编写 X 语言代码。
自动格式化与 x fmt
x fmt 工具根据标准样式格式化你的代码。无论你喜欢什么样式,许多项目都选择使用 x fmt 来结束关于样式的争论:每个人都使用 X 语言官方工具格式化他们的代码。
要格式化任何 X 语言项目,请运行:
x fmt
此工具将以标准 X 语言样式重新格式化所有代码。许多程序员将 x fmt 作为其工作流程的一部分,以保持代码整洁。
修复代码与 x fix
x fix 工具会自动修复代码中的某些问题。
自动修复警告
如果你有一个会产生警告的代码,x fix 可能能够自动修复它:
x fix
版本过渡
x fix 还可以帮助你的代码过渡到新版本的 X 语言:
x fix --edition
代码分析与 x check
x check 工具快速检查你的代码是否有错误。
x check
这比完整的编译更快,并且对于快速检查错误很有用。
语言服务器与 LSP 支持
X 语言对 LSP(语言服务器协议)有支持,以支持 IDE 集成。
设置 LSP
许多 IDE 和编辑器支持 LSP。要使用 X 语言 LSP:
- 安装 LSP 服务器 - 如果单独提供的话,安装 X 语言 LSP 服务器
- 配置编辑器 - 配置你的编辑器使用它
- 享受! - 享受自动完成、跳转定义等功能
LSP 功能
X 语言 LSP 提供:
- 自动完成 - 代码的建议补全
- 跳转定义 - 跳转到定义某物的地方
- 查找引用 - 查找某物在何处使用
- 悬停信息 - 悬停时显示信息
- 实时诊断 - 实时显示错误和警告
- 重命名 - 安全地重命名事物
- 格式化 - 使用
x fmt格式化
调试支持
X 语言支持使用标准调试器进行调试。
GDB/LLDB
你可以使用 GDB 或 LLDB 调试 X 语言程序:
# 使用调试信息构建
x build --debug
# 使用 GDB 调试
gdb ./target/debug/my_program
# 或者使用 LLDB
lldb ./target/debug/my_program
IDE 集成
大多数 IDE 也内置了对调试的支持。
性能分析
基准测试
你可以使用内置的基准测试支持对代码进行基准测试:
benchmark my_benchmark {
// 你要测量的代码
}
然后运行:
x bench
性能分析工具
X 语言程序可以使用标准性能分析工具进行性能分析:
- perf - 在 Linux 上
- Instruments - 在 macOS 上
- VTune - 在 Windows 上
文档生成
x doc
x doc 工具为你的代码生成 HTML 文档:
x doc --open
这将生成文档并在 Web 浏览器中打开它。
文档注释
使用文档注释记录你的代码:
/// 将给定的数字加一。
///
/// # 示例
///
/// ```
/// let five = 5;
/// assert_eq!(6, add_one(five));
/// ```
function add_one(x: integer) -> integer {
x + 1
}
其他有用的工具
x clippy(如果可用)
如果可用,x clippy 是一个提供额外警告和建议的 lint 工具。
x test
我们已经讨论了测试,但提醒一下,x test 运行你的测试:
x test
x expand(如果可用)
x expand 展开宏以查看它们生成什么代码。
IDE 支持
流行的 IDE
X 语言支持多种 IDE:
- VS Code - 带有 X 语言扩展
- IntelliJ IDEA - 带有 X 语言插件
- Vim/Neovim - 带有 X 语言支持
- Emacs - 带有 X 语言模式
- Sublime Text - 带有 X 语言包
功能
大多数现代 X 语言 IDE 支持:
- 语法高亮
- 自动完成
- 跳转定义
- 查找引用
- 内联错误
- 重构工具
- 调试支持
- 集成测试运行器
总结
有用的 X 语言开发工具:
- x fmt - 自动格式化你的代码
- x fix - 自动修复某些问题
- x check - 快速检查错误
- LSP 支持 - IDE 集成
- 调试器 - GDB、LLDB 和 IDE 支持
- x bench - 基准测试
- x doc - 文档生成
- IDE - VS Code、IntelliJ、Vim 等
这些工具使编写 X 语言代码变得更加容易和愉快!
附录 E - 版本说明
X 语言使用 editions(版本)系统来管理语言随时间的演变。版本允许 X 语言以向后兼容的方式发展,同时仍然引入新功能和改进。
在本附录中,我们将讨论 X 语言的 editions 系统、它们是什么、它们如何工作,以及每个 edition 中包含什么。
什么是 Editions?
edition 是 X 语言语言的一组功能的版本。新的 edition 可以引入不兼容的更改,例如:
- 新的语法
- 新的关键字
- 新的警告
- 默认行为的更改
但是,edition 与版本号不同。一个编译器可以支持多个 edition,你可以为每个包选择使用哪个 edition。
为什么使用 Editions?
Editions 很重要有几个原因:
- 演进 - 允许语言演进而不破坏现有代码
- 采用 - 允许项目可以按照自己的节奏采用新功能
- 兼容性 - 旧代码继续工作而无需修改
- 清晰度 - 清楚哪些功能属于一起
指定 Edition
你在包的 x.toml 中指定 edition:
[package]
name = "my_package"
version = "0.1.0"
edition = "2024"
Edition 列表
让我们看看假设的 X 语言 editions。
2024 Edition
2024 edition 是 X 语言语言的第一个稳定 edition。
包含的内容
- 基本语法和类型
- 所有权和借用
- 结构体和枚举
- 模式匹配
- 错误处理(Option 和 Result)
- 泛型和 traits
- 迭代器和闭包
- 异步/等待
- 等等。
示例
// 2024 edition 代码
function main() {
let x = 5;
println("x 是 {}", x);
}
未来 Editions
未来的 editions 可能包括:
- 新语法糖
- 改进的类型推断
- 新的标准库功能
- 性能改进
- 等等。
过渡指南
当新 edition 发布时,你可以按照自己的节奏过渡你的项目。
检查兼容性
大多数时候,编译器可以帮助你检查与新 edition 的兼容性:
# 检查你的代码与新 edition 的兼容性
x fix --edition 2027
自动修复
许多更改可以自动应用:
# 自动修复代码以使用新 edition
x fix --edition 2027
预览功能
在新 edition 发布之前,你可以使用预览标志预览新功能:
[package]
name = "my_package"
edition = "2024"
[features]
preview = ["async_fn_traits", "let_chains"]
然后在你的代码中:
// 这启用预览功能
#![feature(async_fn_traits)]
async function example() {
// ...
}
最佳实践
关于 editions 的一些最佳实践:
- 使用最新 edition - 对于新项目,使用最新的稳定 edition
- 逐渐过渡 - 对于现有项目,当你准备好时过渡
- 使用预览功能 - 小心使用,用于测试新功能
- 阅读发布说明 - 每个 edition 都有包含更改的发布说明
编译器支持
X 语言编译器支持多个 editions。你可以使用 --edition 标志为单个文件指定 edition:
# 使用特定 edition 编译
x compile --edition 2024 file.x
总结
X 语言的 editions 系统:
- 允许语言以向后兼容的方式演进
- 你可以为每个包选择 edition
- 新 editions 可能引入不兼容的更改
- 你可以使用预览功能尝试新功能
- 过渡工具可以帮助更新代码
Editions 是保持 X 语言语言现代化同时保持与现有代码兼容性的重要部分!
附录 F - 翻译
《The X Programming Language》这本书已经被翻译成多种语言。在本附录中,我们将提供可用翻译的列表,以及如何帮助翻译这本书的信息。
官方翻译
简体中文
- 翻译者:X 语言社区
- 状态:完整
- URL:https://doc.x-lang.org/stable/book/zh-CN/
(你正在读这个!)
其他语言
如果其他语言的官方翻译可用,它们将列在这里。
社区翻译
社区还提供了许多社区制作的翻译。这些不是官方的,但可能仍然有用。
如何帮助翻译
如果你想帮助将这本书翻译成另一种语言,感谢你的帮助!
入门
- 检查是否已经有翻译 - 在开始之前,检查是否已经有人在做翻译
- 加入翻译团队 - 联系现有翻译者
- 遵循翻译指南 - 遵循翻译风格指南
- 提交拉取请求 - 完成后提交你的工作
翻译指南
翻译时,请遵循这些准则:
-
保持技术术语一致 - 技术术语应该保持一致
- `所有权“ - 所有权
- “借用” - 借用
- “trait” - trait(或特性)
- “宏” - 宏
- 等等。
-
保持代码示例不变 - 代码示例应该保持不变,除非有特定于语言的示例。
-
清晰准确 - 翻译应该清晰准确。
-
保持语气 - 保持原书的语气。
翻译平台
翻译通常托管在:
- GitHub - 大多数翻译在 GitHub 上
- Crowdin - 一些使用 Crowdin 等翻译平台
- Transifex - 一些使用 Transifex
术语表
这里有一些常见术语的翻译:
| 英文 | 中文 |
|---|---|
| Ownership | 所有权 |
| Borrowing | 借用 |
| Trait | Trait / 特性 |
| Struct | 结构体 |
| Enum | 枚举 |
| Pattern matching | 模式匹配 |
| Closure | 闭包 |
| Iterator | 迭代器 |
| Lifetime | 生命周期 |
| Crate | 包 / Crate |
| Module | 模块 |
| Package | 包 |
| Macro | 宏 |
| Async/Await | 异步/等待 |
| Result | Result / 结果 |
| Option | Option / 可选 |
| Panic | Panic / 崩溃 |
| Reference | 引用 |
| Pointer | 指针 |
| Stack | 栈 |
| Heap | 堆 |
| Trait object | Trait 对象 |
| Generic | 泛型 |
| Associated type | 关联类型 |
| Associated function | 关联函数 |
| Method | 方法 |
报告翻译问题
如果你在翻译中发现问题:
- **在翻译存储库中提交问题
- 如果你能,提交修复
- 联系翻译者
致谢
感谢所有为翻译这本书的人!翻译是一项很大的工作,社区感谢你的努力!
总结
这本书有:
- 官方翻译在几种语言
- 社区翻译在更多语言中
- 你可以帮助翻译!
- 遵循翻译指南
- 保持技术术语一致
翻译帮助使 X 语言语言对世界各地的更多人可用!
附录 G - X 是如何开发的?
你有没有想过 X 语言语言本身是如何开发的?在本附录中,我们将看看 X 语言项目是如何组织的、人们如何贡献,以及决策是如何做出的。
X 语言项目
X 语言项目是一个开源项目。这意味着任何人都可以为其做出贡献。
治理
X 语言项目有一个治理结构,包括:
- 核心团队 - 指导项目的整体方向
- 语言团队 - 决定语言更改
- 库团队 - 决定标准库更改
- 编译器团队 - 致力于编译器
- 工具团队 - 致力于开发工具
- ** moderation 团队** - 维护行为准则
RFC 流程
语言的重大更改经过 RFC(征求意见)流程。
编写 RFC
对于重大更改,有人编写 RFC:
- 想法 - 在内部讨论想法
- RFC - 编写 RFC 文档
- 反馈 - 社区提供反馈
- 修订 - 根据反馈进行修订
- 决策 - 团队做出决策
- 实现 - 如果被接受,实现它
RFC 文档
RFC 文档包括:
- 摘要 - 更改的简要摘要
- 动机 - 为什么需要此更改
- 详细设计 - 详细设计
- 缺点 - 缺点
- 替代方案 - 考虑的替代方案
- 未解决的问题 - 未解决的问题
贡献
任何人都可以为 X 语言项目。
如何贡献
有很多贡献方式:
- 代码 - 编写代码
- 文档 - 编写文档
- 测试 - 编写测试
- 错误报告 - 报告错误
- 审查 - 审查拉取请求
- 社区 - 帮助社区
入门
如果你是新手,想要贡献:
- **找到“适合新手“问题 - 寻找标有“E-easy“或“good first issue“的问题
- **阅读贡献指南 - 阅读项目有贡献指南
- **加入交流 - 加入交流渠道
- 从小处开始 - 从小的更改开始
- **提问 - 提问!
贡献者许可证
贡献通常是根据项目使用 Apache 2.0 和 MIT 许可证的双重许可。
发布流程
X 语言有发布流程。
发布列车
X 语言在发布列车上:
- Nightly - 每晚构建,最新功能
- Beta - 每六周,测试版
- Stable - 每六周,稳定版
发布版本
Stable 版本每六周发布一次。这意味着你可以期待定期、可预测地获得新功能。
向后兼容性
X 语言非常重视向后兼容性。Stable 版本中的代码通常向后兼容旧代码继续在新编译器上编译。
代码库
X 语言代码库托管在 GitHub 上。
主要组件
主要组件是:
- 编译器 - X 语言编译器
- 标准库 - 标准库
- 文档 - 这本书和其他文档
- Cargo - 包管理器(如果适用)
- 工具 - 其他工具
构建系统
构建系统使用 Cargo(如果这是 Rust 项目)或其他构建系统。
社区
X 语言有一个友好和欢迎的社区。
交流渠道
有几种交流方式:
- GitHub - 问题和拉取请求
- Discord/Slack - 实时聊天
- 论坛 - 异步讨论
- 邮件列表 - 公告
行为准则
X 语言项目有行为准则,旨在营造一个安全和欢迎的环境。
致谢
X 语言之所以成为今天的样子,要感谢所有为它做出贡献的每一个人。感谢所有贡献者!
总结
X 语言项目:
- 是开源的
- 有治理结构
- 使用 RFC 流程进行重大更改
- 欢迎贡献
- 每六周发布一次
- 重视向后兼容性
- 有友好和欢迎的社区
如果你有兴趣,任何人都可以参与!
X 编程语言文档 - 发布说明
文档版本
版本: 1.0.0 发布日期: 2026-03-07
文档结构
X 编程语言文档包含以下主要部分:
1. 开始使用
- 前言: 介绍 X 语言的设计理念和目标
- 介绍: X 语言的基本介绍
- 安装 X: 如何安装 X 语言编译器和工具链
- Hello, World!: 第一个 X 语言程序
- 编写一个简单的程序: 简单的猜数字游戏示例
2. 常见编程概念
- 变量与可变性: 变量声明和可变性
- 数据类型: X 语言的数据类型系统
- 函数: 函数定义和使用
- 注释: 代码注释的使用
- 控制流: 条件语句和循环
3. Perceus 内存管理
- 理解 Perceus: Perceus 算法的基本原理
- Perceus 基础: Perceus 的基本使用
- Perceus 高级特性: Perceus 的高级用法
4. 结构体、枚举与模式匹配
- 结构体与记录: 结构体的定义和使用
- 记录类型: 记录类型的特性
- 枚举与模式匹配: 枚举类型和模式匹配
5. 包与模块
- 包和 Crate: 包的结构和管理
- 模块与作用域: 模块系统和作用域规则
- 路径与导入: 路径解析和导入机制
6. 常见集合
- 列表: 列表的操作和使用
- 字符串: 字符串的处理
- Dictionary/Map: 映射的使用
- Set: 集合的操作
7. 错误处理
- 错误处理概览: 错误处理的基本概念
- panic!: 程序崩溃机制
- Result: 错误返回类型
- Option: 可选值类型
8. 泛型、Trait 与生命周期
- 泛型数据类型: 泛型的使用
- Trait:定义共享行为: Trait 系统
- 生命周期: 生命周期标注
9. 类与面向对象
- 面向对象编程: X 语言的面向对象特性
- 类与对象: 类的定义和使用
- 继承: 继承机制
- 抽象类与接口: 抽象类和接口的使用
10. 函数式特性
- 闭包: 闭包的定义和使用
- 迭代器: 迭代器的操作
- 管道操作符: 管道操作符的使用
11. 测试与标准库
- 如何编写测试: 测试的编写方法
- 测试组织: 测试的组织方式
- 标准库概览: 标准库的模块和功能
- Prelude: 自动导入的模块
- 常用模块: 常用的标准库模块
- I/O 项目: I/O 操作的示例
12. 高级特性
- 效果系统: 效果系统的使用
- 异步编程: 异步编程模型
- 元编程: 元编程的特性
- X 工具链进阶: 工具链的高级使用
- 智能指针: 智能指针的使用
- 无畏并发: 并发编程
- 模式与模式匹配: 高级模式匹配
- 不安全 X: 不安全操作
- 高级 Trait: Trait 的高级用法
- 高级类型: 高级类型系统特性
- 高级函数和闭包: 函数和闭包的高级用法
- 宏: 宏的定义和使用
13. 最佳实践
- X 语言最佳实践: 代码风格、性能优化等建议
14. 附录
- A - 关键字: X 语言的关键字
- B - 操作符与符号: 操作符的优先级和用法
- C - 可派生的 Trait: 可自动派生的 Trait
- D - 有用的开发工具: 开发工具的使用
- E - 版本说明: 语言版本的变更
- F - 翻译: 文档的翻译情况
- G - X 是如何开发的?: 语言的开发过程
如何使用本文档
- 在线阅读: 可以通过浏览器直接打开
index.html文件来阅读文档 - 本地构建: 可以使用 Jekyll 构建工具来生成完整的文档网站
cd docs jekyll build - 导航: 使用左侧的导航栏浏览不同的章节
- 搜索: 可以使用浏览器的搜索功能查找特定内容
文档特点
- 全面性: 覆盖了 X 语言的所有核心特性
- 准确性: 内容与语言规格保持一致
- 实用性: 提供了丰富的代码示例和最佳实践
- 清晰性: 结构清晰,易于理解
- 可访问性: 支持不同设备和屏幕尺寸
贡献指南
如果您发现文档中的错误或有改进建议,请通过以下方式贡献:
- 提交 Issue 描述问题
- 提交 Pull Request 修复问题
- 提供翻译或其他改进
许可协议
本文档采用多重许可协议发布:
- MIT License
- Apache License 2.0
- BSD 3-Clause License
您可以选择任一许可证来使用、修改和分发本文档。
X 语言编译器架构设计
支持 AOT 和多平台 JIT 的统一编译器架构
目录
总体架构
flowchart TB
subgraph "前端 Frontend"
Source[X 源码]
Lexer[词法分析器<br/>x-lexer]
Parser[语法分析器<br/>x-parser]
TypeChecker[类型检查器<br/>x-typechecker]
end
subgraph "中间层 Middle End"
AST[抽象语法树 AST]
HIR[高级中间表示 HIR]
PIR[Perceus 中间表示 PIR]
XIR[统一中间表示 XIR]
end
subgraph "AOT 编译 AOT Compilation"
C23[C23 后端]
LLVM[LLVM 后端]
Native[原生机器码]
end
subgraph "JIT 编译 JIT Compilation"
JITDriver[JIT 驱动层<br/>x-jit]
JVMRuntime[JVM 后端]
CLRRuntime[CLR 后端]
PythonRuntime[Python 后端]
JSRuntime[JavaScript 后端]
end
Source --> Lexer
Lexer --> Parser
Parser --> AST
AST --> TypeChecker
TypeChecker --> HIR
HIR --> PIR
PIR --> XIR
XIR --> C23
XIR --> LLVM
C23 --> Native
LLVM --> Native
XIR --> JITDriver
JITDriver --> JVMRuntime
JITDriver --> CLRRuntime
JITDriver --> PythonRuntime
JITDriver --> JSRuntime
核心设计原则
- 统一 IR:XIR 作为所有后端的公共输入
- 共享优化:优化层在 XIR 级别完成,所有后端受益
- 可插拔后端:AOT 和 JIT 后端可独立插拔
- 延迟编译:JIT 支持函数级别的延迟编译
- 多模式运行:同一程序可选择解释执行、AOT 编译或 JIT 编译
中间表示层
多层 IR 设计
X 源码
↓ [Lexer + Parser]
AST (抽象语法树)
↓ [TypeChecker]
Typed AST (带类型标注的 AST)
↓ [Lower]
HIR (高级中间表示) - 保留高级语义
↓ [Perceus]
PIR (Perceus IR) - 带 dup/drop/reuse 指令
↓ [Lower]
XIR (X 中间表示) - 类 SSA 的低级 IR
↓
AOT / JIT 后端
XIR 设计增强
现有 XIR 需要增强以支持 JIT:
#![allow(unused)]
fn main() {
// compiler/x-codegen/src/xir.rs (增强版)
/// XIR 程序 - 支持模块级别的增量编译
pub struct Program {
pub modules: Vec<Module>,
}
/// 模块 - 可独立编译的单元
pub struct Module {
pub name: String,
pub declarations: Vec<Declaration>,
pub dependencies: Vec<String>,
}
/// 函数 - 支持 JIT 懒编译标记
pub struct Function {
pub name: String,
pub signature: FunctionSignature,
pub body: Option<Block>, // None 表示尚未编译
pub is_jit_ready: bool,
pub optimization_level: OptimizationLevel,
}
/// 优化级别
pub enum OptimizationLevel {
None, // JIT 快速编译
Basic, // 基础优化
Standard, // 标准优化
Aggressive, // 激进优化 (AOT 默认)
}
}
AOT 编译管道
AOT 编译流程
x compile --target native hello.x
↓
1. 解析源码 → AST
2. 类型检查 → Typed AST
3. 降级到 HIR → PIR → XIR
4. XIR 优化 (内联、常量传播、DCE 等)
5. 后端代码生成 (C23/LLVM)
6. 链接 → 可执行文件
增强的 AOT 配置
#![allow(unused)]
fn main() {
// compiler/x-codegen/src/config.rs
pub struct AotConfig {
pub target: Target,
pub output_path: Option<PathBuf>,
pub optimization_level: OptimizationLevel,
pub debug_info: bool,
pub link_time_optimization: bool,
pub code_model: CodeModel,
pub relocation_model: RelocationModel,
}
pub enum Target {
// 原生目标
Native,
Wasm,
// 源码目标
C,
JavaScript,
TypeScript,
// 字节码目标
JvmBytecode,
DotNetCil,
PythonBytecode,
}
}
JIT 编译架构
JIT 核心设计
flowchart LR
subgraph "用户程序 User Program"
Main[main 函数]
Foo[foo 函数]
Bar[bar 函数]
end
subgraph "JIT 引擎 JIT Engine"
Driver[JIT 驱动器<br/>x-jit::JitEngine]
Cache[编译缓存<br/>CompilationCache]
Profiler[性能分析器<br/>Profiler]
Optimizer[优化决策器<br/>OptimizationDecider]
end
subgraph "平台后端 Platform Backends"
JVM[JVM 后端]
CLR[CLR 后端]
Python[Python 后端]
JS[JavaScript 后端]
end
Main -->|调用| Driver
Driver -->|检查缓存| Cache
Cache -->|未命中| Driver
Driver -->|编译| JVM
Driver -->|编译| CLR
Driver -->|编译| Python
Driver -->|编译| JS
Profiler -->|采样| Driver
Profiler -->|反馈| Optimizer
Optimizer -->|重新优化| Driver
JIT 引擎核心 Trait
#![allow(unused)]
fn main() {
// compiler/x-jit/src/engine.rs
/// JIT 引擎 - 主入口
pub struct JitEngine {
config: JitConfig,
cache: CompilationCache,
profiler: Profiler,
backend: Box<dyn JitBackend>,
context: JitContext,
}
impl JitEngine {
/// 创建新的 JIT 引擎
pub fn new(config: JitConfig) -> Result<Self, JitError> {
// 根据配置选择后端
let backend = Self::create_backend(&config)?;
Ok(Self {
config,
cache: CompilationCache::new(),
profiler: Profiler::new(),
backend,
context: JitContext::new(),
})
}
/// 编译并执行一个函数
pub fn execute_function(
&mut self,
function_name: &str,
args: &[Value],
) -> Result<Value, JitError> {
// 1. 检查缓存
if let Some(compiled) = self.cache.get(function_name) {
return self.backend.execute(compiled, args);
}
// 2. 获取 XIR
let xir = self.context.get_function_xir(function_name)?;
// 3. JIT 编译
let compiled = self.backend.compile_function(xir, &self.config)?;
// 4. 缓存
self.cache.insert(function_name, compiled.clone());
// 5. 执行
self.backend.execute(compiled, args)
}
/// 异步编译函数(后台)
pub async fn compile_async(&mut self, function_name: &str) -> Result<(), JitError> {
let xir = self.context.get_function_xir(function_name)?;
let compiled = self.backend.compile_function(xir, &self.config)?;
self.cache.insert(function_name, compiled);
Ok(())
}
}
/// JIT 后端 trait
pub trait JitBackend: Send + Sync {
fn compile_function(
&self,
xir: &xir::Function,
config: &JitConfig,
) -> Result<CompiledFunction, JitError>;
fn execute(
&self,
function: &CompiledFunction,
args: &[Value],
) -> Result<Value, JitError>;
fn compile_module(
&self,
xir: &xir::Module,
config: &JitConfig,
) -> Result<CompiledModule, JitError>;
}
/// JIT 配置
#[derive(Debug, Clone)]
pub struct JitConfig {
pub target: JitTarget,
pub optimization_level: JitOptimizationLevel,
pub compilation_strategy: CompilationStrategy,
pub enable_profiling: bool,
pub enable_tiered_compilation: bool,
}
/// JIT 目标平台
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JitTarget {
Jvm,
Clr,
Python,
JavaScript,
Wasm,
Native, // 使用 LLVM ORC JIT
}
/// 编译策略
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CompilationStrategy {
Lazy, // 函数首次调用时编译
Eager, // 启动时全部编译
Adaptive, // 根据调用频率自适应
Background, // 后台编译
}
/// 分层编译级别
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JitOptimizationLevel {
Tier0, // 无优化,快速编译
Tier1, // 简单优化
Tier2, // 标准优化
Tier3, // 激进优化 (需要 profiling 反馈)
}
}
编译缓存设计
#![allow(unused)]
fn main() {
// compiler/x-jit/src/cache.rs
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
/// 编译缓存 - 线程安全
pub struct CompilationCache {
functions: RwLock<HashMap<String, Arc<CompiledFunction>>>,
modules: RwLock<HashMap<String, Arc<CompiledModule>>>,
stats: CacheStats,
}
impl CompilationCache {
pub fn new() -> Self {
Self {
functions: RwLock::new(HashMap::new()),
modules: RwLock::new(HashMap::new()),
stats: CacheStats::default(),
}
}
pub fn get(&self, name: &str) -> Option<Arc<CompiledFunction>> {
let guard = self.functions.read().unwrap();
guard.get(name).cloned()
}
pub fn insert(&self, name: &str, function: CompiledFunction) {
let mut guard = self.functions.write().unwrap();
guard.insert(name.to_string(), Arc::new(function));
self.stats.hits += 1;
}
/// 基于内容哈希的缓存键
pub fn make_key(function_name: &str, xir_hash: u64, opt_level: JitOptimizationLevel) -> String {
format!("{function_name}_{xir_hash}_{opt_level:?}")
}
}
#[derive(Debug, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub compiled_functions: u64,
}
}
性能分析与分层编译
#![allow(unused)]
fn main() {
// compiler/x-jit/src/profiler.rs
/// 性能分析器 - 收集调用信息用于分层编译
pub struct Profiler {
function_calls: RwLock<HashMap<String, FunctionProfile>>,
sampling_enabled: bool,
}
#[derive(Debug, Clone)]
pub struct FunctionProfile {
pub call_count: u64,
pub total_execution_time_ns: u64,
pub average_execution_time_ns: u64,
pub last_call_timestamp: u64,
pub optimization_tier: JitOptimizationLevel,
}
impl Profiler {
/// 记录函数调用
pub fn record_call(&self, function_name: &str, execution_time_ns: u64) {
let mut guard = self.function_calls.write().unwrap();
let profile = guard.entry(function_name.to_string())
.or_insert_with(FunctionProfile::new);
profile.call_count += 1;
profile.total_execution_time_ns += execution_time_ns;
profile.average_execution_time_ns =
profile.total_execution_time_ns / profile.call_count;
}
/// 判断是否应该升级优化级别
pub fn should_upgrade_tier(&self, function_name: &str) -> Option<JitOptimizationLevel> {
let guard = self.function_calls.read().unwrap();
let profile = guard.get(function_name)?;
let next_tier = match profile.optimization_tier {
JitOptimizationLevel::Tier0 if profile.call_count > 10 =>
Some(JitOptimizationLevel::Tier1),
JitOptimizationLevel::Tier1 if profile.call_count > 100 =>
Some(JitOptimizationLevel::Tier2),
JitOptimizationLevel::Tier2 if profile.call_count > 1000 =>
Some(JitOptimizationLevel::Tier3),
_ => None,
};
next_tier
}
}
/// 优化决策器
pub struct OptimizationDecider {
profiler: Arc<Profiler>,
recompile_queue: mpsc::Sender<RecompileTask>,
}
pub struct RecompileTask {
pub function_name: String,
pub target_tier: JitOptimizationLevel,
}
}
多平台 JIT 后端
1. JVM 后端
#![allow(unused)]
fn main() {
// compiler/x-jit-jvm/src/lib.rs
pub struct JvmBackend {
class_loader: ClassLoader,
bytecode_builder: BytecodeBuilder,
}
impl JvmBackend {
pub fn new() -> Result<Self, JitError> {
Ok(Self {
class_loader: ClassLoader::new()?,
bytecode_builder: BytecodeBuilder::new(),
})
}
}
impl JitBackend for JvmBackend {
fn compile_function(
&self,
xir: &xir::Function,
config: &JitConfig,
) -> Result<CompiledFunction, JitError> {
// 1. XIR → JVM 字节码
let class_name = format!("x/lang/generated/{}", xir.name);
let bytecode = self.bytecode_builder.build(xir, config)?;
// 2. 加载类
let class = self.class_loader.define_class(&class_name, &bytecode)?;
// 3. 获取方法句柄
let method = class.get_method(xir.name)?;
Ok(CompiledFunction::Jvm(JvmCompiledFunction {
class_name,
method,
bytecode,
}))
}
fn execute(
&self,
function: &CompiledFunction,
args: &[Value],
) -> Result<Value, JitError> {
match function {
CompiledFunction::Jvm(jvm_func) => {
// 调用 JVM 方法
let jni_args = Self::convert_args(args);
let result = jvm_func.method.invoke(jni_args)?;
Self::convert_result(result)
}
_ => Err(JitError::InvalidBackend),
}
}
}
}
2. CLR (.NET) 后端
#![allow(unused)]
fn main() {
// compiler/x-jit-clr/src/lib.rs
use dnlib::{ModuleDef, MethodDef, ILGenerator};
pub struct ClrBackend {
assembly_builder: AssemblyBuilder,
}
impl ClrBackend {
pub fn new() -> Result<Self, JitError> {
Ok(Self {
assembly_builder: AssemblyBuilder::new("x.runtime")?,
})
}
}
impl JitBackend for ClrBackend {
fn compile_function(
&self,
xir: &xir::Function,
config: &JitConfig,
) -> Result<CompiledFunction, JitError> {
// 1. 创建动态程序集/模块/类型
let module = self.assembly_builder.create_module(&format!("XModule_{}", xir.name))?;
let type_def = module.create_type(&format!("XType_{}", xir.name))?;
// 2. XIR → CIL
let mut method_def = type_def.create_method(
xir.name,
Self::convert_signature(&xir.signature),
)?;
let mut il = method_def.get_il_generator();
XirToCil::lower(xir, &mut il)?;
// 3. 保存并加载
let assembly = self.assembly_builder.build()?;
let method = assembly.get_method(&type_def.name, &xir.name)?;
Ok(CompiledFunction::Clr(ClrCompiledFunction {
method,
assembly,
}))
}
fn execute(
&self,
function: &CompiledFunction,
args: &[Value],
) -> Result<Value, JitError> {
match function {
CompiledFunction::Clr(clr_func) => {
let clr_args = Self::convert_args(args);
let result = clr_func.method.invoke(clr_args)?;
Self::convert_result(result)
}
_ => Err(JitError::InvalidBackend),
}
}
}
}
3. Python 后端
#![allow(unused)]
fn main() {
// compiler/x-jit-python/src/lib.rs
use pyo3::{Python, PyObject, types::PyModule};
pub struct PythonBackend {
py: Python<'static>,
module: PyObject,
}
impl PythonBackend {
pub fn new() -> Result<Self, JitError> {
let gil = Python::acquire_gil();
let py = gil.python();
// 创建主模块
let module = PyModule::new(py, "x_runtime")?;
Ok(Self {
py,
module: module.into(),
})
}
}
impl JitBackend for PythonBackend {
fn compile_function(
&self,
xir: &xir::Function,
config: &JitConfig,
) -> Result<CompiledFunction, JitError> {
// 1. XIR → Python AST
let py_ast = XirToPythonAst::lower(xir)?;
// 2. Python AST → 源码字符串
let source = PythonAstPrinter::print(&py_ast)?;
// 3. 编译源码
let code = self.py.compile(
&source,
&format!("<x-generated-{}.py>", xir.name),
"exec",
)?;
// 4. 在模块中执行
self.py.run_code(&code, Some(&self.module), None)?;
// 5. 获取函数对象
let func = self.module.getattr(self.py, xir.name)?;
Ok(CompiledFunction::Python(PythonCompiledFunction {
function: func,
source_code: source,
}))
}
fn execute(
&self,
function: &CompiledFunction,
args: &[Value],
) -> Result<Value, JitError> {
match function {
CompiledFunction::Python(py_func) => {
let py_args = Self::convert_args(self.py, args);
let result = py_func.function.call1(self.py, py_args)?;
Self::convert_result(self.py, result)
}
_ => Err(JitError::InvalidBackend),
}
}
}
}
4. JavaScript 后端
#![allow(unused)]
fn main() {
// compiler/x-jit-js/src/lib.rs
use rquickjs::{Context, Runtime, Function, Value as JsValue};
pub struct JavaScriptBackend {
runtime: Runtime,
context: Context,
}
impl JavaScriptBackend {
pub fn new() -> Result<Self, JitError> {
let runtime = Runtime::new()?;
let context = Context::new(&runtime)?;
Ok(Self { runtime, context })
}
}
impl JitBackend for JavaScriptBackend {
fn compile_function(
&self,
xir: &xir::Function,
config: &JitConfig,
) -> Result<CompiledFunction, JitError> {
// 1. XIR → JavaScript 源码
let js_source = XirToJavaScript::lower(xir)?;
// 2. 在 QuickJS 中编译
let function = self.context.with(|ctx| {
let func: Function = ctx.eval(&js_source)?;
Ok::<_, JitError>(func)
})?;
Ok(CompiledFunction::JavaScript(JavaScriptCompiledFunction {
function,
source_code: js_source,
}))
}
fn execute(
&self,
function: &CompiledFunction,
args: &[Value],
) -> Result<Value, JitError> {
match function {
CompiledFunction::JavaScript(js_func) => {
self.context.with(|ctx| {
let js_args = Self::convert_args(ctx, args);
let result: JsValue = js_func.function.call((js_args,))?;
Self::convert_result(result)
})
}
_ => Err(JitError::InvalidBackend),
}
}
}
}
Crate 组织结构
完整 Crate 列表
x-lang/
├── compiler/
│ ├── x-lexer/ # 词法分析器 (已存在)
│ ├── x-parser/ # 语法分析器 (已存在)
│ ├── x-typechecker/ # 类型检查器 (已存在)
│ ├── x-hir/ # HIR (已存在)
│ ├── x-perceus/ # Perceus 分析 (已存在)
│ ├── x-codegen/ # 公共代码生成 + XIR (已存在)
│ │
│ ├── x-jit/ # [新增] JIT 核心引擎
│ ├── x-jit-jvm/ # [新增] JVM JIT 后端
│ ├── x-jit-clr/ # [新增] CLR JIT 后端
│ ├── x-jit-python/ # [新增] Python JIT 后端
│ ├── x-jit-js/ # [新增] JavaScript JIT 后端
│ │
│ ├── x-codegen-llvm/ # LLVM AOT 后端 (已存在)
│ ├── x-codegen-c/ # C23 AOT 后端 (已存在)
│ ├── x-codegen-jvm/ # JVM AOT 后端 (已存在)
│ ├── x-codegen-dotnet/ # .NET AOT 后端 (已存在)
│ ├── x-codegen-js/ # JavaScript AOT 后端 (已存在)
│ │
│ └── x-interpreter/ # 树遍历解释器 (已存在)
│
├── library/
│ ├── x-stdlib/ # 标准库 (已存在)
│ ├── x-runtime-jvm/ # [新增] JVM 运行时支持
│ ├── x-runtime-clr/ # [新增] CLR 运行时支持
│ ├── x-runtime-python/ # [新增] Python 运行时支持
│ └── x-runtime-js/ # [新增] JavaScript 运行时支持
│
├── tools/
│ ├── x-cli/ # CLI (已存在)
│ └── x-lsp/ # [新增] LSP 服务器
│
└── spec/
└── x-spec/ # 规格测试 (已存在)
新增 Crate 依赖关系
# compiler/x-jit/Cargo.toml
[package]
name = "x-jit"
version = "0.1.0"
[dependencies]
x-codegen = { path = "../x-codegen" }
x-hir = { path = "../x-hir" }
# JIT 后端可选依赖
x-jit-jvm = { path = "../x-jit-jvm", optional = true }
x-jit-clr = { path = "../x-jit-clr", optional = true }
x-jit-python = { path = "../x-jit-python", optional = true }
x-jit-js = { path = "../x-jit-js", optional = true }
[features]
default = []
jvm = ["x-jit-jvm"]
clr = ["x-jit-clr"]
python = ["x-jit-python"]
js = ["x-jit-js"]
all = ["jvm", "clr", "python", "js"]
API 设计
CLI 扩展
# AOT 编译 (已有)
x compile hello.x -o hello
x compile --target jvm hello.x -o hello.jar
# JIT 运行 (新增)
x jit --target jvm hello.x
x jit --target python hello.x
x jit --target js hello.x
# REPL 模式 (新增)
x repl --target jvm
x repl --target js
# 混合模式 (新增)
x run --jit hello.x # 自动选择 AOT + JIT 混合
x run --jit-tiered hello.x # 启用分层编译
编程 API
// 作为库使用的 API 示例
use x_jit::{JitEngine, JitConfig, JitTarget, CompilationStrategy};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 创建 JIT 引擎
let config = JitConfig {
target: JitTarget::Jvm,
optimization_level: JitOptimizationLevel::Tier0,
compilation_strategy: CompilationStrategy::Adaptive,
enable_profiling: true,
enable_tiered_compilation: true,
};
let mut engine = JitEngine::new(config)?;
// 2. 加载 XIR 模块
let program = x_parser::parse_file("hello.x")?;
let typed = x_typechecker::check(program)?;
let xir = x_codegen::lower_to_xir(typed)?;
engine.load_module(xir)?;
// 3. 执行函数
let args = vec![Value::Integer(42)];
let result = engine.execute_function("compute", &args)?;
println!("Result: {:?}", result);
// 4. 异步预编译热点函数
engine.compile_async("hot_function").await?;
Ok(())
}
REPL API
// REPL 示例
use x_jit::{Repl, JitTarget};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut repl = Repl::new(JitTarget::JavaScript);
println!("X REPL (JavaScript backend)");
println!("Type ':quit' to exit\n");
loop {
let input = repl.read_line("> ")?;
match input.trim() {
":quit" => break,
":clear" => repl.clear(),
":env" => repl.print_environment(),
line => {
match repl.eval(line) {
Ok(value) => println!("{:?}", value),
Err(e) => eprintln!("Error: {}", e),
}
}
}
}
Ok(())
}
实现阶段
Phase 1: JIT 核心框架
-
x-jitcrate 基础结构 - JIT 引擎 trait 定义
- 编译缓存实现
- 与 XIR 集成
Phase 2: JavaScript JIT 后端
-
x-jit-jscrate - XIR → JavaScript 源码生成
- QuickJS 集成
- 基本执行测试
Phase 3: Python JIT 后端
-
x-jit-pythoncrate - XIR → Python AST/源码
- PyO3 集成
- 基本执行测试
Phase 4: JVM JIT 后端
-
x-jit-jvmcrate - XIR → JVM 字节码
- JNI 集成
- 类加载器实现
Phase 5: CLR JIT 后端
-
x-jit-clrcrate - XIR → CIL
- .NET 宿主集成
- 动态程序集生成
Phase 6: 优化与工具
- 分层编译
- 性能分析器
- 优化反馈循环
- REPL 实现
- CLI 集成
总结
本架构设计提供了:
- 统一 IR 层:XIR 作为 AOT 和 JIT 的公共输入
- 可插拔 JIT 后端:支持 JVM、CLR、Python、JavaScript
- 分层编译:基于 profiling 的自适应优化
- 缓存机制:避免重复编译
- 完整的工具链:CLI、REPL、库 API
这个设计使得 X 语言可以灵活地在不同场景下选择最优的执行方式:
- AOT:适合需要最高性能、启动延迟不敏感的场景
- JIT:适合需要快速启动、交互式使用、动态生成代码的场景
- 混合:热点函数 JIT 优化,其他函数 AOT 编译
函数与闭包
函数定义
函数是 X 语言中最基本的抽象单元,用于封装可重用的代码逻辑。在 X 中,函数使用 function 关键字声明,遵循以下语法:
function add(a: Integer, b: Integer) -> Integer = a + b
function greet(name: String) -> String {
let message = "Hello, {name}!"
message
}
函数定义由以下部分组成:
- 函数名:遵循标识符命名规则,绑定在当前作用域中。
- 参数列表:定义函数的输入参数,每个参数可以包含类型注解和默认值。
- 返回类型:可选,使用
-> Type语法显式声明,也可由编译器自动推导。 - 函数体:有两种形式:
- 表达式形式:使用
= expression直接返回表达式的值 - 块形式:使用
{ statements }执行多个语句,最后一个表达式为返回值
- 表达式形式:使用
函数声明语法
FunctionDeclaration ::= 'function' Identifier Parameters ('->' Type)? ('with' EffectList)? FunctionBody
Parameters ::= '(' (Parameter (',' Parameter)*)? ')'
Parameter ::= Identifier (':' Type)? ('=' Expression)?
FunctionBody ::= '=' Expression
| Block
函数参数
X 语言支持多种参数形式,提供灵活的函数调用方式:
位置参数
按顺序传递的参数,数量和类型必须与函数声明匹配:
function add(a: Integer, b: Integer) -> Integer = a + b
add(1, 2) // 3
命名参数
通过参数名指定,可以任意顺序传递:
function formatDate(year: Integer, month: Integer, day: Integer) -> String {
"{year}-{month}-{day}"
}
formatDate(year = 2026, month = 3, day = 7) // "2026-3-7"
默认参数
为参数提供默认值,调用时可省略:
function increment(x: Integer, step: Integer = 1) -> Integer = x + step
increment(5) // 6
increment(5, 2) // 7
管道运算符
使用 |> 运算符将左侧表达式作为第一个参数传入右侧函数,支持函数链式调用:
let result = data
|> filter(is_valid)
|> map(transform)
|> take(10)
函数返回值
函数可以显式声明返回类型,也可以由编译器通过 Hindley-Milner 类型推断自动推导:
显式返回类型
function add(a: Integer, b: Integer) -> Integer = a + b
隐式返回类型
function add(a: Integer, b: Integer) = a + b // 编译器推断返回类型为 Integer
块形式的返回值
在块形式的函数体中,最后一个表达式的结果作为函数返回值,也可以使用 return 语句显式返回:
function max(a: Integer, b: Integer) -> Integer {
if a > b {
return a
} else {
b
}
}
效果注解
函数可以通过 with 关键字声明可能产生的效果,如 IO、异步操作、异常等:
function printMessage(message: String) -> () with IO {
print(message)
}
function readFile(path: String) -> String with IO, Throws<FileNotFound> {
let file = open(path)?
file.readAll()
}
闭包
闭包是可以捕获周围作用域变量的函数值。在 X 中,匿名函数(Lambda)会创建闭包:
function make_counter() -> () -> Integer {
let mutable count = 0
() -> {
count = count + 1
count
}
}
let counter = make_counter()
counter() // 1
counter() // 2
counter() // 3
闭包的特性
- 变量捕获:闭包在定义时捕获周围作用域的变量。
- 环境关联:被捕获的变量保持与外部作用域的关联。
- 可变变量:若外部变量为
let mutable,闭包内外的修改相互可见。
匿名函数语法
Lambda ::= '(' (Identifier (',' Identifier)*)? ')' '->' Expression
| '(' (Identifier (',' Identifier)*)? ')' '->' Block
| '.' Identifier // 点缩写
点缩写语法
点缩写提供简洁的字段/方法访问语法,等价于以对象为参数的单参数 lambda:
let names = users |> map(.name) // 等价于 map((u) -> u.name)
let adults = users |> filter(.age >= 18) // 等价于 filter((u) -> u.age >= 18)
高阶函数
高阶函数是指以函数为参数或以函数为返回值的函数。在 X 中,函数是一等公民,可以作为参数传递、作为返回值返回、赋值给变量。
函数作为参数
function apply_twice(f: (Integer) -> Integer, x: Integer) -> Integer {
f(f(x))
}
let result = apply_twice((x) -> x * 2, 5) // 20
函数作为返回值
function make_adder(n: Integer) -> (Integer) -> Integer {
(x) -> x + n
}
let add5 = make_adder(5)
add5(10) // 15
柯里化与部分应用
function multiply(a: Integer, b: Integer) -> Integer = a * b
let double = multiply(2, _) // 部分应用
let triple = multiply(3, _) // 部分应用
double(5) // 10
triple(5) // 15
常见高阶函数
X 语言提供了丰富的高阶函数,如 map、filter、reduce 等:
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers |> map((x) -> x * 2) // [2, 4, 6, 8, 10]
let evens = numbers |> filter((x) -> x % 2 == 0) // [2, 4]
let sum = numbers |> reduce((acc, x) -> acc + x, 0) // 15
递归函数
函数可以在其体内引用自身实现递归。rec 关键字是可选的,主要用于明确表示递归意图:
function factorial(n: Integer) -> Integer {
match n {
0 => 1
_ => n * factorial(n - 1)
}
}
function rec fibonacci(n: Integer) -> Integer {
match n {
0 => 0
1 => 1
_ => fibonacci(n - 1) + fibonacci(n - 2)
}
}
分段定义
X 支持分段定义,允许将多个 case 写成独立的函数子句:
function fibonacci(0) -> Integer = 0
function fibonacci(1) -> Integer = 1
function fibonacci(n) -> Integer = fibonacci(n - 1) + fibonacci(n - 2)
多态函数
函数可以有类型参数(泛型),使用 <> 语法。类型参数在调用时由编译器根据实参类型推断,也可以显式指定:
function identity<T>(x: T) -> T = x
function max<T: Comparable>(a: T, b: T) -> T {
if a > b { a } else { b }
}
function swap<A, B>(pair: (A, B)) -> (B, A) = (pair.1, pair.0)
// 调用:类型由推断确定
let x = identity(42) // T = Integer
let m = max(3, 7) // T = Integer
let s = swap(("hello", 42)) // A = String, B = Integer
总结
X 语言的函数系统提供了丰富的特性,包括:
- 灵活的函数定义语法,支持表达式形式和块形式
- 多种参数形式:位置参数、命名参数、默认参数
- 强大的类型系统,支持类型推断和泛型
- 闭包机制,允许函数捕获和修改外部变量
- 高阶函数支持,使函数成为一等公民
- 递归和分段定义,方便实现复杂算法
- 效果注解,使副作用在类型层面可见
这些特性使得 X 语言能够表达从简单到复杂的各种计算逻辑,同时保持代码的清晰性和可维护性。
X 编程语言类型系统
X 语言拥有完整的、健全的类型系统,基于 Hindley-Milner 类型推断与代数数据类型。所有值都有确定的类型,所有错误路径在类型签名中显式可见。本文档详细介绍 X 语言的类型系统核心特性。
1. 类型推断
X 的类型推断基于 Hindley-Milner(HM)算法及其扩展,使得绝大多数类型注解可省略,编译器能够自动推断出表达式的类型。
1.1 推断原理
类型推断的核心是通过以下规则推导出表达式的类型:
- 变量查找:从类型环境中查找变量的类型
- 函数应用:根据函数的参数类型和返回类型,推断函数调用的类型
- 函数抽象:根据函数体的类型,推断函数的类型
- 条件表达式:要求条件分支的类型一致
- Let 绑定:支持类型泛化,使得同一变量在不同上下文中可以有不同的具体类型
1.2 统一算法
类型推断的关键是**统一(Unification)**算法,它通过找到最通用的类型替换,使得不同的类型表达式能够匹配:
unify(α, T) = [α ↦ T] if α ∉ FTV(T)
unify(T, α) = [α ↦ T] if α ∉ FTV(T)
unify(C<T₁,...,Tₙ>, C<U₁,...,Uₙ>) = σₙ ∘ ... ∘ σ₁
where σᵢ = unify(σᵢ₋₁(Tᵢ), σᵢ₋₁(Uᵢ))
1.3 推断示例
// 编译器自动推断所有类型
let x = 42 // x : integer
let f = function(a, b) => a + b // f : (integer, integer) -> integer
let xs = [1, 2, 3] // xs : [integer]
let result = xs |> map(function(n) => n * 2) // result : [integer]
let pair = (true, "hello") // pair : (boolean, string)
1.4 局部推断与顶层签名
- 局部类型推断:函数体内几乎不需要任何类型注解
- 顶层签名:公共 API 建议写类型签名以提升可读性
- 双向类型检查:结合自上而下(期望类型)与自下而上(推断类型)两个方向
2. 代数数据类型
X 语言支持代数数据类型(Algebraic Data Types, ADTs),包括积类型(product types)和和类型(sum types)。
2.1 积类型
积类型表示多个值的组合,包括:
-
元组(Tuple):固定长度、异构类型的有序集合
let pair: (integer, string) = (42, "answer") -
记录(Record):具名字段的积类型
type Point = { x: float, y: float } let origin: Point = { x: 0.0, y: 0.0 }
2.2 和类型
和类型表示多个可能类型的选择,主要通过**联合类型(Union)**实现:
type Shape =
| Circle { radius: float }
| Rect { width: float, height: float }
| Point
type Color =
| Red
| Green
| Blue
| Custom(integer, integer, integer)
2.3 常用代数数据类型
X 语言内置了几个常用的代数数据类型:
-
Option 类型:表示可能缺失的值
Option<T> = Some(T) | None -
Result 类型:表示可能失败的操作
Result<T, E> = Ok(T) | Err(E)
2.4 模式匹配
代数数据类型通常通过模式匹配来使用:
function area(shape: Shape) -> float {
match shape {
Circle { radius } => 3.14159 * radius * radius
Rect { width, height } => width * height
Point => 0.0
}
}
3. 泛型
X 语言支持泛型(参数多态),允许定义与具体类型无关的代码。
3.1 泛型类型
type Pair<A, B> = {
first: A,
second: B
}
let pair: Pair<integer, string> = { first: 42, second: "answer" }
3.2 泛型函数
function identity<T>(x: T) -> T {
x
}
function map<A, B>(list: [A], f: (A) -> B) -> [B] {
// 实现细节
}
3.3 类型约束
泛型参数可以有类型约束,限制它们必须实现特定的 Trait:
function sum<T: Numeric>(list: [T]) -> T {
list |> reduce(function(a, b) => a.add(b))
}
3.4 类型别名
可以为复杂类型创建别名,包括泛型类型:
type Name = string
type UserMap = {string: User}
type Callback<T> = (T) -> unit
4. Trait 系统
Trait 定义一组行为约束,类型可以实现 trait 以满足这些约束。
4.1 Trait 定义
trait Printable {
function show(): string
}
trait Numeric {
function add(other: Self) -> Self
function multiply(other: Self) -> Self
}
4.2 Trait 实现
implement Printable for Point {
function show() -> string {
"(${this.x}, ${this.y})"
}
}
4.3 Trait 约束
Trait 约束用于泛型参数,确保类型参数实现了特定的行为:
function print_all<T: Printable>(items: [T]) -> unit {
for item in items {
println(item.show())
}
}
function display_and_compare<T: Printable + Comparable>(a: T, b: T) -> string {
if a > b { a.show() } else { b.show() }
}
4.4 子类型关系
X 语言支持子类型关系,其中 Never 类型是所有类型的子类型:
Never <: T // 对所有类型 T
函数类型的子类型关系遵循逆变参数,协变返回的原则:
T'₁ <: T₁ ... T'ₙ <: Tₙ R <: R' Δ ⊆ Δ'
────────────────────────────────────────────────────
(T₁, ..., Tₙ) -> R with Δ <: (T'₁, ..., T'ₙ) -> R' with Δ'
5. 类型转换
X 语言的类型转换系统设计注重类型安全,避免运行时类型错误。
5.1 隐式类型转换
X 语言在某些情况下会进行隐式类型转换:
- 数字类型:从小范围类型到大范围类型的转换(如 integer 32 到 integer 64)
- 子类型:子类型可以隐式转换为父类型
- Never 类型:Never 类型可以转换为任何类型
5.2 显式类型转换
对于需要显式转换的情况,X 语言提供了类型转换操作:
let x: integer = 42
let y: float = float(x) // 显式转换为浮点数
let s: string = "123"
let n: integer = integer(s) // 字符串转整数,可能失败
5.3 类型安全
X 语言的类型转换设计确保了类型安全:
- 可能失败的转换返回 Result 类型
- 编译器在编译时检查类型转换的有效性
- 不存在运行时类型错误
5.4 类型组合
X 语言支持类型组合操作:
- 类型交集:
T & U表示同时具有 T 和 U 的所有成员 - 类型联合:
T | U表示具有 T 或 U 的成员
总结
X 语言的类型系统具有以下特点:
- 类型安全:编译时检查类型错误,无运行时类型异常
- 类型推断:基于 Hindley-Milner 算法的强大类型推断能力
- 代数数据类型:支持积类型和和类型,表达能力强
- 泛型:支持参数多态,代码复用性高
- Trait 系统:提供行为约束和接口抽象
- 无 null:使用 Option 类型替代 null,消除空指针错误
- 无异常:使用 Result 类型替代异常,错误处理显式可见
这些特性使得 X 语言在保证类型安全的同时,保持了代码的简洁性和表达能力。
X 表达式
说明
本文专门讲解 X 语言中的表达式体系,以及这些设计背后的原因。
- 侧重回答三个问题:
- 什么算“表达式”,能在什么位置出现?
- 有哪些核心表达式形态(算术、逻辑、控制流、集合、异步等)?
- 为什么 X 要把这么多东西设计成“表达式而不是语句”?
- 如与总规格说明书
README.md有冲突,以README.md为准。
1. 表达式 vs 语句:X 更偏“表达式语言”
在 X 中,只要能产生值,就尽量设计成 表达式(expression),而不是“只能单独成行”的语句:
- 表达式:有结果,可以嵌套、组合、赋值给变量、作为参数传递。
- 语句:主要承担结构和作用域,数量尽量减少(如
function定义、class定义、let绑定等)。
对比示例:
// if 作为表达式使用
let label =
if score >= 60 {
"pass"
} else {
"fail"
}
// when-then-else 完全是表达式
let level = when score >= 90 then "A"
else when score >= 75 then "B"
else "C"
这么做的原因:
- 让代码更接近数学函数式写法,减少“中间变量 + 多行 if”的样板代码。
- 组合性更强:任何控制流都可以内联在更大的表达式里,而无需拆成多段语句。
2. 字面量与基础表达式
2.1 数字、字符串、布尔值
42 // Integer
3.14 // Float
true, false // Boolean
"Hello, X" // String
"""
多行字符串
保留格式
"""
设计理由:
- 与主流语言的字面量形式兼容,降低迁移成本。
- 提供多行 / 插值字符串,方便直接表达文本模板。
2.2 记录、列表、字典等复合字面量
let point = { x: 1.0, y: 2.0 }
let nums = [1, 2, 3, 4]
let map = { "alice": 10, "bob": 20 }
设计理由:
- 首选 “所见即所得” 的直观字面量,而不是必须通过构造函数调用。
3. 变量、成员与函数调用表达式
3.1 变量与成员访问
user // 变量引用
user.name // 成员访问
point.x // 记录字段
设计理由:
- 与 C/Java/JavaScript 等保持一致,
.统一表示“从某个值中取出某个部分”,减少符号种类。
3.2 函数与方法调用
let sum = add(1, 2)
let s = user.toString()
// 链式调用
let result = users
.filter(.active)
.sortBy(.score)
.take(10)
设计理由:
- 保留传统“函数名 + 括号 + 实参”形式,符合所有主流语言习惯。
- 同时支持链式风格(类似 JavaScript / Kotlin / C#),方便面向对象 / Fluent API。
4. 运算符表达式:算术、比较、逻辑与管道
4.1 算术与比较
let a = 1 + 2 * 3
let ok = a >= 0 and a < 10
运算符:
- 算术:
+ - * / % - 比较:
== != < > <= >=
设计理由:
- 与 C 家族、Python、JavaScript 等完全一致,避免重新学习。
4.2 逻辑:not / and / or
let ready = not hasError and isConnected
if ready or isAdmin {
start()
}
设计理由:
- 放弃
!/&&/||这些符号,改用自然语言单词,类似 Python / SQL,语义更清晰,尤其在复杂条件中可读性更高。
4.3 管道运算符 |>
let topUsers =
users
|> filter(.active)
|> sortBy(.score)
|> take(10)
设计理由:
- 借鉴 F#、Elixir 等语言的
|>,把“数据流向”从左到右写出来,符合人眼阅读习惯。 - 避免深层嵌套括号,例如
take(10, sortBy(score, filter(active, users)))。
5. 函数与 Lambda 表达式
5.1 Lambda:(参数) -> 表达式
let double = (x) -> x * 2
let scores =
users |> map((u) -> u.score)
设计理由:
- 使用
->表示“从参数到结果”,接近数学函数写法,且在 TypeScript / Kotlin / Scala 等中已经广泛使用。
5.2 多行 Lambda
let sumOfSquares =
numbers |> reduce(0, (acc, x) -> {
let y = x * x
acc + y
})
设计理由:
- 允许在表达式内就地写出小块逻辑,而不必提升为顶层函数,兼顾局部性与可读性。
6. 控制流表达式:if / when / match
6.1 if 作为表达式
let label =
if x > 0 {
"positive"
} else {
"non-positive"
}
设计理由:
- 对齐 Rust、Kotlin 等“表达式导向”语言,使分支可以出现在任何需要值的地方。
6.2 when ... then ... else ...
let level =
when score >= 90 then "A"
else when score >= 75 then "B"
else "C"
设计理由:
- 避免传统三元运算符
condition ? a : b的符号负担。 - 读起来更像自然语言:“当…时,然后…,否则…”,适合作为更长表达式的一部分。
6.3 match 模式匹配
function area(shape: Shape) -> Float =
match shape {
Circle { radius } => pi * radius ^ 2
Rect { width, height } => width * height
Point => 0.0
}
``>
设计理由:
- 借鉴 Rust、Haskell、Scala 的 `match`,用 **穷尽匹配** 代替大量 `if-else` 链。
- 使代数数据类型(`type Shape = ...`)在语法层面一等公民,表达错误 / 状态 / 业务分支更安全。
---
### 7. 集合与推导式表达式
#### 7.1 列表 / 字典推导
```x
let evens = [x | x in 1..100, x mod 2 == 0]
let squares = [x^2 | x in 1..10]
let names = [u.name | u in users, u.active]
let scoreMap = {u.id: u.score | u in users}
设计理由:
- 直接参考数学集合表示和 Python 的列表推导式,适合描述“从一个集合变成另一个集合”的变换。
- 比链式
map/filter在简单场景下更短、更直观。
7.2 范围表达式
0..10 // [0, 1, ..., 9]
0..=10 // [0, 1, ..., 10]
设计理由:
- 用
../..=明确是否包含末尾,比“魔法数字” +< / <=组合更容易一眼看出边界。
8. Option / Result 与错误处理表达式
虽然 Option 和 Result 不是关键字,但在 X 的表达式世界里非常重要:
type User = { id: Integer, name: String }
function findUser(users: List<User>, id: Integer) -> Option<User> {
users |> filter(.id == id) |> first
}
let user = findUser(users, 42)
let name = user?.name ?? "Guest"
以及:
type IoError = NotFound { path: String } | PermissionDenied
function readConfig(path: String) -> Result<String, IoError> {
if not exists(path) {
return Err(IoError.NotFound(path))
}
Ok(readFile(path))
}
function loadConfig() -> Result<Config, IoError> {
let content = readConfig("config.toml")? // ? 表达式:失败时向上传播
parseConfig(content)?
}
设计理由:
- 无异常:错误处理完全通过表达式和类型组合完成,避免隐藏控制流。
?/??/?.等操作符让 Option/Result 表达式在多数场景下依然保持简洁。
9. 声明式风格表达式:管道 + where + sort by
let topUsers =
users
where .active and .score > 80
sort by .score descending
take 10
设计理由:
- 受 SQL / LINQ 启发,为 X 提供 接近自然语言的声明式管道:
where对应过滤条件sort by、take等都是表达式链的一部分
- 让“描述要什么结果”比“详细写循环和 if”更自然,特别适合数据处理、业务逻辑代码。
10. 设计总结:为什么 X 这么“偏爱表达式”
综合来看,X 在表达式设计上的几个核心目标是:
-
1. 代码像散文一样可读
尽可能用完整单词(function、not、and、or、match、where、async/await),而不是大量缩写与符号。阅读体验更接近英文说明书而非“符号谜题”。 -
2. 尽量一切皆表达式,减少语句种类
if/when/match、推导式、错误传播?、管道|>等都可以组成更大的表达式,使逻辑可以局部化表达,不必打碎到多处。 -
3. 对齐主流语言的直觉,降低学习成本
算术 / 比较 / 调用 / 类 / 模块等基础部分与 Python、C、JavaScript、Rust、Kotlin 等高度相似,只在“可读性确实更好”的地方(逻辑运算、声明式 where、管道、match)做了有意识的改进。 -
4. 让类型与控制流紧密结合
通过match+ ADT、Option/Result、where守卫等,让“所有可能分支”在语法上被清晰枚举,避免隐式异常和隐藏控制流。
这套表达式设计,是让 X 同时具备:
- 函数式语言的表达力与组合性,
- 命令式 / 面向对象语言的熟悉感,
- 以及接近自然语言的可读性。
X 关键字
说明
这篇文章专门介绍 X 语言的关键字及其用法示例,并参考了 languages.md 中主流语言(如 Python、JavaScript、C、Rust、Kotlin 等)的关键字设计,从中选取了 可读性最强、跨语言最容易理解 的风格来统一 X 的关键字体系。
选择这些关键字的总体原则:
- 使用完整英文单词(
function、not、and、or),避免缩写(对比 Python 的def、Rust 的fn) - 优先选用“行业共识”的单词(如
if/else、class、async/await、import/export) - 尽量避免符号化操作符(如
&&/||),用自然语言单词提高可读性 - 关键字语义“顾名思义”,不需要记忆额外规则
下文会按类别依次介绍核心关键字,并配上简短示例。
1. 变量与绑定:let / let mutable / const
1.1 设计理由
- 很多语言使用
var/let/const(参见 JavaScript、Kotlin、Swift),let已经是事实标准。 - X 延续
let的习惯,但改用 “完整短语”let mutable明确可变语义,比 C/C++ 中在类型前加const/省略更直观。 - X 额外提供
const表示“编译期常量”,其值在编译阶段就完全确定,便于优化与语义清晰。
1.2 用法示例:运行期绑定
// 不可变绑定(默认)
let name = "Alice"
let age: Integer = 30
// 可变变量
let mutable count = 0
count += 1
也可以配合解构使用:
// 元组解构
let (x, y) = (10, 20)
// 记录解构
let { name, age } = user
1.3 const:编译期常量
- 含义:
const声明的绑定在 编译期即确定值,不能依赖运行时输入或 I/O。 - 典型用途:版本号、协议标识、固定缓冲区大小等永远不会变化的配置。
const MAX-RETRY-COUNT: Integer = 3
const APP-NAME: String = "x-lang"
function connect() {
for i in 0..MAX-RETRY-COUNT {
// 尝试连接,使用编译期常量控制重试次数
}
}
与 let 的关系:
const:值在编译期就必须可计算出来,不能修改,编译器可以进行内联、折叠等优化。let:不可变绑定,但值可以在运行期计算(比如来自函数调用或 I/O)。let mutable:运行期可修改的变量。
2. 类型与抽象:type / trait
2.1 type
- 对比:很多语言有
type(TypeScript、Haskell)或typedef(C),而 X 采用简洁的type。 - 用途:定义别名、记录类型和代数数据类型,语义一目了然。
// 记录类型
type Point = {
x: Float,
y: Float
}
// 代数数据类型(ADT)
type Shape =
| Circle { radius: Float }
| Rect { width: Float, height: Float }
| Point
2.2 trait
- 对比:等价于许多语言中的 interface(Java、TypeScript、Go)的概念。
- 命名理由:选择 Rust 等语言中
trait这一 语义清晰的词,强调“可被类型实现的一组行为”。
trait Printable {
function show(): String
}
trait Comparable<T> {
function compare(other: T): Integer
}
3. 函数:function / return / ->
3.1 function
- 对比:
- Python / Ruby 使用
def - Rust 使用
fn - JavaScript / C 使用
function/ 函数声明
- Python / Ruby 使用
- 设计选择:X 采用 完整单词
function,对初学者和跨语言读者都更直观。
// 单行函数(隐式返回)
function add(a: Integer, b: Integer) -> Integer = a + b
// 多行函数
function factorial(n: Integer) -> Integer {
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
3.2 return
- 与 C / Java / Python 等主流语言一致,用于 提前返回,语义无歧义。
function findUser(id: Integer) -> Option<User> {
let user = database.query(id)
if user != None {
return Some(user)
}
None
}
3.3 箭头 ->(返回类型与 Lambda)
- 参考 TypeScript / Kotlin / Scala,使用
->表示“从参数到返回值”的映射,更接近数学函数记号。
// Lambda
let double = (x) -> x * 2
let sumOfSquares =
numbers |> map((n) -> n * n)
|> reduce(0, (acc, x) -> acc + x)
4. 控制流:if / else / when ... then ... else ... / while / for ... in
4.1 if / else
- 这些关键字在 C、Java、Python、JavaScript 等几乎所有主流语言中都存在,是 跨语言最容易识别 的控制流。
let label =
if x > 0 {
"positive"
} else {
"non-positive"
}
4.2 when ... then ... else ...
- 受数学“条件表达式”启发,使用完整单词
when/then,比三元运算符?:更可读。
let label = when x > 0 then "positive" else "non-positive"
4.3 while
let mutable i = 0
while i < 5 {
println(i)
i += 1
}
4.4 for ... in
- 对应 Python / Rust / Kotlin 等语言的
for ... in,相比传统 C 风格for(;;)更直观。
for user in users {
println(user.name)
}
for i in 0..=10 { // 含 10
println(i)
}
5. 模式匹配与守卫:match / where / _
5.1 match
- 借鉴 Rust、Haskell、Scala 的
match,命名清晰地表达“匹配多个分支”的含义。
function area(shape: Shape) -> Float =
match shape {
Circle { radius } => pi * radius ^ 2
Rect { width, height } => width * height
Point => 0.0
}
5.2 where
- 参考 SQL 和数学中的 where 子句,在
match和声明式查询中用于添加条件,语义接近自然语言。
// 守卫条件
function grade(score: Integer) -> String =
match score {
s where s >= 90 => "A"
s where s >= 75 => "B"
s where s >= 60 => "C"
_ => "F"
}
5.3 _(通配符)
- 与 Haskell、Rust、Scala 等保持一致,用
_作为“我不在乎这个值”的统一标记。
match option {
Some(v) => println(v)
_ => println("none")
}
6. 类型检查与转换:is / as
6.1 is
- 类似 C#、Python 中的
is语义,用自然语言单词而不是符号进行类型判断。
if value is String {
println("it's a String")
}
6.2 as
- 对应许多语言中的类型转换(C# 的
as、TypeScript 的as),统一采用自然语言关键字。
let x: Integer = 42
let y: Float = x as Float
7. 布尔逻辑:not / and / or
7.1 设计理由
- 大多数语言使用
&&/||/!,对初学者不够直观。 - 借鉴 Python、SQL 等,采用完整单词
not/and/or,与英语逻辑表达一致。
let ok = not hasError and isReady
if isAdmin or isOwner {
grantAccess()
}
8. 异步与并发:async / await / together / race / go / actor
8.1 async / await
- 参考 C#、JavaScript、Rust、Kotlin Dart 等主流语言,
async/await已经形成事实标准。 - 直接沿用这两个关键字,降低跨语言迁移成本。
async function fetchData() -> Data {
let users = await fetch("/api/users")
let posts = await fetch("/api/posts")
combine(users, posts)
}
8.2 together / race
- 使用自然语言短语代替库函数名(例如很多语言用
Promise.all/race,或Task.WhenAll)。 - X 使用
together强调“一起执行”,race强调“比谁先完成”,语义非常直观。
let (users, posts) = await together {
fetch("/api/users"),
fetch("/api/posts")
}
let fastest = await race { fetchPrimary(), fetchReplica() }
8.3 go
- 致敬 Go 语言中的
go关键字,表示启动轻量级协程。由于已经有广泛认知,沿用该短关键字即可。
go function() {
let result = computeHeavy()
channel.send(result)
}
8.4 actor
- 直接用领域术语
actor,与 Akka、Erlang/Elixir 的 Actor 模型概念对齐。
actor Counter {
let mutable count = 0
receive Increment => count += 1
receive GetCount(reply) => reply.send(count)
}
9. 模块与导入导出:module / import / export
9.1 module
- 与许多语言(F#, OCaml、Python 的 module 概念)保持一致,用
module标记模块声明。
module com.example.utils.string
9.2 import / export
- 借鉴 JavaScript / TypeScript、Python 等语言,使用非常直观的两个单词说明“导入 / 导出”。
// 导出符号
export function toCamelCase(s: String) -> String
export function toSnakeCase(s: String) -> String
// 导入整个模块
import com.example.utils.string
// 导入特定函数
import com.example.utils.string.toCamelCase
import com.example.utils.string.toSnakeCase as snakeCase
10. 类与对象:class / extends / new / virtual / override / abstract / extension
10.1 class / extends / new
- 完全沿用 Java / C# / TypeScript 等主流语言的命名,降低理解门槛。
class Animal {
name: String
age: Integer
new(name: String, age: Integer) {
this.name = name
this.age = age
}
function greet() -> String = "I'm {name}"
}
class Dog extends Animal {
breed: String
}
10.2 virtual / override / abstract
- 与 C#、C++ 等语言相同的术语,用来描述多态行为,语义清晰。
abstract class Shape {
abstract function area(): Float
abstract function perimeter(): Float
}
class Circle extends Shape {
radius: Float
override function area(): Float = pi * radius ^ 2
}
10.3 extension
- 借鉴 C# 和 Kotlin 的扩展方法概念,用
extension作为统一关键字,替代魔法语法或装饰器。
extension String {
function isPalindrome() -> Boolean {
this == reverse(this)
}
}
11. 推导式与管道:in / 管道运算符 |>
11.1 推导式中的 in
- 与 Python 列表推导、SQL 语义一致,
in表示“从某个集合中取值”。
let evens = [x | x in 1..100, x mod 2 == 0]
let scores = {u.id: u.score | u in users}
11.2 管道运算符 |>
- 不是关键字,但在 X 中非常重要,借鉴 F#、Elixir 等语言,使数据流表达更接近“从左到右”的自然阅读顺序。
let topUsers = users
|> filter(.active)
|> sortBy(.score)
|> take(10)
12. 小结:从主流语言中“借鉴可读性最强的关键字”
综合 languages.md 中各语言的关键字设计,可以看到几条共识:
- 控制流关键字高度收敛:几乎所有语言都使用
if/else、for/while,X 直接沿用这些“行业标准”。 - 命名更偏自然语言:与大量使用缩写的语言(
def、fn、sub)相比,X 选择function、not/and/or、match/where等完整单词,使代码接近英文散文。 - 异步与模块关键字与现代生态对齐:
async/await、import/export、module与 C#、JavaScript、TypeScript 等现代语言保持一致,方便开发者迁移。 - 并发与 OOP 使用领域通用术语:
actor、trait、extension等直接采用在多门语言中已经广泛使用的专业术语,避免发明新的名词。
因此,X 语言的关键字表是在 不改变现有规格的前提下,从 Python、JavaScript、C#、Rust、Kotlin 等语言的实践中,挑选出 最易读、最直观、跨语言迁移成本最低 的单词作为统一的关键字体系,并配合自然语言风格的控制流(如 when ... then ... else ...、where)和管道 |>,让代码尽可能接近人类日常阅读习惯。
X 类型
说明
本篇文档专门介绍 X 语言中的基础数据类型,按以下几个类别展开:
- 整数类型
- 浮点类型
- 布尔类型
- 字符类型
- 字符串类型
- 元组类型
- 数组类型
- 区间类型
这里的内容以根目录 README.md 的规格为依据,并补充了设计理由与示例,方便从其他语言迁移到 X 的读者快速建立直觉。
整数类型
抽象整数:integer / Integer
X 中的基础整数类型对外有一对名称:
-
integer:值类型(primitive),用于绝大多数计算场景 -
Integer:引用类型(boxed),在需要以对象形式存在(如放入统一的对象容器)时使用 -
抽象上表示数学意义上的整数(…,-2,-1,0,1,2,…)
-
规格上定义为 任意精度整数:理论上只受内存限制,不会像传统 32/64 位整型那样静默溢出
let a: integer = 42
let big: integer = 1_000_000_000_000_000
let sum: integer = a + big
let diff = big - a // 类型推断为 integer
固定位宽整数:完整英文短语形式
在需要与底层平台或其他语言精确对齐时,X 也提供了 固定位宽整数类型的内置别名,并且名称使用完整英文短语 + 空格,避免 i8 / u64 这类缩写和符号化命名:
- 有符号:如
signed 8bit integersigned 16bit integersigned 32bit integersigned 64bit integersigned 128bit integer
- 无符号:如
unsigned 8bit integerunsigned 16bit integerunsigned 32bit integerunsigned 64bit integerunsigned 128bit integer
示例:
let small: signed 8bit integer = 127
let port: unsigned 16bit integer = 8080
let size: unsigned 64bit integer = 1_000_000_000
let mask: unsigned 128bit integer = 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF
这些类型在语义上对应传统意义上的 int8 / uint64 / int128 / uint128,只是名字更直观;解析时它们被视为内置的多词类型名,不是普通标识符的组合。它们主要用于:
- 与 C/LLVM 等后端的整数类型一一对应
- 需要精确控制大小和溢出行为的性能敏感代码
在一般业务逻辑中,推荐优先使用抽象的 integer(值类型),只在有明确位宽需求时才选用这些别名类型;若需要装箱为对象时,则使用 Integer 引用类型。
设计原因
- 默认的
integer避免 C / Java 风格整型溢出的常见陷阱,特别是在财务、计费和计数场景。 - 通过完整英文短语别名(
signed 8bit integer/unsigned 64bit integer等),在 不牺牲可读性 的前提下,依然可以精确表达底层位宽与符号信息。 - 大写形式
Integer作为引用类型存在,方便在需要对象语义(如运行时反射、统一对象容器)时使用,同时保持“首字母大写 = 引用类型”的统一规则。
浮点类型
抽象浮点:float / Float
X 中的基础浮点类型也有一对名称:
-
float:值类型(primitive),默认对应 64 位双精度 -
Float:引用类型(boxed),用于需要对象语义的场合 -
默认对应 双精度浮点数(与大多数现代语言的
double类似) -
用于近似实数计算:物理量、评分、概率、统计等
let pi: float = 3.1415926535
let radius: float = 2.5
let area: float = pi * radius ^ 2
固定位宽浮点:32bit float / 64bit float 与十进制 decimal
类似整数,X 为浮点数提供了使用简洁完整短语的内置别名(值类型),分为二进制浮点和十进制浮点两类:
- 二进制浮点
32bit float:对应 32 位单精度64bit float:对应 64 位双精度
- 十进制浮点
32bit decimal:32 位十进制浮点64bit decimal:64 位十进制浮点128bit decimal:128 位十进制浮点,适合高精度金融 / 结算场景
示例:
let x: 32bit float = 1.0
let y: 64bit float = 3.1415926535
let price: 64bit decimal = 123.45
let amount: 128bit decimal = 1_000_000_000_000.0001
推荐用法:
- 绝大多数场景使用抽象的
float(默认 64 位) - 需要与外部系统 / 硬件精确对齐时,显式写出
32bit float、64bit float或未来的128bit float,让读者一眼看懂位宽 - 在金融、价格、计费等对十进制精度敏感的场景,优先使用
64bit decimal或128bit decimal,避免二进制浮点带来的舍入误差
设计原因
- 工程实践中,双精度已经覆盖绝大部分需求,因此
float默认指向 64 位浮点。 - 与其在语言里引入
f32/f64等缩写,不如用简洁但语义明确的短语(如32bit float)表达语义,符合 X “可读性优先”的设计哲学。 - 大写形式
Float作为引用类型存在,以支持装箱、运行时对象系统等需求,延续“小写值类型 / 大写引用类型”的整体约定。
布尔类型
语义
布尔类型为 boolean,只有两个字面量:
truefalse
let is-active: boolean = true
let has-error: boolean = false
if is-active and not has-error {
start()
}
设计原因
- 与所有主流语言保持一致,禁止 以
0/1充当真假,减少隐式转换带来的歧义。 - 配合
not/and/or这些关键字式逻辑运算符,让布尔表达式读起来更接近自然语言。
字符类型
语义
字符类型分为:
character:值类型,代表单个 Unicode 字符Character:引用类型,用于需要对象封装时
let ch: character = '中'
let letter: character = 'A'
可用于:
- 解析文本、编写词法分析器
- 单字符处理(如分类、过滤)
设计原因
- 与
String区分,方便编写对“一个字符”的 API,不必总是用长度为 1 的字符串来模拟。 - 使用 Unicode 而不是 ASCII,适应多语言环境。
字符串类型
语义
字符串类型分为:
-
string:值类型,用于绝大多数文本数据场景 -
String:引用类型,提供面向对象的字符串接口 -
普通字符串:
"..."(支持转义) -
多行字符串:
""" ... """(保留缩进和换行) -
插值字符串:
"Hello, {name}!"
let greeting: string = "Hello, X"
let multi = """
多行字符串
保留格式
"""
let name: string = "Alice"
let msg: string = "Hello, {name}!" // 插值
设计原因
- 字符串是最常用的基础类型之一,直接在语法层面支持多行与插值,减少模板拼接的样板代码。
- 插值风格与很多现代语言(Kotlin、C#、TS 模板字符串等)类似,降低迁移成本。
元组类型
语义
X 支持使用括号 (...) 表示位置元组,可以包含多个不同类型的元素:
let pair = (1, "one")
let triple = (user-id, is-active, last-login)
常见用途:
- 从函数返回多个值
- 在推导式或管道中组合临时数据
解构
元组可以通过 let 解构成多个绑定:
let (x, y) = (10, 20)
let (id, name) = (user.id, user.name)
设计原因
- 与记录类型
{ x: ..., y: ... }相比,元组更适合“一次性、局部的小组合”,无需命名字段。 - 对齐许多现代语言(Rust、Kotlin、Python)的设计,让“多返回值”等模式更自然。
数组类型
说明:在当前实现中,顺序集合的主要一等类型是
List<T>(以及语法糖[T]),用于大多数“数组场景”。本节中的“数组”更多是概念层面的描述:固定长度、连续存储的序列,底层通常由后端或标准库实现。
语义
数组可以理解为:
- 元素类型统一
- 长度在创建时固定
- 内存上通常连续存储(便于与 C / 低层 API 交互)
一个可能的数组声明示例(以概念说明为主):
// 概念示意:长度为 4 的 Integer 数组
let xs: Array<Integer> = [1, 2, 3, 4]
与 List<T> 的区别(设计意图):
List<T>:面向通用业务代码,可变长、高层抽象,配合管道、推导式使用。- 数组:面向性能敏感 / 底层交互场景,长度固定、布局可预测,更接近 C 风格数组。
设计原因
- 不在语言层面强制引入大量数组语法,而是以
List<T>作为默认顺序容器,简化心智模型。 - 为将来在标准库中提供高性能数组类型(如
Array<T>、FixedArray<T, N>)保留空间,同时保持语法上的直观度。
字典类型
语义
字典类型用于表示键值映射,在 X 中有两种等价写法:
- 泛型形式:
Dictionary<K, V> - 语法糖形式:
{K: V}(更常用)
let scores: {string: integer} = {
"alice": 90,
"bob": 80
}
let id_to_user: {integer: User} = {
1: user1,
2: user2
}
这里:
- 花括号外层表示“这是一个字典类型”
- 冒号左边是键类型,右边是值类型
典型用途
- 配置项(按 key 查 value)
- 缓存 / 索引结构(按 id 查实体)
- 频率统计(某个键出现次数)
与数组 / 列表的区别
- 数组 / 列表:强调顺序,通过整数下标访问(
0, 1, 2, ...) - 字典:强调按键访问,键的类型可以是
string、integer等任意可 hash/比较的类型
设计原因
- 键值映射是现代程序中最常用的数据结构之一,用
{K: V}这种直观语法可以一眼看出“这是从 K 到 V 的映射”。 - 同时保留
Dictionary<K, V>泛型形式,和其他泛型容器(如List<T>)保持风格一致。
区间类型
语法与语义
X 使用 .. / ..= 表示整数区间(范围):
a..b:从a到b,不包含 右端点ba..=b:从a到b,包含 右端点b
let r1 = 0..10 // 0, 1, ..., 9
let r2 = 1..=3 // 1, 2, 3
区间在很多地方可以直接使用:
for循环- 列表推导式
for i in 0..10 {
println(i)
}
let squares = [x^2 | x in 1..=5] // [1, 4, 9, 16, 25]
设计原因
- 区间在数学和日常编程中非常常见,用专门语法表示可以减少“魔法数字 + 比较”的样板代码。
- 与 Rust、Swift 等语言类似的
../..=设计,降低学习成本,同时在语义上精确区分“左闭右开”和“左闭右闭”。
总结
本篇从 整数、浮点、布尔、字符、字符串、元组、数组、区间 八个角度,介绍了 X 的基础数据类型设计思路:
- 在语义上尽量简单、明确(如任意精度
Integer、区间语法表达边界) - 在命名和行为上对齐主流语言的直觉,减少迁移成本
- 在表达能力上为更高层的抽象(Record、ADT、
Option/Result、集合推导等)提供扎实基础
当你在其他文档(如 x-keywords.md、x-expressions.md 和 README.md)中看到这些类型时,可以把本篇作为一个快速词典来回查它们的含义和设计理由。
X语言工具链
X语言提供了一套完整的工具链,帮助开发者高效地构建、测试和部署X语言项目。本文档详细介绍X语言的工具链组件,包括命令行工具、构建系统、测试框架和调试工具。
命令行工具
X语言的命令行工具是 x,它是一个功能强大的命令行界面,提供了丰富的命令来管理X语言项目。
基本用法
x [命令] [选项]
全局选项
--verbose或-v:使用详细输出--quiet或-q:不输出日志信息--color:控制颜色输出(auto, always, never)--directory或-C:在指定目录中运行
命令分类
构建命令
| 命令 | 描述 | 主要选项 |
|---|---|---|
build | 构建当前项目 | --release:使用release配置构建--target:构建目标平台--jobs:并行作业数--features:启用指定特性 |
run | 运行X语言程序 | --release:使用release配置--example:运行指定示例--bin:运行指定二进制目标 |
check | 检查项目语法和类型(不生成代码) | --all-targets:检查所有目标 |
compile | 编译X语言源代码到目标文件/可执行文件 | --output:指定输出文件--emit:输出中间结果--no-link:仅生成目标文件,不链接 |
test | 运行项目测试 | --release:使用release配置--lib:仅测试库--doc:运行文档测试--no-run:仅编译不运行 |
bench | 运行项目基准测试 | --no-run:仅编译不运行 |
clean | 清除构建产物 | --doc:仅清除文档--release:仅清除release目录 |
doc | 生成项目文档 | --open:在浏览器中打开--no-deps:不生成依赖的文档--document-private-items:包含私有项的文档 |
fetch | 获取依赖(不构建) | - |
fix | 自动修复代码中的警告 | --allow-dirty:允许在有未提交更改时修复--allow-staged:允许在有暂存更改时修复 |
依赖管理命令
| 命令 | 描述 | 主要选项 |
|---|---|---|
add | 添加依赖到x.toml | --dev:添加为开发依赖--build:添加为构建依赖--optional:标记为可选依赖--path:使用本地路径--git:从Git仓库添加 |
remove | 从x.toml移除依赖 | --dev:从开发依赖移除--build:从构建依赖移除 |
generate-lockfile | 生成或更新x.lock | - |
locate-project | 输出项目清单路径 | --workspace:查找工作区根 |
metadata | 以JSON格式输出项目元数据 | --no-deps:不包含依赖解析 |
pkgid | 输出完整的包标识符 | - |
tree | 显示依赖树 | --depth:最大深度--invert:反转依赖方向 |
update | 更新x.lock中的依赖版本 | --aggressive:激进更新--dry-run:试运行,不实际修改 |
vendor | 将依赖复制到本地目录 | --path:输出目录--versioned-dirs:使用带版本号的目录名 |
项目管理命令
| 命令 | 描述 | 主要选项 |
|---|---|---|
init | 在当前目录初始化新项目 | --lib:创建库项目--vcs:版本控制系统--edition:X语言版本 |
new | 创建新项目 | --lib:创建库项目--vcs:版本控制系统--edition:X语言版本 |
install | 安装X语言可执行包 | --path:从本地路径安装--git:从Git仓库安装--version:指定版本--list:列出已安装的包 |
uninstall | 卸载已安装的可执行包 | - |
search | 在注册表中搜索包 | --limit:最大结果数--registry:指定注册表 |
发布命令
| 命令 | 描述 | 主要选项 |
|---|---|---|
login | 登录到包注册表 | --registry:指定注册表 |
logout | 注销包注册表 | --registry:指定注册表 |
owner | 管理包的所有者 | --add:添加所有者--remove:移除所有者--list:列出所有者 |
package | 将项目打包为可分发的压缩包 | --list:列出将要打包的文件--no-verify:跳过验证 |
publish | 发布包到注册表 | --dry-run:试运行,不实际发布--no-verify:跳过验证 |
yank | 撤回已发布的版本 | --undo:取消撤回--registry:指定注册表 |
工具命令
| 命令 | 描述 | 主要选项 |
|---|---|---|
fmt | 格式化X语言源代码 | --check:仅检查格式,不修改--all:格式化所有文件 |
lint | 代码检查(类似clippy) | --fix:自动修复--allow:允许的lint--deny:拒绝的lint--warn:警告的lint |
repl | 启动X语言REPL | --target:REPL目标平台 |
config | 管理全局配置 | - |
version | 显示版本信息 | - |
构建系统
X语言的构建系统由 x build 命令驱动,它负责编译X语言源代码并生成可执行文件或库。构建系统的主要功能包括:
构建配置
- 开发模式:默认构建模式,包含调试信息,优化级别较低,构建速度快
- 发布模式:使用
--release选项启用,优化级别高,生成的可执行文件更小、运行更快
构建目标
X语言支持多种构建目标,可以通过 --target 选项指定。构建目标决定了生成的代码将在哪个平台上运行。
依赖管理
构建系统会自动处理项目依赖,包括:
- 解析
x.toml文件中的依赖声明 - 从注册表下载依赖包
- 构建依赖项
- 链接依赖项到最终的可执行文件或库
并行构建
通过 --jobs 选项,可以指定并行构建的作业数,加快构建速度。
特性管理
X语言支持特性标志(feature flags),允许用户选择性地启用或禁用某些功能:
--features:启用指定特性--all-features:启用所有特性--no-default-features:不使用默认特性
测试框架
X语言的测试框架由 x test 命令提供,它支持多种类型的测试:
单元测试
单元测试是最基本的测试类型,用于测试代码的各个组件是否正常工作。在X语言中,单元测试通常放在与被测试代码相同的文件中,使用 test 关键字标记。
集成测试
集成测试测试多个组件如何协同工作,通常放在项目的 tests 目录中。
文档测试
文档测试从代码文档中提取示例代码并执行它们,确保文档中的示例代码是正确的。使用 --doc 选项运行文档测试。
基准测试
基准测试用于测量代码的性能,使用 x bench 命令运行。
测试过滤
通过 x test <filter> 命令,可以只运行名称匹配指定模式的测试。
调试工具
X语言提供了多种调试工具,帮助开发者诊断和解决问题:
REPL环境
x repl 命令启动一个交互式REPL(读取-求值-打印循环)环境,允许开发者直接执行X语言代码并查看结果。REPL支持两种目标平台:
interpreter:使用X语言解释器(默认)js:编译为JavaScript并在JavaScript引擎中运行
代码格式化
x fmt 命令自动格式化X语言源代码,确保代码风格一致。它支持:
- 格式化单个文件:
x fmt <file> - 检查格式:
x fmt --check - 格式化所有文件:
x fmt --all
代码检查
x lint 命令分析X语言代码,检测潜在的问题和不良实践。它可以:
- 自动修复某些问题:
x lint --fix - 配置lint规则:
--allow、--deny、--warn
编译中间结果
x compile 命令的 --emit 选项可以输出编译过程中的中间结果,帮助开发者理解代码是如何被编译的:
tokens:词法分析结果ast:抽象语法树hir:高级中间表示pir:低级中间表示llvm-ir:LLVM中间表示
工具链集成
X语言工具链与现代开发工作流无缝集成,支持:
编辑器集成
- 与VS Code、IntelliJ等编辑器的集成
- 提供语法高亮、代码补全、错误检查等功能
CI/CD集成
- 在CI/CD流水线中使用
x check、x test等命令 - 支持自动化构建、测试和部署
包管理
- 通过
x publish发布包到X语言包注册表 - 通过
x install安装第三方包
示例工作流
新建项目
# 创建新项目
x new my-project
# 进入项目目录
cd my-project
# 构建项目
x build
# 运行项目
x run
# 运行测试
x test
# 生成文档
x doc --open
添加依赖
# 添加依赖
x add serde
# 添加开发依赖
x add --dev test-framework
# 从Git仓库添加依赖
x add --git https://github.com/example/library
发布包
# 登录到包注册表
x login
# 发布包
x publish
总结
X语言工具链是一个功能完整、易于使用的工具集合,为X语言开发者提供了从项目初始化到发布的全流程支持。通过命令行工具、构建系统、测试框架和调试工具的紧密集成,X语言工具链使得开发过程更加高效、可靠。
无论是个人项目还是大型企业应用,X语言工具链都能满足各种开发需求,帮助开发者专注于业务逻辑的实现,而不是繁琐的构建和部署流程。
语言比较
说明
本页按照 TIOBE Index 2026‑02 排名 的 前 50 名编程语言 进行整理。
对每门语言,统一给出:
- 1. 简介:定位、典型应用场景与主要特性
- 2. 关键字列表及示例:几类代表性关键字 / 语句,说明其作用,并给出简单示例
此外,大多数语言还共享一套 常见运算符(算术、比较、逻辑、赋值等),下面先给出一个跨语言的运算符速查表,后续在各小节的代码中也会自然出现这些运算符的用法。
通用运算符速查(跨语言)
- 算术运算符
+:加法(如a + b)-:减法或一元取负(如a - b、-a)*:乘法(如a * b)/:除法(如a / b)%:取余(模运算,如a % b)
- 比较运算符
==/=:相等(部分语言使用==,部分 SQL 方言使用=)!=/<>:不等(C 系一族是!=,SQL 常见<>)<、>、<=、>=:大小比较
- 逻辑运算符
- C/Java/JavaScript 等:
&&(与)、||(或)、!(非) - Python / SQL / X 语言等更偏向可读性的语言:
and、or、not
- C/Java/JavaScript 等:
- 自增 / 自减与复合赋值(部分语言支持)
++a、a++、--a、a--:自增 / 自减(C、C++、Java、C#、JavaScript 等)+=、-=、*=、/=、%=:在原值基础上进行运算并赋值回去
- 位运算(主要出现在 C 家族、Java、C#、Rust 等)
&(按位与)、|(按位或)、^(按位异或)、~(按位取反)<<、>>:左移 / 右移
典型对比示例:
-
Python(关键字风格逻辑运算,适合阅读)
x = 10 + 2 * 3 # 算术 if x > 10 and x < 20: print("in range") -
C(符号风格逻辑运算,接近硬件)
int x = 10 + 2 * 3; if (x > 10 && x < 20) { printf("in range\n"); } -
JavaScript(符号逻辑 + 复合赋值)
let x = 10; x += 2; // x = x + 2 if (x >= 10 && x <= 20) { console.log("in range"); }
代码示例偏向“入门级直观理解”,不追求覆盖全部语法细节。
1. Python
1. 简介
Python 是目前最流行的通用高级语言之一,语法简洁、生态庞大,广泛用于数据科学、机器学习、Web 开发、自动化脚本、运维等。
2. 关键字列表及示例
def/return:定义函数和返回值。
def add(a, b):
return a + b
print(add(1, 2))
if/elif/else:条件分支。
x = 10
if x > 10:
print("big")
elif x == 10:
print("equal")
else:
print("small")
for/while/break/continue:循环控制。
for i in range(3):
if i == 1:
continue
print(i)
try/except/finally/raise:异常处理。
def safe_div(a, b):
try:
return a / b
except ZeroDivisionError:
return None
finally:
print("done")
if safe_div(1, 0) is None:
raise ValueError("division failed")
import/from/as:模块导入。
import math
from collections import Counter as C
print(math.sqrt(4))
print(C("abca"))
2. C
1. 简介
C 是经典的过程式系统编程语言,广泛用于操作系统、嵌入式、编译器和高性能库等底层场景。
2. 关键字列表及示例
int/char/double等:基本类型。
int x = 10;
double y = 3.14;
char c = 'A';
if/else/switch:条件控制。
int n = 2;
switch (n) {
case 1:
printf("one\n");
break;
case 2:
printf("two\n");
break;
default:
printf("other\n");
}
for/while/do:循环。
for (int i = 0; i < 3; i++) {
printf("%d\n", i);
}
struct/typedef:结构体与类型别名。
typedef struct {
int x;
int y;
} Point;
Point p = { .x = 1, .y = 2 };
3. C++
1. 简介
C++ 在 C 的基础上加入面向对象、泛型和现代抽象机制,是系统软件、游戏引擎、高性能库等领域的主力语言。
2. 关键字列表及示例
class/public/private:类与访问控制。
class Person {
public:
explicit Person(std::string name) : name_(std::move(name)) {}
void greet() const { std::cout << "Hello, " << name_ << "\n"; }
private:
std::string name_;
};
template/typename:模板与泛型。
template <typename T>
T add(T a, T b) {
return a + b;
}
auto/constexpr:类型推断与编译期常量。
constexpr int square(int x) { return x * x; }
auto v = square(4);
4. Java
1. 简介
Java 是面向对象的通用语言,运行在 JVM 上,适合大型企业应用、Android 开发和后端服务。
2. 关键字列表及示例
class/interface:类与接口。
public interface Greet {
void greet();
}
public class Person implements Greet {
private final String name;
public Person(String name) { this.name = name; }
@Override
public void greet() {
System.out.println("Hello, " + name);
}
}
public/private/static/final:访问控制与修饰符。
public final class MathUtil {
private MathUtil() {}
public static int add(int a, int b) {
return a + b;
}
}
try/catch/finally/throw/throws:异常处理。
public int parse(String s) throws NumberFormatException {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw e;
} finally {
System.out.println("done");
}
}
5. C#
1. 简介
C# 是 .NET 平台上的现代面向对象语言,支持泛型、LINQ、async/await 等特性,广泛用于桌面、Web、云服务和游戏(Unity)。
2. 关键字列表及示例
class/interface/struct:引用类型与值类型。
public interface IGreet {
void Greet();
}
public class Person : IGreet {
public string Name { get; }
public Person(string name) => Name = name;
public void Greet() => Console.WriteLine($"Hello, {Name}");
}
var/async/await:类型推断与异步。
public async Task<string> FetchAsync(HttpClient client) {
var res = await client.GetStringAsync("https://example.com");
return res;
}
using:资源自动释放(IDisposable)。
using var file = File.OpenText("data.txt");
Console.WriteLine(file.ReadLine());
6. JavaScript
1. 简介
JavaScript 最初是浏览器脚本语言,如今已发展为可在浏览器、Node.js、Deno 等环境中运行的全栈语言。
2. 关键字列表及示例
let/const/var:变量和常量声明。
let x = 1; // 块级作用域,可重新赋值
const y = 2; // 块级作用域,不可重新赋值
var z = 3; // 函数作用域
function/=>:函数声明与箭头函数。
function add(a, b) {
return a + b;
}
const mul = (a, b) => a * b;
async/await:异步编程。
async function fetchData() {
const res = await fetch("/api/data");
const json = await res.json();
console.log(json);
}
7. Visual Basic
1. 简介
这里的 Visual Basic 主要指 Visual Basic .NET,是 .NET 平台上的基于 Basic 语法的语言,常用于 Windows 桌面和业务系统开发。
2. 关键字列表及示例
Module/Sub/Function:模块与过程。
Module Program
Sub Main()
Console.WriteLine(Add(1, 2))
End Sub
Function Add(a As Integer, b As Integer) As Integer
Return a + b
End Function
End Module
If/Then/Else/End If:条件控制。
If x > 10 Then
Console.WriteLine("big")
Else
Console.WriteLine("small")
End If
8. R
1. 简介
R 是面向统计分析和数据可视化的语言,拥有丰富的统计模型和绘图库,在学术研究和数据科学中广泛使用。
2. 关键字列表及示例
function:函数定义。
add <- function(a, b) {
a + b
}
add(1, 2)
if/else/for/while:控制流。
for (i in 1:3) {
print(i)
}
9. SQL
1. 简介
SQL(Structured Query Language)是关系型数据库的标准查询语言,用于定义表结构和操作数据。
2. 常见语句及示例(类比关键字)
SELECT/FROM/WHERE:查询。
SELECT id, name
FROM users
WHERE active = 1;
INSERT/UPDATE/DELETE:写入与修改。
INSERT INTO users (name) VALUES ('Alice');
CREATE TABLE/ALTER TABLE/DROP TABLE:表结构。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
10. Delphi / Object Pascal
1. 简介
Delphi 使用 Object Pascal 语言,侧重于快速构建 Windows 桌面和数据库应用,也支持跨平台开发。
2. 关键字列表及示例
program/begin/end:程序入口。
program Hello;
begin
Writeln('Hello, world');
end.
type/class:类型与类。
type
TPerson = class
private
FName: string;
public
constructor Create(const AName: string);
procedure Greet;
end;
11. Perl
1. 简介
Perl 以强大的文本处理和正则表达式能力著称,早期常用于 CGI 脚本、系统管理和日志分析。
2. 关键字列表及示例
my/sub:变量与子例程。
use strict;
use warnings;
sub add {
my ($a, $b) = @_;
return $a + $b;
}
print add(1, 2);
if/elsif/else/for/foreach:控制流。
for my $x (1..3) {
print "$x\n";
}
12. Fortran
1. 简介
Fortran 是最早的高级语言之一,仍在数值计算、工程仿真和科学计算中广泛使用。
2. 关键字列表及示例
program/end program:程序入口。
program hello
print *, "Hello, world"
end program hello
do/if:循环与条件。
do i = 1, 3
print *, i
end do
13. PHP
1. 简介
PHP 是主要用于服务端 Web 开发的脚本语言,可嵌入 HTML,生态中有 Laravel、Symfony 等框架。
2. 关键字列表及示例
function:定义函数。
<?php
function add(int $a, int $b): int {
return $a + $b;
}
echo add(1, 2);
class/interface:类与接口。
<?php
interface Greet {
public function greet(): void;
}
class Person implements Greet {
public function __construct(private string $name) {}
public function greet(): void {
echo "Hello, {$this->name}";
}
}
14. Rust
1. 简介
Rust 是强调内存安全与并发安全的系统编程语言,通过所有权和借用系统在无 GC 的情况下避免常见内存错误。
2. 关键字列表及示例
fn/let/mut:函数与绑定。
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
a + b
}
let mut x = 10;
x += 1;
}
struct/enum/impl:数据类型与方法。
#![allow(unused)]
fn main() {
struct Point {
x: i32,
y: i32,
}
impl Point {
fn len2(&self) -> i32 {
self.x * self.x + self.y * self.y
}
}
}
15. Scratch
1. 简介
Scratch 是面向儿童和初学者的图形化编程语言,通过拖拽积木块构建程序,常用于编程启蒙教育。
2. 常见积木(类比关键字)
- 控制类积木:
重复执行、如果…那么、等待等。 - 事件类积木:如“当绿旗被点击时”作为程序入口。
(Scratch 是图形化环境,这里不以文本代码展示示例。)
16. Go
1. 简介
Go(Golang)是 Google 设计的静态类型语言,内置 goroutine 与 channel 并发模型,适合云原生服务和工具开发。
2. 关键字列表及示例
func:函数。
func Add(a, b int) int {
return a + b
}
go/chan/select:并发与通道。
func worker(ch chan int) {
ch <- 42
}
func main() {
ch := make(chan int)
go worker(ch)
v := <-ch
println(v)
}
17. Ada
1. 简介
Ada 是为高可靠性和安全关键系统(航空航天、军工等)设计的强类型语言,强调可读性和并发。
2. 关键字列表及示例
procedure/is/begin/end:过程定义。
procedure Hello is
begin
Put_Line("Hello, world");
end Hello;
type/record:自定义类型。
type Point is record
X : Integer;
Y : Integer;
end record;
18. MATLAB
1. 简介
MATLAB 是面向矩阵运算和数值分析的商业语言与环境,在工程、控制、信号处理等领域广泛使用。
2. 关键字列表及示例
function:函数定义。
function y = add(a, b)
y = a + b;
end
for/if:控制流。
for i = 1:3
disp(i);
end
19. Assembly language
1. 简介
汇编语言是紧贴硬件指令集的低级语言,不同 CPU 架构有不同语法,常用于性能关键或直接硬件控制场景。
2. 常见指令及示例(x86 伪代码)
MOV/ADD/JMP:数据移动、算术和跳转。
MOV AX, 1
ADD AX, 2
JMP done
done:
20. Kotlin
1. 简介
Kotlin 是 JetBrains 设计的现代静态类型语言,可编译到 JVM、Android、Native 和 JavaScript,是 Android 官方首选语言之一。
2. 关键字列表及示例
val/var:不可变 / 可变变量。
val name = "Alice" // 只读
var age = 18 // 可变
age += 1
fun:函数。
fun add(a: Int, b: Int): Int = a + b
fun main() {
println(add(1, 2))
}
data class/object:数据类和单例。
data class User(val id: Int, val name: String)
object Config {
const val Version = "1.0"
}
21. Swift
1. 简介
Swift 是 Apple 推出的现代静态类型语言,用于 iOS、macOS、watchOS、tvOS 等平台开发。
2. 关键字列表及示例
let/var:常量与变量。
let name = "Alice"
var age = 18
age += 1
struct/class/enum:类型定义。
struct Point {
var x: Int
var y: Int
}
if/guard/switch:控制流。
func greet(_ name: String?) {
guard let name else { return }
print("Hello, \(name)")
}
22. COBOL
1. 简介
COBOL 是为商业数据处理设计的老牌语言,在金融、保险等大型机系统中仍有大量遗留代码。
2. 关键字列表及示例
IDENTIFICATION DIVISION/PROCEDURE DIVISION:程序结构。
IDENTIFICATION DIVISION.
PROGRAM-ID. HELLO.
PROCEDURE DIVISION.
DISPLAY "Hello, world".
STOP RUN.
23. Classic Visual Basic
1. 简介
Classic Visual Basic 通常指 VB6 及更早版本,主要用于早期 Windows 桌面应用开发。
2. 关键字列表及示例
Sub/Function:过程与函数。
Sub Hello()
MsgBox "Hello"
End Sub
If/Then/Else:条件控制。
If x > 10 Then
MsgBox "big"
Else
MsgBox "small"
End If
24. Prolog
1. 简介
Prolog 是逻辑编程语言,基于事实和规则,通过“查询”让系统推理出答案,常用于人工智能和知识表示。
2. 关键字与结构示例
- 事实与规则:
:-表示蕴含。
parent(alice, bob).
parent(bob, carol).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
25. Ruby
1. 简介
Ruby 是语法优雅、强调开发者愉悦度的动态语言,Ruby on Rails 框架在 Web 开发领域影响深远。
2. 关键字列表及示例
def/end:定义方法。
def add(a, b)
a + b
end
puts add(1, 2)
class/module:类与模块。
module Greeting
def greet
puts "Hello, #{@name}"
end
end
class Person
include Greeting
def initialize(name)
@name = name
end
end
26. Dart
1. 简介
Dart 是 Google 推出的语言,常与 Flutter 一起用于跨平台移动、Web 和桌面应用开发。
2. 关键字列表及示例
var/final/const:变量与常量。
var x = 1; // 可变
final y = 2; // 运行期常量
const z = 3; // 编译期常量
class/extends/implements:OOP。
class Person {
final String name;
Person(this.name);
void greet() => print('Hello, $name');
}
27. Lua
1. 简介
Lua 是轻量级脚本语言,常嵌入游戏引擎和应用程序,用作配置和扩展语言。
2. 关键字列表及示例
function/local:函数与局部变量。
local function add(a, b)
return a + b
end
print(add(1, 2))
if/elseif/else/end:条件。
local x = 10
if x > 10 then
print("big")
elseif x == 10 then
print("equal")
else
print("small")
end
28. SAS
1. 简介
SAS 是商业统计分析系统和语言,常用于数据仓库、商业智能和医疗统计。
2. 关键字与示例
DATA/SET/RUN:数据步。
DATA work.sample;
SET work.source;
RUN;
PROC:过程分析,如PROC MEANS。
PROC MEANS DATA=work.sample;
RUN;
29. Julia
1. 简介
Julia 是为数值和科学计算设计的高性能动态语言,兼具易用性和接近 C 的速度。
2. 关键字列表及示例
function/end:函数。
function add(a, b)
a + b
end
println(add(1, 2))
struct/mutable struct:结构体。
struct Point
x::Int
y::Int
end
30. Lisp
1. 简介
Lisp 是历史悠久的函数式语言家族,特点是 S 表达式、宏系统和强大的元编程能力。
2. 关键字列表及示例(以 Common Lisp 风格为例)
defun/let:定义函数和局部绑定。
(defun add (a b)
(+ a b))
(let ((x 1) (y 2))
(print (add x y)))
31. Objective-C
1. 简介
Objective-C 在 C 的基础上加入 Smalltalk 风格消息机制,曾是 macOS / iOS 开发的主要语言。
2. 关键字列表及示例
@interface/@implementation:类声明与实现。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)greet;
@end
@implementation Person
- (void)greet {
NSLog(@"Hello, %@", self.name);
}
@end
@autoreleasepool:自动释放池。
@autoreleasepool {
Person *p = [Person new];
p.name = @"Alice";
[p greet];
}
32. TypeScript
1. 简介
TypeScript 是 JavaScript 的超集,引入静态类型和语言扩展,最终编译为普通 JavaScript。
2. 关键字列表及示例
type/interface:类型别名与接口。
type ID = number | string;
interface User {
id: ID;
name: string;
}
enum/implements/extends:枚举与继承。
enum Direction { Up, Down, Left, Right }
interface Greet { greet(): void; }
class Person implements Greet {
constructor(private name: string) {}
greet() {
console.log(`Hello, ${this.name}`);
}
}
33. PL/SQL
1. 简介
PL/SQL 是 Oracle 数据库的过程化扩展 SQL,用于在数据库内部编写存储过程、函数和触发器。
2. 关键字与示例
DECLARE/BEGIN/END:块结构。
DECLARE
v_sum NUMBER;
BEGIN
v_sum := 1 + 2;
DBMS_OUTPUT.PUT_LINE(v_sum);
END;
34. VBScript
1. 简介
VBScript 是基于 Visual Basic 语法的脚本语言,曾常用于 Windows 脚本和早期 IE 浏览器脚本。
2. 关键字列表及示例
Dim/Sub/Function。
Dim x
x = 1
Sub Hello()
MsgBox "Hello"
End Sub
35. Haskell
1. 简介
Haskell 是纯函数式语言,具有惰性求值和强类型系统,适合抽象表达和形式化推理。
2. 关键字列表及示例
data/type:代数数据类型。
data Direction = Up | Down | Left | Right
type Name = String
let/where:局部绑定。
area r = pi * r2
where r2 = r * r
36. Erlang
1. 简介
Erlang 是为电信和高并发系统设计的函数式语言,提供轻量级进程和消息传递模型。
2. 关键字列表及示例
receive/fun/case。
loop() ->
receive
{From, Msg} ->
From ! {ok, Msg},
loop()
end.
37. Ladder Logic
1. 简介
梯形图(Ladder Logic)是一种用于 PLC(可编程逻辑控制器)的图形化编程语言,外观类似电气继电器电路。
2. 常见结构(概念性说明)
- 触点(常开 / 常闭):表示输入条件。
- 线圈:表示输出动作。
(梯形图主要以图形形式编辑,一般不使用文本关键字表示。)
38. (Visual) FoxPro
1. 简介
Visual FoxPro 是微软推出的面向数据的编程语言和数据库系统,曾在桌面数据库应用中流行。
2. 关键字与示例
SELECT/FROM/WHERE:内建数据库查询语句。
SELECT name FROM users WHERE active = .T.
39. Scala
1. 简介
Scala 结合面向对象与函数式编程,运行在 JVM 上,常用于数据处理(如 Spark)、分布式系统和后端服务。
2. 关键字列表及示例
object/class/trait。
trait Greet {
def greet(): Unit
}
class Person(name: String) extends Greet {
def greet(): Unit = println(s"Hello, $name")
}
object Main {
def main(args: Array[String]): Unit = {
new Person("Alice").greet()
}
}
val/var:不可变 / 可变变量。
val x = 1
var y = 2
y += 1
40. LabVIEW
1. 简介
LabVIEW 是图形化编程环境和语言,主要用于测试测量、数据采集和工业控制领域。
2. 常见结构
- 虚拟仪器(VI):图形化函数单元。
- 数据流连线:表示数据依赖关系。
(LabVIEW 程序以图形方式构建,不以文本关键字为主。)
41. PowerShell
1. 简介
PowerShell 是基于 .NET 的命令行外壳与脚本语言,使用对象管道,擅长系统管理与自动化。
2. 关键字与示例
function:定义函数。
function Add($a, $b) {
$a + $b
}
Add 1 2
if/elseif/else:条件。
if ($x -gt 10) {
"big"
} elseif ($x -eq 10) {
"equal"
} else {
"small"
}
42. Transact-SQL
1. 简介
Transact-SQL(T‑SQL)是 Microsoft SQL Server 等使用的 SQL 扩展,加入了过程化控制结构。
2. 关键字与示例
DECLARE/BEGIN/END/IF。
DECLARE @x INT = 10;
IF @x > 5
BEGIN
PRINT 'big';
END;
43. X++
1. 简介
X++ 是 Microsoft Dynamics 365 Finance and Operations 等产品使用的语言,语法类似 C#,用于业务逻辑与数据访问。
2. 关键字与示例(概念性)
class/static/void。
class Hello {
public static void main(Args _args) {
info("Hello, world");
}
}
44. ABAP
1. 简介
ABAP 是 SAP 系统中的主要编程语言,用于编写业务逻辑、报表和扩展。
2. 关键字与示例
REPORT/WRITE。
REPORT zhello.
WRITE 'Hello, world'.
SELECT:数据库访问。
SELECT * FROM mara INTO TABLE @DATA(lt_mara) UP TO 10 ROWS.
45. Elixir
1. 简介
Elixir 构建在 Erlang VM(BEAM)之上,是一门函数式语言,适合高并发 Web 服务和实时系统。
2. 关键字列表及示例
defmodule/def/defp。
defmodule Greeter do
def greet(name) do
IO.puts("Hello, #{name}")
end
end
case/ 模式匹配。
case {:ok, 42} do
{:ok, value} -> IO.puts(value)
:error -> :noop
end
46. Zig
1. 简介
Zig 是新兴的系统编程语言,主打可预测性能、简洁语义和手动内存管理,常与 C 互操作。
2. 关键字列表及示例
fn/var/const。
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() !void {
const stdout = std.io.getStdOut().writer();
try stdout.print("{}\n", .{add(1, 2)});
}
47. ActionScript
1. 简介
ActionScript 是基于 ECMAScript 的语言,主要用于 Adobe Flash 平台上的交互内容和动画。
2. 关键字列表及示例
function/var/class。
package {
public class Main {
public function Main() {
trace(add(1, 2));
}
}
}
function add(a:int, b:int):int {
return a + b;
}
48. D
1. 简介
D 语言结合了 C++ 的性能和更现代的语法特性,支持系统编程和高层抽象。
2. 关键字列表及示例
module/import/auto。
import std.stdio;
auto add(int a, int b) {
return a + b;
}
void main() {
writeln(add(1, 2));
}
49. Logo
1. 简介
Logo 是面向教育的编程语言,以“海龟绘图”著称,常用于儿童编程启蒙。
2. 常见命令(类比关键字)
FORWARD/BACK/LEFT/RIGHT:控制海龟移动。
FORWARD 100
RIGHT 90
FORWARD 100
50. PL/I
1. 简介
PL/I 是面向科学计算和商业应用的多用途语言,在大型机环境中使用。
2. 关键字与示例
DECLARE/PROC/END。
HELLO: PROC OPTIONS(MAIN);
PUT SKIP LIST('Hello, world');
END HELLO;
以上即 TIOBE Index 2026‑02 前 50 名编程语言的简介与关键字示例,后续如排名变化,可以按相同结构更新本页面中的顺序与内容。
layout: page title: 编程语言列表(示例与关键字)
说明
本文档基于 Languish 的语言列表,数据来源参考:
总共约有 544 种编程语言。由于完整覆盖所有语言会非常庞大,本页面:
- 为主流语言提供较完整的内容示例
- 给出统一的章节结构与模板
- 方便后续按需增补其他语言
建议为每种语言使用如下结构:
- 1. 简介
- 2. 关键字列表
- 关键字名称
- 功能说明
- 简短代码示例
下面先给出若干主流语言的完整示例。
Python
1. 简介
Python 是一种强调可读性和快速开发的高级通用编程语言,广泛用于数据分析、机器学习、Web 开发、脚本自动化等领域。它拥有丰富的标准库和第三方生态。
2. 关键字列表及示例
def:定义函数。
def add(a, b):
return a + b
print(add(1, 2))
class:定义类。
class Person:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, {self.name}")
Person("Alice").greet()
if/elif/else:条件分支。
x = 10
if x > 10:
print("large")
elif x == 10:
print("equal")
else:
print("small")
for/while:循环。
for i in range(3):
print(i)
count = 0
while count < 3:
print(count)
count += 1
try/except/finally/raise:异常处理。
def safe_div(a, b):
try:
return a / b
except ZeroDivisionError:
return None
finally:
print("done")
if safe_div(1, 0) is None:
raise ValueError("division failed")
with:上下文管理(资源自动管理)。
with open("data.txt", "w", encoding="utf-8") as f:
f.write("hello")
import/from/as:模块导入。
import math
from collections import Counter as C
print(math.sqrt(4))
print(C("abca"))
JavaScript
1. 简介
JavaScript 最初是浏览器脚本语言,现在已经发展为全栈通用语言,可运行在浏览器、Node.js、Deno 等多种环境中。
2. 关键字列表及示例
let/const/var:变量和常量声明。
let x = 1; // 块级作用域,可重新赋值
const y = 2; // 块级作用域,不可重新赋值
var z = 3; // 函数作用域
function:函数声明。
function add(a, b) {
return a + b;
}
console.log(add(1, 2));
if/else if/else:条件分支。
const score = 85;
if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else {
console.log("C");
}
for/while/do...while:循环。
for (let i = 0; i < 3; i++) {
console.log(i);
}
let n = 0;
while (n < 3) {
console.log(n);
n++;
}
switch:多分支选择。
const day = 2;
switch (day) {
case 1:
console.log("Mon");
break;
case 2:
console.log("Tue");
break;
default:
console.log("Other");
}
try/catch/finally/throw:异常处理。
function safeParse(json) {
try {
return JSON.parse(json);
} catch (e) {
return null;
} finally {
console.log("parsed");
}
}
if (!safeParse("not json")) {
throw new Error("invalid json");
}
async/await:异步函数与等待 Promise。
async function fetchData() {
const res = await fetch("/api/data");
const json = await res.json();
console.log(json);
}
fetchData();
TypeScript
1. 简介
TypeScript 是 JavaScript 的超集,在 JS 基础上增加了静态类型系统和一些语言扩展特性,最终编译为普通 JavaScript 运行。
2. 关键字列表及示例
type/interface:类型别名与接口。
type ID = number | string;
interface User {
id: ID;
name: string;
}
const u: User = { id: 1, name: "Alice" };
enum:枚举类型。
enum Direction {
Up,
Down,
Left,
Right,
}
function move(dir: Direction) {
console.log("move", dir);
}
move(Direction.Up);
implements/extends:接口实现与类继承。
interface Greet {
greet(): void;
}
class Person implements Greet {
constructor(private name: string) {}
greet() {
console.log(`Hello, ${this.name}`);
}
}
class Student extends Person {
constructor(name: string, public grade: number) {
super(name);
}
}
public/private/protected/readonly:成员可见性修饰符。
class Counter {
private count = 0;
readonly name = "counter";
public inc() {
this.count++;
}
}
Java
1. 简介
Java 是面向对象的通用编程语言,运行在 JVM 上,广泛用于企业级应用、Android 开发和大规模后端系统。
2. 关键字列表及示例
class/interface:类与接口。
public class Person implements Greet {
private String name;
public Person(String name) {
this.name = name;
}
@Override
public void greet() {
System.out.println("Hello, " + name);
}
}
public/private/protected/static/final:访问控制和修饰符。
public final class MathUtil {
private MathUtil() {}
public static int add(int a, int b) {
return a + b;
}
}
if/else/switch:条件与分支。
int score = 85;
if (score >= 90) {
System.out.println("A");
} else if (score >= 80) {
System.out.println("B");
} else {
System.out.println("C");
}
try/catch/finally/throw/throws:异常处理。
public int parse(String s) throws NumberFormatException {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw e;
} finally {
System.out.println("done");
}
}
for/while/do:循环。
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
C#
1. 简介
C# 是由微软主导设计的现代面向对象语言,运行在 .NET 平台上,适用于桌面、Web、移动、游戏(Unity)等多种场景。
2. 关键字列表及示例
class/interface/struct:类型定义。
public interface IGreet {
void Greet();
}
public class Person : IGreet {
public string Name { get; }
public Person(string name) => Name = name;
public void Greet() => Console.WriteLine($"Hello, {Name}");
}
var/dynamic:类型推断与动态类型。
var x = 10; // 编译期推断类型为 int
dynamic y = "hi"; // 运行期决定成员解析
async/await:异步编程。
public async Task<string> FetchAsync(HttpClient client) {
var res = await client.GetStringAsync("https://example.com");
return res;
}
using:资源自动释放(IDisposable)。
using var file = File.OpenText("data.txt");
Console.WriteLine(file.ReadLine());
C
1. 简介
C 是一种过程式系统级编程语言,广泛用于操作系统、嵌入式、编译器和高性能系统软件。
2. 关键字列表及示例
int/char/float/double:基本类型关键字。
int x = 10;
double y = 3.14;
if/else/switch:条件控制。
int n = 2;
switch (n) {
case 1:
printf("one\n");
break;
case 2:
printf("two\n");
break;
default:
printf("other\n");
}
for/while/do:循环。
for (int i = 0; i < 3; i++) {
printf("%d\n", i);
}
struct/typedef:结构体与类型别名。
typedef struct {
int x;
int y;
} Point;
Point p = { .x = 1, .y = 2 };
C++
1. 简介
C++ 在 C 基础上加入了面向对象、泛型与现代抽象机制,同时保留了对底层资源的精细控制能力。
2. 关键字列表及示例
class/struct/public/private:面向对象与访问控制。
class Person {
public:
explicit Person(std::string name) : name_(std::move(name)) {}
void greet() const { std::cout << "Hello, " << name_ << "\n"; }
private:
std::string name_;
};
template/typename:模板与泛型。
template <typename T>
T add(T a, T b) {
return a + b;
}
auto/constexpr/inline:类型推断与编译期计算。
constexpr int square(int x) { return x * x; }
auto v = square(4);
Go
1. 简介
Go(Golang)是 Google 设计的静态类型、编译型语言,强调简单、高并发和快速编译,内置 goroutine 和 channel 并发模型。
2. 关键字列表及示例
func:函数或方法定义。
func Add(a, b int) int {
return a + b
}
go/chan/select:轻量级并发与通道。
func worker(ch chan int) {
ch <- 42
}
func main() {
ch := make(chan int)
go worker(ch)
v := <-ch
println(v)
}
defer:延迟执行(通常用于资源释放)。
func readFile() {
f, _ := os.Open("data.txt")
defer f.Close()
}
Rust
1. 简介
Rust 是一门强调内存安全和并发安全的系统编程语言,通过所有权和借用系统在无需垃圾回收的前提下防止数据竞争和悬垂指针。
2. 关键字列表及示例
fn/let/mut:函数与变量绑定。
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 {
a + b
}
let mut x = 10;
x += 1;
}
struct/enum/impl:数据类型与方法实现。
#![allow(unused)]
fn main() {
struct Point {
x: i32,
y: i32,
}
impl Point {
fn len2(&self) -> i32 {
self.x * self.x + self.y * self.y
}
}
}
match:模式匹配。
#![allow(unused)]
fn main() {
let value = Some(10);
match value {
Some(v) if v > 5 => println!("big: {v}"),
Some(v) => println!("small: {v}"),
None => println!("none"),
}
}
async/await:异步。
#![allow(unused)]
fn main() {
async fn fetch() -> Result<(), reqwest::Error> {
let body = reqwest::get("https://example.com").await?.text().await?;
println!("{body}");
Ok(())
}
}
HTML(超文本标记语言)
严格来说 HTML 不是“编程语言”,但在许多统计与趋势网站(包括 Languish)中常被列入语言列表。
1. 简介
HTML 是用于描述 Web 页面结构的标记语言,由标签(tag)和属性(attribute)组成,配合 CSS 和 JavaScript 构建完整的 Web 应用。
2. 常见标签及作用示例(类比关键字)
<html>/<head>/<body>:文档结构。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<title>示例</title>
</head>
<body>
<h1>你好</h1>
</body>
</html>
<div>/<span>:块级 / 行内通用容器。
<div class="card">
<span class="title">标题</span>
</div>
<a>/<img>/<button>:超链接、图片、按钮。
<a href="https://example.com">链接</a>
<img src="logo.png" alt="Logo" />
<button type="button">点击</button>
PHP
1. 简介
PHP 是一种主要用于服务端 Web 开发的脚本语言,嵌入 HTML 使用,生态中有 Laravel、Symfony 等主流框架。
2. 关键字列表及示例
function:定义函数。
<?php
function add(int $a, int $b): int {
return $a + $b;
}
echo add(1, 2);
class/interface:定义类与接口。
<?php
interface Greet {
public function greet(): void;
}
class Person implements Greet {
public function __construct(private string $name) {}
public function greet(): void {
echo "Hello, {$this->name}";
}
}
if/elseif/else/foreach:条件与循环。
<?php
$nums = [1, 2, 3];
foreach ($nums as $n) {
if ($n % 2 === 0) {
echo "even\n";
} else {
echo "odd\n";
}
}
Ruby
1. 简介
Ruby 是一门强调开发者愉悦度、语法优雅的动态语言,常用于 Web 开发(如 Ruby on Rails)、脚本和自动化。
2. 关键字列表及示例
def/end:定义方法。
def add(a, b)
a + b
end
puts add(1, 2)
class/module:类与模块。
module Greeting
def greet
puts "Hello, #{@name}"
end
end
class Person
include Greeting
def initialize(name)
@name = name
end
end
if/elsif/else/unless:条件控制。
x = 10
if x > 10
puts "big"
elsif x == 10
puts "equal"
else
puts "small"
end
puts "ok" unless x < 0
Kotlin
1. 简介
Kotlin 是 JetBrains 设计的静态类型语言,可编译到 JVM、Android、Native 和 JavaScript,是现代 Android 开发的官方首选语言之一。
2. 关键字列表及示例
val/var:不可变 / 可变变量。
val name = "Alice" // 只读
var age = 18 // 可变
age += 1
fun:函数与方法定义。
fun add(a: Int, b: Int): Int = a + b
fun main() {
println(add(1, 2))
}
data class/object/companion object:数据类与单例。
data class User(val id: Int, val name: String)
object Config {
const val Version = "1.0"
}
Swift
1. 简介
Swift 是 Apple 推出的现代静态类型语言,用于 iOS、macOS、watchOS、tvOS 等平台的应用开发。
2. 关键字列表及示例
let/var:常量与变量。
let name = "Alice"
var age = 18
age += 1
struct/class/enum:值类型、引用类型和枚举。
struct Point {
var x: Int
var y: Int
}
enum Direction {
case up, down, left, right
}
if/guard/switch:控制流。
func greet(_ name: String?) {
guard let name else { return }
print("Hello, \(name)")
}
Dart
1. 简介
Dart 是 Google 推出的语言,常与 Flutter 框架一起用于跨平台移动、Web 与桌面应用开发。
2. 关键字列表及示例
var/final/const:变量与常量声明。
var x = 1; // 可变
final y = 2; // 运行期常量
const z = 3; // 编译期常量
class/extends/implements:面向对象。
class Person {
final String name;
Person(this.name);
void greet() => print('Hello, $name');
}
Scala
1. 简介
Scala 结合了面向对象与函数式编程特性,运行在 JVM 上,常用于数据处理、分布式系统和后端服务。
2. 关键字列表及示例
object/class/trait:单例对象、类与特质。
trait Greet {
def greet(): Unit
}
class Person(name: String) extends Greet {
def greet(): Unit = println(s"Hello, $name")
}
object Main {
def main(args: Array[String]): Unit = {
new Person("Alice").greet()
}
}
val/var:不可变与可变变量。
val x = 1
var y = 2
y += 1
Haskell
1. 简介
Haskell 是纯函数式编程语言,具有惰性求值和强大的类型系统,适合抽象表达和形式化推理。
2. 关键字列表及示例
data/type:代数数据类型与类型别名。
data Direction = Up | Down | Left | Right
type Name = String
let/where:局部绑定。
area r = pi * r2
where r2 = r * r
Elixir
1. 简介
Elixir 是一种构建在 Erlang VM(BEAM)上的函数式语言,强调并发、容错和分布式,常用于 Web 服务与实时系统。
2. 关键字列表及示例
def/defp/defmodule:定义函数与模块。
defmodule Greeter do
def greet(name) do
IO.puts("Hello, #{name}")
end
end
case/with:模式匹配控制流。
case {:ok, 42} do
{:ok, value} -> IO.puts(value)
:error -> :noop
end
Erlang
1. 简介
Erlang 是为高并发、电信系统设计的函数式语言,提供轻量级进程和消息传递模型。
2. 关键字列表及示例
fun/case/receive:匿名函数、匹配与消息收发。
loop() ->
receive
{From, Msg} ->
From ! {ok, Msg},
loop()
end.
R
1. 简介
R 是面向统计计算和数据可视化的语言,广泛用于数据分析、统计建模和科研。
2. 关键字列表及示例
function:定义函数。
add <- function(a, b) {
a + b
}
add(1, 2)
if/else/for/while:控制流。
for (i in 1:3) {
print(i)
}
Julia
1. 简介
Julia 是为数值计算和科学计算设计的高性能动态语言,兼具易用语法和接近 C 的速度。
2. 关键字列表及示例
function/end:函数。
function add(a, b)
a + b
end
println(add(1, 2))
struct/mutable struct:不可变 / 可变结构体。
struct Point
x::Int
y::Int
end
SQL(结构化查询语言)
1. 简介
SQL 是用于关系型数据库的查询与数据操作语言,几乎所有主流关系数据库(MySQL、PostgreSQL、SQLite 等)都支持。
2. 常见语句及示例(类比关键字)
SELECT/FROM/WHERE:查询数据。
SELECT id, name
FROM users
WHERE active = 1;
INSERT/UPDATE/DELETE:修改数据。
INSERT INTO users (name) VALUES ('Alice');
CREATE TABLE/ALTER TABLE/DROP TABLE:定义表结构。
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
Bash(Shell 脚本)
1. 简介
Bash 是类 Unix 系统最常用的 Shell 之一,同时也是脚本语言,用于系统管理、自动化任务等。
2. 关键字与常用语法示例
if/then/elif/else/fi:条件。
if [ "$1" -gt 10 ]; then
echo "big"
else
echo "small"
fi
for/while/do/done:循环。
for f in *.txt; do
echo "$f"
done
PowerShell
1. 简介
PowerShell 是基于 .NET 的命令行外壳与脚本语言,使用管道传递对象而非纯文本,擅长系统管理与自动化。
2. 关键字与常用语法示例
function:定义函数。
function Add($a, $b) {
$a + $b
}
Add 1 2
if/elseif/else:条件。
if ($x -gt 10) {
"big"
} elseif ($x -eq 10) {
"equal"
} else {
"small"
}
Lua
1. 简介
Lua 是一门轻量级脚本语言,常嵌入到游戏引擎和应用中作为配置与扩展语言。
2. 关键字列表及示例
function/local:函数与局部变量。
local function add(a, b)
return a + b
end
print(add(1, 2))
if/elseif/else/end:条件。
local x = 10
if x > 10 then
print("big")
elseif x == 10 then
print("equal")
else
print("small")
end
Perl
1. 简介
Perl 以强大的文本处理能力著称,早期常用于 CGI、系统管理和日志分析等任务。
2. 关键字列表及示例
my/sub:变量与子例程。
use strict;
use warnings;
sub add {
my ($a, $b) = @_;
return $a + $b;
}
print add(1, 2);
if/elsif/else/for/foreach:控制流。
for my $x (1..3) {
print "$x\n";
}
Objective-C
1. 简介
Objective-C 是在 C 语言基础上加入 Smalltalk 风格消息发送的面向对象语言,主要用于早期的 macOS 和 iOS 开发。
2. 关键字列表及示例
@interface/@implementation:类声明与实现。
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)greet;
@end
@implementation Person
- (void)greet {
NSLog(@"Hello, %@", self.name);
}
@end
@autoreleasepool:自动释放池块。
@autoreleasepool {
Person *p = [Person new];
p.name = @"Alice";
[p greet];
}
F#
1. 简介
F# 是 .NET 平台上的函数式优先语言,支持不可变数据、代数数据类型和模式匹配,也可与 C# 等语言互操作。
2. 关键字列表及示例
let:绑定值或函数。
let add a b = a + b
printfn "%d" (add 1 2)
type/ 模式匹配match:定义类型与匹配。
type Direction = Up | Down | Left | Right
let describe dir =
match dir with
| Up -> "up"
| Down -> "down"
| Left -> "left"
| Right -> "right"
OCaml
1. 简介
OCaml 是一门静态类型的函数式语言,同时支持面向对象特性,常用于编译器、形式化验证和系统工具开发。
2. 关键字列表及示例
let/let rec:绑定与递归函数。
let rec fib n =
if n <= 1 then n
else fib (n - 1) + fib (n - 2)
type/match:代数数据类型与模式匹配。
type direction = Up | Down | Left | Right
let show = function
| Up -> "up"
| Down -> "down"
| Left -> "left"
| Right -> "right"
Clojure
1. 简介
Clojure 是运行在 JVM 和 JavaScript 平台上的现代 Lisp 方言,强调不可变数据结构和并发编程。
2. 关键字列表及示例
def/defn:定义变量与函数。
(def x 1)
(defn add [a b]
(+ a b))
(println (add 1 2))
let/if/cond:局部绑定与条件。
(let [n 10]
(cond
(> n 10) "big"
(= n 10) "equal"
:else "small"))
Scheme
1. 简介
Scheme 是 Lisp 家族的一员,语法极简、核心概念精炼,常用于教学、研究和实验性语言设计。
2. 关键字列表及示例
define/lambda:定义变量与匿名函数。
(define (add a b)
(+ a b))
(display (add 1 2))
if/cond/let:控制流与局部绑定。
(let ((x 10))
(if (> x 10)
(display "big")
(display "not-big")))
Common Lisp
1. 简介
Common Lisp 是一种多范式 Lisp 语言,支持面向对象(CLOS)、宏系统和动态元编程。
2. 关键字列表及示例
defun/let:定义函数与局部绑定。
(defun add (a b)
(+ a b))
(let ((x 1) (y 2))
(print (add x y)))
defclass/defmethod:定义类与方法。
(defclass person ()
((name :initarg :name :accessor person-name)))
Elm
1. 简介
Elm 是一门针对前端 Web 应用的函数式语言,具有强类型系统和“无运行时异常”的设计目标。
2. 关键字列表及示例
type/type alias:自定义类型与别名。
type Direction = Up | Down | Left | Right
type alias User =
{ id : Int
, name : String
}
case .. of:模式匹配。
describe dir =
case dir of
Up -> "up"
Down -> "down"
Left -> "left"
Right -> "right"
Solidity
1. 简介
Solidity 是在以太坊等区块链平台上编写智能合约的主流语言,语法类似于 JavaScript / C++。
2. 关键字列表及示例
contract/function:定义合约与函数。
pragma solidity ^0.8.0;
contract Counter {
uint256 public value;
function inc() public {
value += 1;
}
}
mapping/address:映射与地址类型。
mapping(address => uint256) public balances;
Groovy
1. 简介
Groovy 是 JVM 上的动态语言,语法类似 Java 但更简洁,常用于脚本、构建工具(如 Gradle)和 Web 开发。
2. 关键字列表及示例
def/ 闭包{}:动态变量与闭包。
def add = { a, b -> a + b }
println add(1, 2)
class/implements/extends:面向对象。
class Person {
String name
}
Crystal
1. 简介
Crystal 是一门语法类似 Ruby 的静态编译语言,目标是提供接近 C 的性能和友好的开发体验。
2. 关键字列表及示例
def/class:函数与类。
def add(a, b)
a + b
end
puts add(1, 2)
if/else/elsif:条件语句。
x = 10
if x > 10
puts "big"
elsif x == 10
puts "equal"
else
puts "small"
end
Nim
1. 简介
Nim 是一门静态类型、编译型语言,语法简洁,支持宏和元编程,能编译为 C、C++ 或 JavaScript。
2. 关键字列表及示例
proc:过程(函数)定义。
proc add(a, b: int): int =
a + b
echo add(1, 2)
var/let:可变与不可变绑定。
var x = 1
let y = 2
Fortran
1. 简介
Fortran 是最早的高级编程语言之一,主要用于科学计算和工程模拟领域。
2. 关键字列表及示例
program/end program:程序入口。
program hello
print *, "Hello, world"
end program hello
do/if:循环与条件。
do i = 1, 3
print *, i
end do
COBOL
1. 简介
COBOL 是为商业数据处理设计的老牌语言,仍在许多大型机和金融系统中使用。
2. 关键字列表及示例
IDENTIFICATION DIVISION/PROCEDURE DIVISION:程序结构。
IDENTIFICATION DIVISION.
PROGRAM-ID. HELLO.
PROCEDURE DIVISION.
DISPLAY "Hello, world".
STOP RUN.
MATLAB
1. 简介
MATLAB 是面向矩阵运算和数值分析的商业语言与环境,广泛用于工程、控制和信号处理。
2. 关键字列表及示例
function:函数定义。
function y = add(a, b)
y = a + b;
end
for/if:控制流。
for i = 1:3
disp(i);
end
VBA(Visual Basic for Applications)
1. 简介
VBA 是嵌入在 Office 等应用中的脚本语言,用于编写宏和自动化任务。
2. 关键字列表及示例
Sub/Function:过程与函数。
Sub Hello()
MsgBox "Hello"
End Sub
If/Then/Else/End If:条件。
If x > 10 Then
MsgBox "big"
Else
MsgBox "small"
End If
其他语言与扩展方式
上文已经为一批主流语言给出了完整的「简介 + 关键字与示例」。由于 Languish 数据集中包含约 544 种语言,剩余较少使用或较小众的语言可以按以下方式扩展:
- 继续沿用上述结构,为感兴趣的语言补充内容
- 或者使用脚本从 Languish 的数据源解析出语言名称列表,再批量生成类似的 Markdown 小节作为起点
一个简单的扩展模板(复制后将 YourLang 和内容替换为目标语言):
### YourLang
#### 1. 简介
(用 2–4 句话介绍该语言的定位、主要场景和特点。)
#### 2. 关键字列表及示例
- **`keyword1`**:说明关键字的作用。
```yourlang
// 示例代码
keyword2:说明关键字的作用。
// 示例代码
---
### TIOBE 下一批 50 种语言(按字母排序,仅列名称)
以下为 TIOBE Index 中 #51–#100 的编程语言名称列表,来自“Next 50 Programming Languages” 段落,按字母顺序列出,便于后续按本页模板继续补充「简介 + 关键字」内容:
- **Algol**
- **Alice**
- **Apex**
- **Awk**
- **Bash**
- **C shell**
- **Caml**
- **CL (OS/400)**
- **Clojure**
- **Common Lisp**
- **F#**
- **Forth**
- **GAMS**
- **GML**
- **Groovy**
- **Hack**
- **Icon**
- **Inform**
- **Io**
- **J**
- **J#**
- **JScript**
- **JScript.NET**
- **Korn shell**
- **ML**
- **Modula-2**
- **Mojo**
- **MQL5**
- **MS-DOS batch**
- **NATURAL**
- **Nim**
- **OCaml**
- **OpenCL**
- **Q**
- **REXX**
- **RPG**
- **S**
- **Scheme**
- **Small Basic**
- **Smalltalk**
- **Solidity**
- **SPARK**
- **Structured Text**
- **Tcl**
- **V**
- **Vala/Genie**
- **VHDL**
- **WebAssembly**
- **Wolfram**
- **Xojo**