19 四月 2025
Rust 持续吸引着开发者的兴趣,连续多年在 Stack Overflow 调查中获得“最受欢迎”编程语言的称号。这不仅仅是炒作;Rust 提供了一套引人注目的高性能、安全性和现代语言特性的组合,解决了其他系统编程语言中常见的痛点。如果你对 Rust 的独特之处感到好奇并想开始你的学习之旅,这份初学者指南将为你提供入门所需的基础知识。我们将探讨核心语法、像所有权这样的独特概念,以及支撑 Rust 生态系统的基本工具。
Rust 将自己定位为一种用于构建可靠且高效软件的语言。其主要优势在于无需依赖垃圾回收器即可实现内存安全,并能实现无畏并发。以下是你应该考虑学习 Rust 的原因:
在编写第一行 Rust 代码之前,你需要设置 Rust 工具链。安装 Rust 的标准且推荐的方法是使用 rustup
,即 Rust 工具链安装器。
rustup
:这个命令行工具管理你的 Rust 安装。它允许你安装、更新以及轻松切换不同的 Rust 版本(如稳定版、测试版或每夜构建版)。访问 Rust 官方网站(https://www.rust-lang.org/tools/install)获取针对你操作系统的具体安装说明。rustc
:这是 Rust 编译器。在你将 Rust 源代码写入 .rs
文件后,rustc
将它们编译成你的计算机可以理解的可执行二进制文件或库。虽然它至关重要,但在日常工作流程中,你通常不会直接频繁调用 rustc
。cargo
:这是 Rust 的构建系统和包管理器,也是你将最常与之交互的工具。Cargo 负责协调许多常见的开发任务:
cargo new
)。cargo build
)。cargo run
)。cargo test
)。Cargo.toml
文件中列出的外部库或 Crate)。如果想快速进行实验而无需本地安装,像官方的 Rust Playground 或集成开发环境如 Replit 这样的在线平台是非常好的选择。
让我们从传统的“Hello, world!”程序开始,这是学习任何新语言的必经之路。创建一个名为 main.rs
的文件,并添加以下代码:
fn main() {
// 这行代码将文本打印到控制台
println!("Hello, Rust!");
}
要使用基本工具编译并运行这个简单的程序:
main.rs
的目录,然后运行:
rustc main.rs
这个命令调用 Rust 编译器(rustc
),它会创建一个可执行文件(例如,在 Linux/macOS 上是 main
,在 Windows 上是 main.exe
)。./main
# 在 Windows 上,使用: .\main.exe
然而,对于任何超出单个文件的项目,使用 Cargo 是标准且方便得多的方法:
cargo new hello_rust
cd hello_rust
Cargo 会创建一个名为 hello_rust
的新目录,其中包含一个 src
子目录(内有 main.rs
,已填充“Hello, Rust!”代码)和一个名为 Cargo.toml
的配置文件。cargo run
Cargo 将处理编译步骤,然后执行生成的程序,在你的控制台上显示“Hello, Rust!”。让我们分解一下这段代码:
fn main()
: 这定义了主函数。fn
关键字表示函数声明。main
是一个特殊的函数名;它是每个可执行 Rust 程序开始运行的入口点。括号 ()
表示该函数不接受任何输入参数。{}
: 花括号定义了一个代码块或作用域。所有属于该函数的代码都放在这些花括号内。println!("Hello, Rust!");
: 这一行执行将文本打印到控制台的操作。
println!
是一个 Rust 宏。宏与函数类似,但有一个关键区别:它们以感叹号 !
结尾。宏执行编译时代码生成,提供比常规函数更强大的功能和灵活性(例如处理可变数量的参数,println!
就是这样做的)。println!
宏将提供的文本打印到控制台,并自动在末尾添加一个换行符。"Hello, Rust!"
是一个字符串字面量——表示文本的固定字符序列,用双引号括起来。;
: 分号标记语句的结束。大多数可执行的 Rust 代码行(语句)都以分号结束。现在,让我们深入了解 Rust 编程语言的基本构建块。
变量用于存储数据值。在 Rust 中,你使用 let
关键字声明变量。
let apples = 5;
let message = "Take five";
Rust 的一个核心概念是变量默认是不可变的。这意味着一旦一个值绑定到一个变量名上,你以后就不能更改该值。
let x = 10;
// x = 15; // 这一行会导致编译时错误!无法对不可变变量 `x` 进行二次赋值。
println!("x 的值是: {}", x); // {} 是 x 值的占位符
这种默认的不可变性是一种刻意的设计选择,它通过防止意外修改数据(这可能是错误的常见来源)来帮助你编写更安全、更可预测的代码。如果你确实需要一个值可以改变的变量,你必须在声明时使用 mut
关键字明确地将其标记为可变。
let mut count = 0; // 将 'count' 声明为可变
println!("初始计数: {}", count);
count = 1; // 这是允许的,因为 'count' 是用 'mut' 声明的
println!("新的计数: {}", count);
Rust 还允许遮蔽(Shadowing)。你可以在同一作用域内声明一个与先前变量同名的新变量。新变量会“遮蔽”旧变量,意味着后续使用该名称将引用新变量。这与可变性不同,因为我们正在创建一个全新的变量,它甚至可以有不同的类型。
let spaces = " "; // 'spaces' 最初是一个字符串切片 (&str)
let spaces = spaces.len(); // 'spaces' 现在被一个持有其长度(一个整数,usize)的新变量遮蔽了
println!("空格的数量: {}", spaces); // 打印整数值
Rust 是一种静态类型语言。这意味着编译器必须在编译时知道每个变量的类型。然而,Rust 具有出色的类型推断能力。在许多情况下,你不需要显式地写出类型;编译器通常可以根据值以及你如何使用它来推断出类型。
let quantity = 10; // 编译器推断为 i32(默认的有符号整数类型)
let price = 9.99; // 编译器推断为 f64(默认的浮点数类型)
let active = true; // 编译器推断为 bool(布尔值)
let initial = 'R'; // 编译器推断为 char(字符)
如果你想要或需要明确指定类型(例如,为了清晰起见或当编译器需要帮助时),你可以使用冒号 :
后跟类型名称来提供类型注解。
let score: i32 = 100; // 显式指定为有符号 32 位整数
let ratio: f32 = 0.5; // 显式指定为单精度浮点数
let is_complete: bool = false; // 显式指定为布尔值
let grade: char = 'A'; // 显式指定为字符(Unicode 标量值)
Rust 有几种内置的标量类型(表示单个值):
i8
, i16
, i32
, i64
, i128
, isize
)存储正整数、负整数和零。无符号整数(u8
, u16
, u32
, u64
, u128
, usize
)仅存储非负整数(包括零)。isize
和 usize
类型的大小取决于计算机的架构(32 位或 64 位),主要用于索引集合。f32
(单精度)和 f64
(双精度)。默认是 f64
。bool
类型有两个可能的值:true
或 false
。char
类型表示单个 Unicode 标量值(比仅 ASCII 字符更全面),用单引号括起来(例如,'z'
, 'π'
, '🚀'
)。String
vs &str
在 Rust 中处理文本通常涉及两种主要的类型,这可能会让新手感到困惑:
&str
(读作“字符串切片”):这是对存储在内存某处的 UTF-8 编码字节序列的不可变引用。字符串字面量(如 "Hello"
)的类型是 &'static str
,意味着它们直接存储在程序的可执行文件中,并在程序的整个生命周期内存在。切片提供了对字符串数据的视图,但不拥有它。它们的大小是固定的。String
:这是一个有所有权的、可增长的、可变的、UTF-8 编码的字符串类型。String
的数据存储在堆上,允许其调整大小。当你需要修改字符串数据,或者字符串需要拥有其数据并管理自己的生命周期时(通常在从函数返回字符串或将其存储在结构体中时),你会使用 String
。// 字符串字面量(存储在程序二进制文件中,不可变)
let static_slice: &'static str = "我是不可变的";
// 从字面量创建一个有所有权的、堆分配的 String
let mut dynamic_string: String = String::from("开始");
// 修改 String(这是可能的,因为它是可变的并且拥有其数据)
dynamic_string.push_str("并增长");
println!("{}", dynamic_string); // 输出: 开始并增长
// 创建一个引用 String 部分内容的字符串切片
// 这个切片借用了 dynamic_string 的数据
let slice_from_string: &str = &dynamic_string[0..5]; // 引用 "开始"
println!("切片: {}", slice_from_string);
函数是将代码组织成命名的、可重用单元的基础。我们已经遇到过特殊的 main
函数。
// 函数定义
fn greet(name: &str) { // 接受一个参数:'name',它是一个字符串切片 (&str)
println!("你好, {}!", name);
}
// 接受两个 i32 参数并返回一个 i32 的函数
fn add(a: i32, b: i32) -> i32 {
// 在 Rust 中,函数体中的最后一个表达式会自动作为返回值,
// 只要它不以分号结尾。
a + b
// 这等同于写:return a + b;
}
fn main() {
greet("爱丽丝"); // 调用 'greet' 函数
let sum = add(5, 3); // 调用 'add',将返回的值绑定到 'sum'
println!("5 + 3 = {}", sum); // 输出: 5 + 3 = 8
}
关于函数的要点:
fn
关键字声明函数。->
之后指定函数的返回类型。如果函数不返回值,其返回类型隐式为 ()
(一个空元组,通常称为“单元类型”)。;
结尾)组成,并可选地以一个表达式(计算出值的某物)结尾。return
关键字可用于从函数内部的任何位置进行显式的、提前的返回。Rust 提供了标准的控制流结构来决定代码执行的顺序:
if
/else
/else if
: 用于条件执行。let number = 6;
if number % 4 == 0 {
println!("数字可以被 4 整除");
} else if number % 3 == 0 {
println!("数字可以被 3 整除"); // 这个分支会执行
} else {
println!("数字不能被 4 或 3 整除");
}
// 重要:在 Rust 中,'if' 是一个表达式,意味着它会计算出一个值。
// 这允许你直接在 'let' 语句中使用它。
let condition = true;
let value = if condition { 5 } else { 6 }; // value 将是 5
println!("这个值是: {}", value);
// 注意:'if' 表达式的两个分支必须计算出相同类型的值。
loop
:创建一个无限循环。你通常使用 break
来退出循环,可以选择性地返回一个值。while
:只要指定的条件保持为真就一直循环。for
:遍历集合或范围中的元素。这是 Rust 中最常用且通常最安全的循环类型。// loop 示例
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // 退出循环并返回 'counter * 2'
}
};
println!("循环结果: {}", result); // 输出: 循环结果: 20
// while 示例
let mut num = 3;
while num != 0 {
println!("{}!", num);
num -= 1;
}
println!("起飞!!!");
// for 示例(遍历数组)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() 创建一个数组元素的迭代器
println!("值是: {}", element);
}
// for 示例(遍历范围)
// (1..4) 创建一个包含 1, 2, 3 的范围(不包括 4)
// .rev() 反转迭代器
for number in (1..4).rev() {
println!("{}!", number); // 打印 3!, 2!, 1!
}
println!("再次起飞!!!");
使用注释向你的代码添加编译器会忽略的解释和说明。
// 这是单行注释。它延伸到行尾。
/*
* 这是多行块注释。
* 它可以跨越多行,对于
* 较长的解释很有用。
*/
let lucky_number = 7; // 你也可以在行尾放置注释。
所有权是 Rust 最独特和核心的特性。正是这个机制使得 Rust 能够在编译时保证内存安全,而无需垃圾回收器。掌握所有权是理解 Rust 的关键。它遵循三个核心规则:
{ // s 在这里无效,它尚未声明
let s = String::from("hello"); // 从此点开始 s 有效;
// s '拥有' 在堆上分配的 String 数据。
// 你可以在这里使用 s
println!("{}", s);
} // 作用域到此结束。's' 不再有效。
// Rust 自动为 s 所拥有的 String 调用一个特殊的 'drop' 函数,
// 释放其堆内存。
当你将一个拥有所有权的值(如 String
、Vec
或包含拥有所有权类型的结构体)赋给另一个变量,或者按值将其传递给函数时,所有权会移动。原始变量变得无效。
let s1 = String::from("原始值");
let s2 = s1; // String 数据的所有权从 s1 移动到了 s2。
// 此后 s1 不再被视为有效。
// println!("s1 是: {}", s1); // 编译时错误!值在移动后被借用。
// s1 不再拥有数据。
println!("s2 是: {}", s2); // s2 现在是所有者并且有效。
这种移动行为防止了“二次释放”错误,即两个变量可能在离开作用域时意外地尝试释放相同的内存位置。像整数、浮点数、布尔值和字符这样的基本类型实现了 Copy
Trait,这意味着它们在赋值或传递给函数时会被简单地复制而不是移动。
如果你想让一个函数使用某个值而不转移所有权怎么办?你可以创建引用。创建引用被称为借用。引用允许你访问由另一个变量拥有的数据,而无需获得其所有权。
// 这个函数接受一个 String 的引用 (&)。
// 它借用了 String 但不获取所有权。
fn calculate_length(s: &String) -> usize {
s.len()
} // 在这里,s(引用)离开了作用域。但因为它不拥有
// String 数据,所以当引用离开作用域时,数据不会被丢弃。
fn main() {
let s1 = String::from("hello");
// 我们使用 '&' 符号传递 s1 的引用。
// s1 仍然拥有 String 数据。
let len = calculate_length(&s1);
// s1 在这里仍然有效,因为所有权从未被移动。
println!("'{}' 的长度是 {}。", s1, len);
}
引用默认是不可变的,就像变量一样。如果你想修改被借用的数据,你需要一个可变引用,用 &mut
表示。然而,Rust 对可变引用强制执行严格的规则以防止数据竞争:
借用规则:
&mut T
)。&T
)。这些规则由编译器强制执行。
// 这个函数接受一个 String 的可变引用
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
fn main() {
// 's' 必须声明为可变,以允许可变借用
let mut s = String::from("hello");
// 借用规则强制执行示例(取消注释行以查看错误):
// let r1 = &s; // 不可变借用 - OK
// let r2 = &s; // 另一个不可变借用 - OK(允许多个不可变借用)
// let r3 = &mut s; // 错误!当存在活跃的不可变借用时,不能将 `s` 作为可变借用
// // Rust 强制执行:要么多个读取者 (&T),要么单个写入者 (&mut T),绝不能同时存在
// println!("{}, {}", r1, r2); // 使用 r1/r2 会使它们保持活跃,从而触发错误
// // 如果没有这个 println,Rust 的 NLL(非词法作用域生命周期)会提前释放 r1/r2,
// // 使得 &mut s 在这里有效
// 这里允许进行可变借用,因为没有其他活跃的借用
change(&mut s);
println!("{}", s); // 输出: hello, world
}
Rust 提供了将多个值组合成更复杂类型的方法。
结构体(Structs,structure 的缩写)允许你通过将相关的数据字段组合在一个名称下,来定义自定义数据类型。
// 定义一个名为 User 的结构体
struct User {
active: bool,
username: String, // 使用有所有权的 String 类型
email: String,
sign_in_count: u64,
}
fn main() {
// 创建 User 结构体的一个实例
// 实例必须为所有字段提供值
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 使用点表示法访问结构体字段
// 实例必须是可变的才能更改字段值
user1.email = String::from("anotheremail@example.com");
println!("用户邮箱: {}", user1.email);
// 使用辅助函数创建 User 实例
let user2 = build_user(String::from("user2@test.com"), String::from("user2"));
println!("用户 2 的活跃状态: {}", user2.active);
// 结构体更新语法:创建一个新实例,
// 使用现有实例的某些字段来填充剩余字段。
let user3 = User {
email: String::from("user3@domain.com"),
username: String::from("user3"),
..user2 // 从 user2 获取 'active' 和 'sign_in_count' 的值
};
println!("用户 3 的登录次数: {}", user3.sign_in_count);
}
// 返回 User 实例的函数
fn build_user(email: String, username: String) -> User {
User {
email, // 字段初始化简写:如果参数名与字段名匹配
username,
active: true,
sign_in_count: 1,
}
}
Rust 还支持元组结构体,它们是命名的元组(例如 struct Color(i32, i32, i32);
),以及类单元结构体,它们没有字段,在你需要在某个类型上实现 Trait 但不需要存储任何数据时很有用(例如 struct AlwaysEqual;
)。
枚举(Enums,enumerations)允许你通过列举其可能的变体来定义一个类型。一个枚举值只能是其可能的变体之一。
// 定义 IP 地址类型的简单枚举
enum IpAddrKind {
V4, // 变体 1
V6, // 变体 2
}
// 枚举变体也可以持有相关联的数据
enum IpAddr {
V4(u8, u8, u8, u8), // V4 变体持有四个 u8 值
V6(String), // V6 变体持有一个 String
}
// Rust 标准库中一个非常常见且重要的枚举:Option<T>
// 它编码了一个可能存在或不存在的值的概念。
// enum Option<T> {
// Some(T), // 表示存在一个类型为 T 的值
// None, // 表示值的缺失
// }
// 另一个关键的标准库枚举:Result<T, E>
// 用于可能成功 (Ok) 或失败 (Err) 的操作。
// enum Result<T, E> {
// Ok(T), // 表示成功,包含一个类型为 T 的值
// Err(E), // 表示失败,包含一个类型为 E 的错误值
// }
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// 创建带有相关数据的 IpAddr 枚举实例
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// Option<T> 示例
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// 'match' 控制流运算符非常适合处理枚举。
// 它允许你根据枚举变体执行不同的代码。
match some_number {
Some(i) => println!("得到一个数字: {}", i), // 如果是 Some,将内部值绑定到 i
None => println!("什么也没有得到。"), // 如果是 None
}
// 'match' 必须是穷尽性的:你必须处理所有可能的变体。
// 下划线 '_' 可以用作通配符模式来捕获任何
// 未显式列出的变体。
}
Option<T>
和 Result<T, E>
是 Rust 健壮地处理潜在缺失值和错误方法的核心。
Cargo 是 Rust 生态系统中不可或缺的一部分,它简化了构建、测试和管理 Rust 项目的过程。这是开发者经常称赞的特性之一。
Cargo.toml
:这是你的 Rust 项目的清单文件。它采用 TOML(Tom's Obvious, Minimal Language)格式编写。它包含关于你项目的重要元数据(如名称、版本和作者),并且至关重要地,列出了项目的依赖项(你的项目所依赖的其他外部 Crate)。
[package]
name = "my_project"
version = "0.1.0"
edition = "2021" # 指定要使用的 Rust 版本(影响语言特性)
# 依赖项在下面列出
[dependencies]
# 示例:添加 'rand' crate 用于生成随机数
# rand = "0.8.5"
# 当你构建时,Cargo 将下载并编译 'rand' 及其依赖项。
cargo new <project_name>
:创建一个新的二进制(可执行)应用程序项目结构。cargo new --lib <library_name>
:创建一个新的库(旨在供其他程序使用的 Crate)项目结构。cargo build
:编译你的项目及其依赖项。默认情况下,它会创建一个未优化的调试构建。输出位于 target/debug/
目录中。cargo build --release
:编译你的项目并启用优化,适用于分发或性能测试。输出位于 target/release/
目录中。cargo run
:编译(如果需要)并运行你的二进制项目。cargo check
:快速检查你的代码是否有编译错误,而不实际生成最终的可执行文件。这通常比 cargo build
快得多,在开发过程中用于快速反馈很有用。cargo test
:运行你项目中定义的任何测试(通常位于 src
目录或单独的 tests
目录中)。当你向 Cargo.toml
文件添加依赖项,然后运行像 cargo build
或 cargo run
这样的命令时,Cargo 会自动处理从中央仓库 crates.io 下载所需的 Crate(以及它的依赖项)并将所有内容编译在一起。
这份入门指南涵盖了让你起步的最基础知识。随着你的进步,Rust 语言提供了许多更强大的功能供你探索:
Result
枚举,使用 ?
操作符进行错误传递,以及定义自定义错误类型。Vec<T>
(动态数组/向量)、HashMap<K, V>
(哈希映射)、HashSet<T>
等。<T>
)编写可以操作不同数据类型的灵活、可重用的代码。'a
)。Mutex
和 Arc
的共享状态,以及 Rust 强大的用于异步编程的 async
/await
语法。Rust 提供了一个独特且引人注目的方案:结合了像 C++ 这样的底层语言所期望的原始性能,以及强大的编译时安全保证,消除了整类的常见错误,尤其是在内存管理和并发方面。虽然学习曲线涉及到内化像所有权和借用这样的新概念(并听从有时严格的编译器),但其回报是软件具有高度可靠性、效率,并且从长远来看通常更易于维护。凭借其通过 Cargo 提供的出色工具和支持性的社区,Rust 是一门值得学习的语言。
从小处着手,使用 Cargo 进行实验,将编译器有用的(尽管有时冗长)错误消息视为指导,你将顺利踏上掌握这门强大且日益流行的语言的道路。祝你 Rust 编程愉快!