这是本教程第一天。今天我们将涵盖许多内容:
- 基本的 Rust 语法:变量、标量和复合类型、枚举、结构体、引用、函数和方法。
- 类型和类型推断。
- 控制流结构:循环、条件语句等。
- 用户定义类型:结构体和枚举。
- 模式匹配:解构枚举、结构体和数组。
什么是 Rust
Rust 是一门新兴的编程语言,于 2015 年发布了 1.0 版本:
- Rust 是一种静态编译语言,与 C++ 类似。
rustc使用 LLVM 作为其后端。
- Rust 支持多种平台和架构:
- x86, ARM, WebAssembly, …
- Linux, Mac, Windows, …
- Rust 被用于各种设备:
- 固件和引导加载程序
- 智能显示屏
- 手机
- 桌面
- 服务器
Rust 有什么好处
Rust 的一些独特的地方:
- 编译时内存安全:所有类型的内存错误在编译时就被阻止了:
- 没有未初始化的变量。
- 没有重复释放(double-frees)。
- 没有释放后使用(use-after-free)。
- 没有空指针(NULL pointers)。
- 没有遗忘的已锁定的互斥锁(locked mutexes)。
- 线程之间没有数据竞争。
- 没有迭代器失效。
- 没有未定义的运行时行为:Rust 语句的作用永远不会不明确。
- 数组访问是边界检查的。
- 整数溢出是定义好的(
panic或unwrap)。
- 现代语言特性:与高级语言一样富有表现力和易用性。
- 枚举和模式匹配。
- 泛型。
- 无开销的 FFI(外部函数接口)。
- 零成本抽象。
- 优秀的编译器错误信息。
- 内置的依赖管理器。
- 内置的测试支持。
- 优秀的语言服务器协议支持。
Hello World
好吧,终于到了这里。
我们直接来看可能是最简单的 Rust 程序,一个经典的 “Hello World” 程序:
fn main() {
println!("Hello 🌍!");
}
// Output
// Hello 🌍!
上面这个简单的例子,我们可以学到:
- 函数使用
fn关键字声明。 main函数是程序的入口点。- 代码块像 C 和 C++ 一样用花括号括起来。
- 语句以分号结尾。
- Rust 有卫生宏,
println!就是一个例子。 - Rust 中的字符串采用 UTF-8 编码,可以包含任何 Unicode 字符。
什么是卫生宏:宏在扩展时不会引入与周围代码环境中的变量名冲突的风险。
变量与值
Rust 通过静态类型提供类型安全。变量绑定使用 let 关键字来实现:
fn main() {
let x: i32 = 10;
println!("x: {x}");
}
// Output
// x: 10
在 Rust 中,我们不说“赋值”,而是说“绑定”。例如上述代码我们说:把 10 这个 i32 类型的值绑定到变量 x 上
以下是一些基本的内置类型,以及每种类型的字面量值的语法。
| 类型 | 字面量 | |
|---|---|---|
| 有符号整数 | i8, i16, i32, i64, i128, isize | -10, 0, 1000, 123i64 |
| 无符号整数 | u8, u16, u32, u64, u128, usize | 0, 123, 10u16 |
| 浮点数 | f32, f64 | 3.14, -10.0e20, 2f32 |
| Unicode标量值 | char | 'a', 'α', '\u{123}' |
| 布尔值 | bool | true, false |
这些类型的宽度如下:
iN、uN和fN是N位宽。isize和usize与指针宽度相同。char是 32 位宽。bool是 8 位宽 。
如表格中表示的那样,我们可以使用一个类型后缀主动声明变量的类型:
let a = 23i32;
let b = 23i64;
let c = 23f32;
Rust 中的算数运算:
fn interproduct(a: i32, b: i32, c: i32) -> i32 {
return a * b + b * c + c * a;
}
fn main() {
println!("result: {}", interproduct(120, 100, 248));
}
// Output
// result: 66560
类型推断:
Rust 将通过变量的使用方式来确定其类型:
fn takes_u32(x: u32) {
println!("u32: {x}");
}
fn takes_i8(y: i8) {
println!("i8: {y}");
}
fn main() {
let x = 10;
let y = 20;
takes_u32(x);
takes_i8(y);
}
// Output
// u32: 10
// i8: 20
当我们定义变量 x 和 y 的时候,我们并没有给予类型,此时 Rust 也不着急确定它们的类型。
在我们第一次使用变量时,Rust 便就确定了它们的类型,x 是 u32,y 是 i8。类型推断完成之后,y 就只能当做 i8 用了。此时如果我们后续再次调用 takes_u32(y),Rust 便会提示一个错误:expected u32, found i8。
练习 1
斐波那契数列以 [0, 1] 开头。对于 n > 1,第 n 个斐波那契数通过递归计算,即第 n-1 个斐波那契数与第 n-2 个斐波那契数之和。
fn fib(n: u32) -> u32 {
if n < 2 {
// The base case.
return todo!("Implement this");
} else {
// The recursive case.
return todo!("Implement this");
}
}
fn main() {
let n = 20;
println!("fib({n}) = {}", fib(n));
}
控制流
在 Rust 中,代码块由花括号 {} 括起来,包含一系列表达式。每个代码块都有一个值和一种类型,它们就是代码块中最后一个表达式的值和类型:
fn main() {
let z = 13;
let x = {
let y = 10;
dbg!(y);
z - y
};
dbg!(x);
}
// Output
// [src/main.rs:5:9] y = 10
// [src/main.rs:8:5] x = 3
如果最后一个表达式以 ; 结尾,那么最终的值和类型就是 () 。
变量的作用域仅限于其所在的代码块 。
此处使用了另外一个打印的宏——dbg!。这里只需要记住:dbg!宏是一个非常实用的调试工具,它能够帮助开发者快速打印变量的值及其相关信息,而不会影响原有代码的逻辑。
if
你使用if表达式的方式与在其他语言中使用if语句的方式完全相同:
fn main() {
let x = 10;
if x == 0 {
println!("zero!");
} else if x < 100 {
println!("biggish");
} else {
println!("huge");
}
}
// Output
// biggish
此外,你可以将if用作表达式。每个代码块的最后一个表达式将成为if表达式的值:
fn main() {
let x = 10;
let size = if x < 20 { "small" } else { "large" };
println!("number size: {}", size);
}
// Output
// number size: small
match
match 可用于将一个值与一个或多个选项进行匹配检查:
fn main() {
let val = 1;
match val {
1 => println!("one"),
10 => println!("ten"),
100 => println!("one hundred"),
_ => {
println!("something else");
}
}
}
// Output
// one
与 if 表达式一样,match 也可以返回一个值;
fn main() {
let flag = true;
let val = match flag {
true => 1,
false => 0,
};
println!("The value of {flag} is {val}");
}
// Output
// The value of true is 1
match 应该是 Rust 中最强大的一个控制流了,后续我们会介绍更多关于 match 的用法,这里只做简单介绍
循环
Rust 中有三个循环关键字:while、loop 和 for:
while 关键字的工作方式与其他语言类似,只要条件为真,就会执行循环体:
fn main() {
let mut x = 200;
while x >= 10 {
x = x / 2;
}
dbg!(x);
}
// Output
// [src/main.rs:6:5] x = 6
for 循环会遍历值的范围或集合中的元素:
fn main() {
for x in 1..5 {
dbg!(x);
}
for elem in [2, 4, 8, 16, 32] {
dbg!(elem);
}
}
// Output
// [src/main.rs:3:9] x = 1
// [src/main.rs:3:9] x = 2
// [src/main.rs:3:9] x = 3
// [src/main.rs:3:9] x = 4
// [src/main.rs:7:9] elem = 2
// [src/main.rs:7:9] elem = 4
// [src/main.rs:7:9] elem = 8
// [src/main.rs:7:9] elem = 16
// [src/main.rs:7:9] elem = 32
loop 语句会一直循环,直到遇到 break,这个和其他语言中的 while(true) 类似,这个也是 Rust 中比较有特色的一个循环:
fn main() {
let mut i = 0;
loop {
i += 1;
dbg!(i);
if i > 100 {
break;
}
}
}
如果你想立即开始下一次迭代,请使用 continue。 如果你想提前退出任何类型的循环,请使用 break。
对于 loop 循环,它可以接受一个可选表达式,该表达式将成为循环表达式的值。
fn main() {
let mut i = 0;
loop {
i += 1;
if i > 5 {
break;
}
if i % 2 == 0 {
continue;
}
dbg!(i);
}
}
// Output
// [src/main.rs:11:9] i = 1
// [src/main.rs:11:9] i = 3
// [src/main.rs:11:9] i = 5
continue 和 break 都可以选择性地接受一个标签参数,用于跳出嵌套循环:
fn main() {
let s = [[5, 6, 7], [8, 9, 10], [21, 15, 32]];
let mut elements_searched = 0;
let target_value = 10;
'outer: for i in 0..=2 {
for j in 0..=2 {
elements_searched += 1;
if s[i][j] == target_value {
break 'outer;
}
}
}
dbg!(elements_searched);
}
// Output
// [src/main.rs:13:5] elements_searched = 6
下面是一个函数的例子,在先前的例子中,我们已经知道函数怎么编写了吧:
fn gcd(a: u32, b: u32) -> u32 {
if b > 0 {
gcd(b, a % b)
} else {
a
}
}
fn main() {
dbg!(gcd(143, 52));
}
// Output
// [src/main.rs:10:5] gcd(143, 52) = 13
宏在编译期间会扩展为 Rust 代码,并且可以接受可变数量的参数。宏通过末尾的 ! 来区分。Rust 标准库包含各种有用的宏:
println!(format, ..)会将一行文本打印到标准输出,并应用std::fmt中描述的格式化规则。format!(format, ..)的工作方式与println!类似,但会将结果作为字符串返回。dbg!(expression)会记录表达式的值并返回该值,同时还会打印文件名,行号信息。todo!()将一段代码标记为尚未实现。如果执行它,将会导致程序崩溃。
fn factorial(n: u32) -> u32 {
let mut product = 1;
for i in 1..=n {
product *= dbg!(i);
}
product
}
fn fizzbuzz(n: u32) -> u32 {
todo!()
}
fn main() {
let n = 4;
println!("{n}! = {}", factorial(n));
}
// Output
// [src/main.rs:4:20] i = 1
// [src/main.rs:4:20] i = 2
// [src/main.rs:4:20] i = 3
// [src/main.rs:4:20] i = 4
练习 2
科拉茨序列(Collatz Sequence)的定义如下,对于任意大于零的 :
- 如果 ,那么序列在 处终止。
- 如果 是偶数,则 。
- 如果 是奇数,则 。
例如,从 开始:
- 3 是奇数,所以 ;
- 10 是偶数,所以 ;
- 5 是奇数,所以 3 \times 5 + 1 = 16$;
- 16 是偶数,所以 ;
- 8 是偶数,所以 ;
- 4 是偶数,所以 ;
- 2 是偶数,所以 ;
- 序列在此处终止。
编写一个函数,用于计算给定初始值 的科拉茨序列的长度。
fn collatz_length(mut n: i32) -> u32 {
todo!("Implement this")
}
fn main() {
println!("Length: {}", collatz_length(11)); // should be 15
}
数组与元组
Rust 中的数组:
fn main() {
let mut a: [i8; 5] = [5, 4, 3, 2, 1];
a[2] = 0;
println!("a: {a:?}");
}
// Output
// a: [5, 4, 0, 2, 1]
注意这里的打印,使用了 {a:?}——一种格式化字符串的语法,用于打印调试信息。
Rust 还提供另一种类型,元组:
fn main() {
let t: (i8, bool) = (7, true);
dbg!(t.0);
dbg!(t.1);
}
// Output
// [src/main.rs:3:5] t.0 = 7
// [src/main.rs:4:5] t.1 = true
获取元组中的元素,使用 .0,.1 这种语法,类似元素在元组中的下标。
for 语句支持遍历数组(但不支持遍历元组):
fn main() {
let primes = [2, 3, 5, 7, 11, 13, 17, 19];
for prime in primes {
for i in 2..prime {
assert_ne!(prime % i, 0);
}
}
}
Rust 支持使用模式匹配将元组等较大的值解构为其组成部分:
fn check_order(tuple: (i32, i32, i32)) -> bool {
let (left, middle, right) = tuple; // 一种解构方法
left < middle && middle < right
}
fn main() {
let tuple = (1, 5, 3);
println!(
"{tuple:?}: {}",
if check_order(tuple) { "ordered" } else { "unordered" }
);
}
// Output
// (1, 5, 3): unordered
练习 3
数组可以包含其他数组:
let array = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
小疑问:这个变量 array 的类型是什么?(编译器会给予提示)
使用上述这样的数组编写一个 transpose 函数,该函数将对矩阵进行转置(将行转换为列):
fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
todo!()
}
fn main() {
let matrix = [
[101, 102, 103], // <-- the comment makes rustfmt add a newline
[201, 202, 203],
[301, 302, 303],
];
dbg!(matrix);
let transposed = transpose(matrix);
dbg!(transposed);
}
引用
引用提供了一种在不获取值的所有权的情况下访问另一个值的方法,也被称为 “借用”。共享引用是只读的,被引用的数据不能更改。
fn main() {
let a = 'A';
let b = 'B';
let mut r: &char = &a;
dbg!(*r);
r = &b;
dbg!(*r);
}
// Output
// [src/main.rs:6:5] *r = 'A'
// [src/main.rs:9:5] *r = 'B'
对类型 T 的共享引用的类型是 &T 。引用值是使用 & 运算符创建的。* 运算符“解引用”一个引用,得到它的值 。
例如上面的例子中,通过 &a 创建一个引用 r,然后使用 *r 获取这个引用的值。
独占引用,也称为可变引用,允许更改它们所引用的值。它们的类型是 &mut T。
fn main() {
let mut point = (1, 2);
let x_coord = &mut point.0;
*x_coord = 20;
println!("point: {point:?}");
}
// Output
// point: (20, 2)
切片让你能够查看更大的集合:
fn main() {
let mut a: [i32; 6] = [10, 20, 30, 40, 50, 60];
println!("a: {a:?}");
let s: &mut [i32] = &mut a[2..4]; // 这里创建了一个可变的数组切片
s[0] = 100i32;
println!("s: {s:?}");
println!("a: {a:?}");
}
// Output
// a: [10, 20, 30, 40, 50, 60]
// s: [100, 40]
// a: [10, 20, 100, 40, 50, 60]
切片从被切片的类型中借用数据。
现在我们可以理解 Rust 中的两种字符串类型了:
&str是一个 UTF-8 编码字节的切片,类似于&[u8]。String是一个拥有所有权的 UTF-8 编码字节缓冲区,类似于[u8]。
fn main() {
let s1: &str = "World";
println!("s1: {s1}");
let mut s2: String = String::from("Hello ");
println!("s2: {s2}");
s2.push_str(s1);
println!("s2: {s2}");
let s3: &str = &s2[2..9];
println!("s3: {s3}");
}
// Output
// s1: World
// s2: Hello
// s2: Hello World
// s3: llo Wor
Rust 对引用强制执行一些规则,以确保它们始终可以安全使用。
- 引用永远不能为空,这样在使用时无需进行空值检查。
- 引用的生命周期不能超过它们所指向的数据的生命周期。
现在,我们探讨第二条:
fn main() {
let x_ref = {
let x = 10;
&x
};
dbg!(x_ref);
}
// 这段代码并不能通过编译,原因就是 `x` 的生命周期要短于 `x_ref`。
练习 4
// 计算向量各坐标的平方和并取平方根来计算向量的大小。使用 `sqrt()` 方法来计算平方根,例如 `v.sqrt()`。
fn magnitude(...) -> f64 {
todo!()
}
// 计算向量的大小并将其所有坐标除以该大小来对向量进行归一化。
fn normalize(...) {
todo!()
}
fn main() {
println!("Magnitude of a unit vector: {}", magnitude(&[0.0, 1.0, 0.0]));
let mut v = [1.0, 2.0, 9.0];
println!("Magnitude of {v:?}: {}", magnitude(&v));
normalize(&mut v);
println!("Magnitude of {v:?} after normalization: {}", magnitude(&v));
}
自定义类型
与 C 和 C++ 一样,Rust 支持自定义结构体:
struct Person {
name: String,
age: u8,
}
fn describe(person: &Person) {
println!("{} is {} years old", person.name, person.age);
}
fn main() {
let mut peter = Person {
name: String::from("Peter"),
age: 27,
};
describe(&peter);
peter.age = 28;
describe(&peter);
let name = String::from("Avery");
let age = 39;
let avery = Person { name, age };
describe(&avery);
}
// Output
// Peter is 27 years old
// Peter is 28 years old
// Avery is 39 years old
如果字段名称不重要,可以使用元组结构体:
struct Point(i32, i32);
fn main() {
let p = Point(17, 23);
println!("({}, {})", p.0, p.1);
}
这通常用于单字段包装器(称为新类型,Kotlin 中有个类似的写法,是 value class):
struct PoundsOfForce(f64);
struct Newtons(f64);
fn compute_thruster_force() -> PoundsOfForce {
todo!("Ask a rocket scientist at NASA")
}
fn set_thruster_force(force: Newtons) {
// ...
}
fn main() {
let force = compute_thruster_force();
set_thruster_force(force);
}
enum 关键字允许创建具有几种不同变体的类型(Rust 中的枚举是非常强大的):
#[derive(Debug)]
enum Direction {
Left,
Right,
}
#[derive(Debug)]
enum PlayerMove {
Pass, // 最简单的
Run(Direction), // 元组
Teleport { x: u32, y: u32 }, // 结构体
}
fn main() {
let dir = Direction::Left;
let player_move: PlayerMove = PlayerMove::Run(dir);
println!("On this turn: {player_move:?}");
}
类型别名是为另一种类型创建的名称。这两种类型可以互换使用:
enum CarryableConcreteItem {
Left,
Right,
}
type Item = CarryableConcreteItem;
// 对于冗长、复杂的类型,别名更为有用
use std::cell::RefCell;
use std::sync::{Arc, RwLock};
type PlayerInventory = RwLock<Vec<Arc<RefCell<Item>>>>; // 这个类型就特别冗长
常量在编译时求值,其值会在使用它们的任何地方内联:
const DIGEST_SIZE: usize = 3;
const FILL_VALUE: u8 = calculate_fill_value();
const fn calculate_fill_value() -> u8 {
if DIGEST_SIZE < 10 {
42
} else {
13
}
}
fn compute_digest(text: &str) -> [u8; DIGEST_SIZE] {
let mut digest = [FILL_VALUE; DIGEST_SIZE];
for (idx, &b) in text.as_bytes().iter().enumerate() {
digest[idx % DIGEST_SIZE] = digest[idx % DIGEST_SIZE].wrapping_add(b);
}
digest
}
fn main() {
let digest = compute_digest("Hello");
println!("digest: {digest:?}");
}
// Output
// digest: [222, 254, 150]
const 的语义与 C++ 中的 constexpr 类似。
静态变量在程序的整个执行过程中都存在,因此不会被移动 :
static BANNER: &str = "Welcome to RustOS 3.14";
fn main() {
println!("{BANNER}");
}
练习 5
我们将创建一个数据结构来表示电梯控制系统中的一个事件。由你来定义构建各种事件的类型和函数。使用 #[derive(Debug)] 以便能够使用 {:?} 对这些类型进行格式化输出。
本练习只需要创建并填充数据结构,以确保 main 函数能无错误运行。本教程的下一部分将介绍如何从这些结构中获取数据。
#![allow(dead_code)]
#[derive(Debug)]
/// 电梯系统中控制器必须做出响应的一个事件
enum Event {
// TODO: 添加所需的字段
}
/// 行进方向
#[derive(Debug)]
enum Direction {
Up,
Down,
}
/// 轿厢已到达指定楼层
fn car_arrived(floor: i32) -> Event {
todo!()
}
/// 轿厢门打开
fn car_door_opened() -> Event {
todo!()
}
/// 轿厢门关闭
fn car_door_closed() -> Event {
todo!()
}
/// 在指定楼层的电梯厅里按下了一个方向按钮
fn lobby_call_button_pressed(floor: i32, dir: Direction) -> Event {
todo!()
}
/// 在轿厢内按下了一个楼层按钮.
fn car_floor_button_pressed(floor: i32) -> Event {
todo!()
}
fn main() {
println!(
"A ground floor passenger has pressed the up button: {:?}",
lobby_call_button_pressed(0, Direction::Up)
);
println!("The car has arrived on the ground floor: {:?}", car_arrived(0));
println!("The car door opened: {:?}", car_door_opened());
println!(
"A passenger has pressed the 3rd floor button: {:?}",
car_floor_button_pressed(3)
);
println!("The car door closed: {:?}", car_door_closed());
println!("The car has arrived on the 3rd floor: {:?}", car_arrived(3));
}
练习答案
练习 1
fn fib(n: u32) -> u32 {
if n < 2 {
return n;
} else {
return fib(n - 1) + fib(n - 2);
}
}
fn main() {
let n = 20;
println!("fib({n}) = {}", fib(n));
}
练习 2
fn collatz_length(mut n: i32) -> u32 {
let mut len = 1;
while n > 1 {
n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
len += 1;
}
len
}
fn main() {
println!("Length: {}", collatz_length(11)); // should be 15
}
练习 3
fn transpose(matrix: [[i32; 3]; 3]) -> [[i32; 3]; 3] {
let mut result = [[0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
result[j][i] = matrix[i][j];
}
}
result
}
fn main() {
let matrix = [
[101, 102, 103], // <-- the comment makes rustfmt add a newline
[201, 202, 203],
[301, 302, 303],
];
dbg!(matrix);
let transposed = transpose(matrix);
dbg!(transposed);
}
练习 4
fn magnitude(vector: &[f64; 3]) -> f64 {
let mut mag_squared = 0.0;
for coord in vector {
mag_squared += coord * coord;
}
mag_squared.sqrt()
}
fn normalize(vector: &mut [f64; 3]) {
let mag = magnitude(vector);
for item in vector {
*item /= mag;
}
}
fn main() {
println!("Magnitude of a unit vector: {}", magnitude(&[0.0, 1.0, 0.0]));
let mut v = [1.0, 2.0, 9.0];
println!("Magnitude of {v:?}: {}", magnitude(&v));
normalize(&mut v);
println!("Magnitude of {v:?} after normalization: {}", magnitude(&v));
}
练习 5
#![allow(dead_code)]
#[derive(Debug)]
enum Event {
/// 按钮按下.
ButtonPressed(Button),
/// 轿厢达到制定楼层.
CarArrived(Floor),
/// 轿厢门打开.
CarDoorOpened,
/// 轿厢门关闭.
CarDoorClosed,
}
/// 楼层使用 Floor 来表示
type Floor = i32;
/// 电梯的运行方向
#[derive(Debug)]
enum Direction {
Up,
Down,
}
/// 用户可操作的按钮
#[derive(Debug)]
enum Button {
/// 楼层电梯厅的一个按钮(电梯外)
LobbyCall(Direction, Floor),
/// 轿厢内的楼层按钮(电梯内)
CarFloor(Floor),
}
fn car_arrived(floor: i32) -> Event {
Event::CarArrived(floor)
}
fn car_door_opened() -> Event {
Event::CarDoorOpened
}
fn car_door_closed() -> Event {
Event::CarDoorClosed
}
fn lobby_call_button_pressed(floor: i32, dir: Direction) -> Event {
Event::ButtonPressed(Button::LobbyCall(dir, floor))
}
fn car_floor_button_pressed(floor: i32) -> Event {
Event::ButtonPressed(Button::CarFloor(floor))
}
fn main() {
println!(
"A ground floor passenger has pressed the up button: {:?}",
lobby_call_button_pressed(0, Direction::Up)
);
println!("The car has arrived on the ground floor: {:?}", car_arrived(0));
println!("The car door opened: {:?}", car_door_opened());
println!(
"A passenger has pressed the 3rd floor button: {:?}",
car_floor_button_pressed(3)
);
println!("The car door closed: {:?}", car_door_closed());
println!("The car has arrived on the 3rd floor: {:?}", car_arrived(3));
}