第一章:入门与基础
本章涵盖了开始使用 Rust 所需了解的基础知识,包括安装、Cargo 工具、基本语法和控制流。
提示:安装与项目生命周期简图
flowchart LR
A[安装 rustup] --> B[安装工具链: stable/beta/nightly]
B --> C[cargo new/init]
C --> D[cargo build/check/run]
D --> E[cargo test/doc]
E --> F[cargo publish]
1. 如何在我的电脑上安装 Rust?
答:
你可以通过 rustup
来安装 Rust,它是 Rust 的官方工具链管理器。
在 Linux 或 macOS 上,打开终端并运行:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
在 Windows 上,访问 rust-lang.org 并按照说明下载 rustup-init.exe
并运行它。
安装完成后,通过运行 rustc --version
和 cargo --version
来验证安装是否成功。
2. rustc
和 cargo
有什么区别?
答:
- rustc
是 Rust 的编译器。它负责将你的 .rs
源代码文件编译成可执行文件。你通常很少直接使用它。
- cargo
是 Rust 的构建系统和包管理器。它是一个功能强大的工具,可以处理依赖管理、编译代码、运行测试、生成文档等。在实际项目中,你几乎总是通过 cargo
来工作。
3. 如何创建一个新的 Rust 项目?
答: 使用 Cargo!在终端中运行以下命令:
# 创建一个二进制程序项目
cargo new my_project
# 创建一个库项目
cargo new my_library --lib
# 在现有目录中初始化项目
cd existing_folder
cargo init
# 指定项目名称和版本
cargo new my_app --name "awesome-app"
Cargo.toml
(配置文件)和 src/main.rs
(或 src/lib.rs
)源文件的新目录。
生成的项目结构:
my_project/
├── Cargo.toml
├── src/
│ └── main.rs
└── .gitignore
4. 如何编译并运行我的 Rust 程序?
答:
在你的项目目录中,使用 cargo run
。这个命令会处理所有事情:
1. 如果需要,它会先编译你的代码。
2. 然后运行生成的可执行文件。
cd my_project
cargo run
cargo build
。
5. Cargo.toml
文件是做什么用的?
答:
Cargo.toml
是 Cargo 的清单文件,采用 TOML 格式。它包含了项目的所有元数据和配置:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A sample Rust project"
license = "MIT"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
[dev-dependencies]
criterion = "0.5"
[[bin]]
name = "main"
path = "src/main.rs"
主要配置段:
- [package]
: 项目信息,如名称、版本、作者
- [dependencies]
: 项目所依赖的外部库(crates)
- [dev-dependencies]
: 开发和测试时的依赖
- [[bin]]
: 二进制目标配置
6. Rust 中的变量默认是可变的还是不可变的?
答:
默认是不可变的。这是 Rust 强调安全性的一个核心特性。如果你想让一个变量可变,必须使用 mut
关键字。
fn main() {
let x = 5; // 不可变
let mut y = 10; // 可变
println!("x = {}, y = {}", x, y);
y = 15; // 正确,y 是可变的
println!("现在 y = {}", y);
// x = 6; // 编译错误!不能修改不可变变量
// 可以通过 shadowing 重新定义 x
let x = x + 1;
println!("通过 shadowing,x = {}", x);
// 甚至可以改变类型
let x = "现在是字符串";
println!("x = {}", x);
}
7. Rust 有哪些基本的标量类型?
答: Rust 有四种主要的标量类型:
fn main() {
// 整数类型
let decimal = 98_222; // 十进制
let hex = 0xff; // 十六进制
let octal = 0o77; // 八进制
let binary = 0b1111_0000; // 二进制
let byte = b'A'; // 字节(仅限 u8)
// 显式类型注解
let x: i32 = 42;
let y: u64 = 1000;
// 浮点数类型
let pi = 3.14159; // f64(默认)
let e: f32 = 2.71828; // f32
// 布尔值
let is_rust_awesome = true;
let is_learning: bool = false;
// 字符类型(Unicode)
let letter = 'A';
let emoji = '😊';
let chinese = '中';
println!("整数: {}, {}, {}, {}, {}", decimal, hex, octal, binary, byte);
println!("浮点数: {}, {}", pi, e);
println!("布尔值: {}, {}", is_rust_awesome, is_learning);
println!("字符: {}, {}, {}", letter, emoji, chinese);
}
类型详解:
- 整数 (Integer): 有符号(i8
, i16
, i32
, i64
, i128
, isize
)和无符号(u8
, u16
, u32
, u64
, u128
, usize
)
- 浮点数 (Floating-Point): f32
和 f64
。f64
是默认类型,精度更高
- 布尔值 (Boolean): bool
,值为 true
或 false
- 字符 (Character): char
,表示一个 Unicode 标量值,大小为 4 个字节
8. &str
和 String
有什么区别?
答:
这是一个常见的问题,涉及到 Rust 的所有权系统。
- String
: 一个在堆上分配的、可增长的、可变的 UTF-8 编码字符串。当你需要拥有字符串数据并可能修改它时使用。
- &str
: 通常称为“字符串切片”,它是一个指向 String
或字符串字面量中一部分数据的不可变引用。它没有所有权,只是“借用”数据。
9. 如何在 Rust 中定义一个函数?
答:
使用 fn
关键字。Rust 使用 snake_case
作为函数名的约定。如果函数有参数,你需要显式地声明它们的类型。如果函数返回值,你需要使用 ->
来指定返回类型。
// 没有参数,没有返回值
fn say_hello() {
println!("Hello, Rust!");
}
// 有参数,有返回值
fn add_one(x: i32) -> i32 {
x + 1 // 表达式作为返回值,注意没有分号
}
// 多个参数
fn calculate_area(width: u32, height: u32) -> u32 {
width * height
}
// 带有默认参数的模拟(通过重载实现)
fn greet_default() {
greet("World");
}
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// 返回多个值(使用元组)
fn swap(x: i32, y: i32) -> (i32, i32) {
(y, x)
}
fn main() {
say_hello();
let result = add_one(5);
println!("5 + 1 = {}", result);
let area = calculate_area(10, 20);
println!("面积 = {}", area);
greet_default();
greet("Rust");
let (a, b) = swap(1, 2);
println!("交换后: a = {}, b = {}", a, b);
}
10. Rust 函数的返回值有什么特别之处?
答:
Rust 是一个基于表达式的语言。函数体中的最后一个表达式会自动成为函数的返回值,前提是该表达式后面没有分号。你也可以使用 return
关键字提前返回。
fn five() -> i32 {
5 // 返回 5
}
fn ten() -> i32 {
return 10; // 使用 return 关键字,效果相同
}
11. Rust 中的 if
语句有什么不同?
答:
if
在 Rust 中是一个表达式,而不是一个语句。这意味着 if
可以返回值,你可以用它来给 let
语句赋值。
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else {
println!("number is not divisible by 4");
}
// `if` 作为表达式
let condition = true;
let x = if condition { 5 } else { 6 }; // x 的值将是 5
if
的每个分支返回的值类型必须相同。
12. Rust 有哪几种循环?
答:
Rust 提供了三种循环结构:
- loop
: 创建一个无限循环,你需要使用 break
来退出。
- while
: 当条件为 true
时循环。
- for
: 遍历一个迭代器。这是最常用和最安全的循环方式。
fn main() {
// for 循环 - 遍历数组
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("数组元素: {}", element);
}
// 遍历数组并获取索引
for (index, value) in a.iter().enumerate() {
println!("索引 {}: 值 {}", index, value);
}
// 遍历范围
for number in 1..4 { // 不包括 4
println!("数字: {}!", number);
}
// 包含结束值的范围
for number in 1..=3 { // 包括 3
println!("包含范围: {}", number);
}
// while 循环
let mut counter = 0;
while counter < 3 {
println!("计数器: {}", counter);
counter += 1;
}
// loop 无限循环
let mut x = 0;
let result = loop {
x += 1;
if x == 10 {
break x * 2; // 从 loop 中返回值
}
};
println!("循环结果: {}", result);
// 带标签的循环
'outer: loop {
println!("外层循环");
'inner: loop {
println!("内层循环");
break 'outer; // 跳出外层循环
}
}
}
13. 什么是 isize
和 usize
?
答:
它们是“指针大小”的整数类型。
- usize
: 无符号整数,其大小与计算机的指针大小相同。在 64 位系统上是 64 位,在 32 位系统上是 32 位。它主要用于索引集合。
- isize
: 有符号整数,大小与指针相同。
当你需要索引数组或 Vector
时,应该使用 usize
。
14. 如何在 Rust 中写注释?
答:
- 行注释: //
,注释到行尾。
- 块注释: /* ... */
,可以跨越多行。
- 文档注释: ///
或 //!
,用于生成 HTML 文档。///
注释它后面的项,//!
注释包含它的项(通常是模块或 crate)。
// 这是一个行注释
/*
这是一个
多行块注释
*/
/// 这是一个文档注释,用于描述下面的函数
fn my_function() {}
15. Rust 中的元组 (Tuple) 是什么?
答: 元组是一种将多个不同类型的值组合成一个复合类型的方式。元组的长度是固定的,一旦声明就无法改变。
fn main() {
// 创建元组
let tup: (i32, f64, u8) = (500, 6.4, 1);
// 解构元组
let (x, y, z) = tup;
println!("解构后: x={}, y={}, z={}", x, y, z);
// 通过索引访问
let five_hundred = tup.0;
let six_point_four = tup.1;
let one = tup.2;
println!("索引访问: {}, {}, {}", five_hundred, six_point_four, one);
// 单元元组(空元组)
let unit = ();
println!("单元类型: {:?}", unit);
// 不同类型的元组
let mixed = ("hello", 42, true, 3.14);
println!("混合类型元组: {:?}", mixed);
// 嵌套元组
let nested = ((1, 2), (3, 4));
println!("嵌套元组: {:?}", nested);
println!("访问嵌套元素: {}", (nested.0).1); // 输出 2
// 元组作为函数返回值
let coords = get_coordinates();
println!("坐标: ({}, {})", coords.0, coords.1);
}
fn get_coordinates() -> (i32, i32) {
(10, 20)
}
16. Rust 中的数组 (Array) 是什么?
答: 数组也是一种将多个值组合在一起的方式,但与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组长度是固定的,存储在栈上。
fn main() {
// 基本数组定义
let a = [1, 2, 3, 4, 5]; // 编译器自动推断类型和长度
let months: [&str; 12] = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
// 初始化相同值的数组
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
let threes = [3; 4]; // [3, 3, 3, 3]
// 访问元素
let first = a[0];
let second = a[1];
println!("第一个元素: {}, 第二个元素: {}", first, second);
// 数组长度
println!("数组 a 的长度: {}", a.len());
println!("数组 months 的长度: {}", months.len());
// 遍历数组
println!("遍历数组 a:");
for element in &a {
println!(" {}", element);
}
// 使用索引遍历
println!("使用索引遍历:");
for i in 0..a.len() {
println!(" a[{}] = {}", i, a[i]);
}
// 数组切片
let slice = &a[1..4]; // [2, 3, 4]
println!("切片: {:?}", slice);
// 多维数组
let matrix: [[i32; 3]; 2] = [[1, 2, 3], [4, 5, 6]];
println!("矩阵: {:?}", matrix);
println!("矩阵元素 [1][2]: {}", matrix[1][2]);
}
17. 什么是表达式和语句?
答:
- 语句 (Statements): 是执行某些操作但不返回值的指令。例如,let x = 5;
是一个语句。
- 表达式 (Expressions): 会计算并产生一个值。例如,5 + 6
是一个表达式,它的值为 11
。if
、函数调用等都是表达式。
在 Rust 中,这是一个重要的区别。例如,你不能写 let x = (let y = 6);
因为 let y = 6
是一个语句,不返回值。
18. cargo check
命令有什么用?
答:
cargo check
是一个非常有用的命令。它会快速检查你的代码以确保它可以通过编译,但不会产生任何可执行文件。这比 cargo build
快得多,因为他跳过了代码生成阶段。
在你编码过程中,可以频繁使用 cargo check
来快速验证你的代码语法和类型是否正确。
19. Rust 如何处理整数溢出?
答:
这取决于构建模式:
- Debug 模式 (默认): 如果发生整数溢出,Rust 会 panic
(程序崩溃)。这有助于在开发阶段尽早发现 bug。
- Release 模式 (--release
): 如果发生整数溢出,Rust 会执行“环绕”(two's complement wrapping)。例如,一个 u8
的值 255
再加 1
会变成 0
。
如果你希望明确处理溢出,可以使用标准库提供的 wrapping_*
、checked_*
、overflowing_*
和 saturating_*
系列方法。
20. 什么是 shadowing (变量遮蔽)?
答: 在 Rust 中,你可以声明一个与之前变量同名的新变量。这个新变量会“遮蔽”前一个变量。这允许你重复使用一个更有意义的变量名,甚至改变它的类型。
let x = 5;
// 在内部作用域中遮蔽 x
let x = x + 1;
{
// 在这个作用域内再次遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x); // 打印 12
}
println!("The value of x is: {}", x); // 打印 6
mut
是不同的,因为遮蔽实际上是创建了一个全新的变量。