Rust 是一种系统编程语言,旨在提供高性能、内存安全和并发性。它由 Mozilla 研究院开发,第一个正式版本(1.0)发布于 2015 年。Rust 的设计融合了静态类型语言的性能和安全性,以及现代语言的便利特性,使其成为系统编程和高性能应用开发的强有力工具。
Rust 诞生的背景主要是解决系统编程中的一些长期挑战,尤其是内存安全问题、并发编程的复杂性和系统开发的难度。在 Rust 出现之前,C 和 C++ 是系统编程的主要语言。虽然它们提供了底层硬件访问和高性能,但这两种语言的开发者需要手动管理内存,并且它们的语法不足以安全地处理并发,容易导致内存泄漏、悬垂指针和数据竞争等问题。
Rust 的设计理念主要围绕以下几个核心目标:
内存安全:通过所有权系统(ownership)、借用(borrowing)和生命周期(lifetimes)的概念,Rust 在编译时防止了悬垂指针和数据竞争等内存安全问题,无需垃圾收集器。
并发安全:Rust 的所有权和类型系统设计也旨在使并发编程更安全、更容易。Rust 鼓励使用消息传递而不是共享内存来进行线程间通信,从而避免了数据竞争。
性能:Rust 生成的代码旨在与 C 和 C++ 的代码性能相当。Rust 没有运行时(runtime)或垃圾收集器,这使得它可以用于性能敏感的应用场景,如操作系统、游戏引擎和浏览器组件开发。
零成本抽象(Zero-cost abstractions):Rust 的高级抽象,如迭代器和闭包,旨在编译成与手写低级代码一样高效的机器码。
工具生态:Rust 强调工具链的重要性,提供了包管理和构建工具 Cargo、集成测试框架和详细的文档,使得开发和维护 Rust 项目更加方便。
可靠性和稳定性:Rust 的语言设计和严格的编译器检查旨在减少程序中的错误,提高软件的可靠性。同时,Rust 保证向后兼容,使得语言和生态系统随着时间的推移而稳定发展。
Rust 通过这些设计理念,试图提供一个既安全又高效的系统编程语言选项,它解决了传统系统编程语言在安全性和并发编程上的一些困难,同时保持了高性能的特点。这些特性使 Rust 在系统编程、Web 应用、嵌入式开发等领域越来越受到欢迎。
打开终端。
使用 cargo new 命令创建一个新的 Rust 项目。Cargo 是 Rust 的包管理器和构建系统,它可以帮助你管理项目的依赖、编译代码和运行测试。运行以下命令创建一个名为 hello_world 的项目:
cargo new hello_world
进入项目目录:
cd hello_world
Cargo 创建的新项目包含一个简单的 "Hello, World!" 程序。你可以在 src 目录下的 main.rs 文件中找到它。使用文本编辑器打开 src/main.rs 文件,你会看到如下代码:
- fn main() {
- println!("Hello, World!");
- }
这段代码定义了一个 main 函数,这是每个可执行 Rust 程序的入口点。println! 是一个宏,用于向控制台输出一行文本。
回到终端,确保你仍在项目目录 hello_world 中,然后执行以下命令来编译并运行程序:
cargo run
当你运行 cargo run 命令时,Cargo 会自动编译项目中的代码(如果需要的话),并运行生成的可执行文件。你应该会在终端看到输出:Hello, World!
Rust 是一种系统编程语言,旨在提供内存安全、并发性和性能。它的设计特别注重安全和速度,是一种编译型语言。让我们来深入了解 Rust 中的基本数据类型。
Rust 有几种不同的整型,这些整型可以是有符号的或无符号的。有符号整型可以存储包括负数在内的数值,而无符号整型只能存储非负数。这些整型的大小(即它们可以存储的数值范围)根据它们的位数(如 8 位、16 位、32 位、64 位和 128 位)而变化。
i8、i16、i32、i64、i128 和 isize(指针大小)u8、u16、u32、u64、u128 和 usize(指针大小)isize 和 usize 类型依赖于运行程序的计算机架构:64 位架构上是 64 位,32 位架构上是 32 位。
Rust 有两种基本的浮点数类型,这两种类型都是有符号的:
f32:32 位浮点数f64:64 位浮点数(默认类型)浮点数用于表示有小数点的数。f64 有更高的精度,并且在现代CPU上通常与 f32 一样快。
Rust 中的布尔型非常简单,有两个可能的值:
truefalse布尔类型在 Rust 中用 bool 表示。
Rust 的字符类型 char 是一个 Unicode 标量值,它表示一个有效的 Unicode 字符,比如字母、数字、符号或空格。char 类型用单引号表示,例如 'a'、'1' 或 '🎉'。
Rust 有两个主要的字符串类型:String 和 &str。
String 类型是可增长的、可变的、有所有权的 UTF-8 字符序列。&str 类型通常被称为字符串切片(string slice),它是一个对存储在别处的 UTF-8 编码字符串数据的引用。字符串切片是静态的,不能更改其中的内容,而 String 可以修改,可以增长或缩小。
Rust 通常不支持隐式类型转换(也称为类型强制),但提供了显式类型转换的机制。例如,使用 as 关键字进行类型转换:
- let x: i32 = 5;
- let y: u64 = x as u64;
Rust 的类型系统和所有权模型是为了最大程度地保证代码安全性和效率,了解和正确使用这些基本数据类型对于有效地编写 Rust 程序至关重要。
在 Rust 中,变量和可变性的概念是理解内存安全和并发编程的基础。Rust 的设计哲学旨在提供高性能且安全的内存管理,而对变量的处理方式正是这一哲学的体现。
在 Rust 中,默认情况下,变量是不可变的(immutable)。这意味着一旦给变量赋值之后,就不能更改这个值。不可变性有助于并发编程时的安全性,因为它防止了数据竞争。
- let x = 5;
- // x = 6; // 这会导致编译错误,因为 x 默认是不可变的
要声明一个可变变量,需要在变量名前使用 mut 关键字。
- let mut y = 5;
- y = 6; // 这是合法的,因为 y 被声明为可变的
可变性使得 Rust 在需要改变数据时更加灵活。它允许你在必要时对数据进行修改,同时通过默认的不可变性来鼓励更安全的编程模式。
Rust 允许变量遮蔽,这意味着你可以声明一个与之前变量同名的新变量。新变量会“遮蔽”掉旧的变量,使得旧的变量不再可用。这并不是改变一个变量的值,而是用同一个名称绑定到一个新的值上。
- let x = 5;
- let x = x + 1;
- let x = x * 2;
- println!("The value of x is: {}", x); // 输出: The value of x is: 12
在上面的例子中,我们三次使用 let 关键字声明了 x,每次都用一个新值绑定 x。这不会影响原始的 x 值,而是每次都创建了一个新的 x。
变量遮蔽和可变性是两个不同的概念:
遮蔽允许你改变变量的类型或者重新初始化变量的值而不需要可变性。例如,你可以把一个整型变量遮蔽成一个字符串类型的变量。
- let spaces = " ";
- let spaces = spaces.len();
在这个例子中,首先 spaces 是一个字符串,然后我们遮蔽了它,将其变为它的长度,一个整数。
Rust 中的这些特性——不可变性、可变性和变量遮蔽——共同工作,以帮助你写出更安全、更清晰的代码。通过默认的不可变性,Rust 鼓励你以一种线程安全的方式来编程。当你需要更改数据时,mut 关键字和变量遮蔽提供了灵活性,同时保持了代码的清晰度。
在 Rust 中,控制流语句允许你根据条件执行代码块,或者重复执行代码块直到满足某个条件。Rust 提供了多种控制流结构,包括 if 语句和三种循环语句:loop、while 和 for。理解这些控制流机制对于编写逻辑复杂的程序是非常重要的。
if 语句if 语句允许你根据条件表达式的真假来决定是否执行某个代码块。在 Rust 中,if 语句的条件表达式必须是布尔值(true 或 false)。
- let number = 3;
-
- if number < 5 {
- println!("condition was true");
- } else {
- println!("condition was false");
- }
Rust 的 if 语句还可以支持多个条件通过 else if 检查:
- let number = 6;
-
- if number % 4 == 0 {
- println!("number is divisible by 4");
- } else if number % 3 == 0 {
- println!("number is divisible by 3");
- } else if number % 2 == 0 {
- println!("number is divisible by 2");
- } else {
- println!("number is not divisible by 4, 3, or 2");
- }
loop 循环loop 关键字创建了一个无限循环。循环将一直执行直到你明确要求退出,通常是通过 break 语句。
- let mut count = 0;
- loop {
- count += 1;
- println!("again!");
- if count == 5 {
- break;
- }
- }
while 循环while 循环在给定的条件表达式保持为真的情况下重复执行一个代码块。
- let mut number = 3;
-
- while number != 0 {
- println!("{}!", number);
- number -= 1;
- }
-
- println!("LIFTOFF!!!");
for 循环for 循环用于遍历集合中的每个元素,并执行一个代码块。它是处理集合元素的首选方法,因为它既安全又简洁。
- let a = [10, 20, 30, 40, 50];
-
- for element in a.iter() {
- println!("the value is: {}", element);
- }
使用 for 循环和一个范围来重写 while 循环的倒计时示例:
- for number in (1..4).rev() {
- println!("{}!", number);
- }
- println!("LIFTOFF!!!");
这些控制流结构为 Rust 程序提供了强大的逻辑能力,使得根据条件执行代码、重复执行代码直到条件满足、以及遍历数据结构变得简单而直观。使用这些结构可以帮助你编写既高效又易于理解的 Rust 代码。
Rust 的所有权系统是其最独特的特性之一,它使 Rust 能够在没有垃圾收集的情况下保证内存安全。所有权系统基于三个核心规则,管理堆内存并防止数据竞争。理解所有权、借用和生命周期是深入学习 Rust 的关键。
所有权的规则如下:
这些规则确保 Rust 在编译时就能检查出悬垂指针、双重释放等内存安全问题。
变量从声明的地方开始到当前作用域结束都是有效的。当变量离开作用域时,Rust 自动调用 drop 函数并清理背后的堆内存。
Rust 没有数据深拷贝的概念,相反,它使用所有权和浅拷贝/移动语义来处理数据。例如,当一个变量赋值给另一个变量时,原始数据会被移动到新变量,原变量将不再有效。
Rust 通过引用(&)允许你访问数据而不取得其所有权,这被称为 借用。Rust 通过借用规则在编译时强制执行内存安全:
这些规则防止了数据竞争,数据竞争发生在两个或更多指针同时访问同一数据,至少有一个指针被用来写入数据,并且没有同步数据访问的机制。
生命周期是 Rust 中的一个关键概念,它确保了引用总是指向有效的数据。简单来说,生命周期用于描述多个引用的生存时间在程序的不同部分如何相互关联。
在函数和结构体定义中,生命周期注解语法允许你指定引用的生命周期。生命周期注解并不改变任何引用的实际生存时间,而是允许 Rust 分析引用之间是否有效。
没有生命周期注解的函数签名可能无法编译,因为 Rust 编译器无法确定返回的引用是否总是有效的。
- fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
- if x.len() > y.len() {
- x
- } else {
- y
- }
- }
在这个例子中,生命周期注解 'a 描述了参数 x 和 y 的引用与返回值生存时间的关系。它告诉 Rust longest 函数返回的引用将至少与传入的两个引用中生命周期较短的那个一样长。
Rust 的所有权、借用和生命周期是相互关联的概念,共同构成了 Rust 语言安全管理内存的基石。通过这些机制,Rust 能够在编译时避免诸如空悬指针、双重释放等错误,无需垃圾收集器的介入。这些特性使 Rust 特别适合系统编程,例如操作系统、文件系统和游戏引擎,其中性能和安全性至关重要。
Rust 中的切片类型允许你引用集合中的一段连续的元素序列,而不是整个集合。切片是一种没有所有权的数据类型,它是对集合中一部分元素的引用。使用切片非常有用,因为它们让你能够安全地访问数组或字符串的部分元素,而不需要复制它们的数据。切片类型在处理字符串或数组时特别有用,当你需要访问一部分元素而不是整个集合时。
在 Rust 中,切片使用 & 符号从数据结构中借用值。切片的类型表示为 &[T],其中 T 表示元素的类型。对于字符串切片,它被特别表示为 &str。
切片可以通过从数组或字符串借用值来创建。
- let arr = [1, 2, 3, 4, 5];
-
- let slice = &arr[1..4]; // 包含从索引1开始到3(不包括4)的元素
这里,hello 和 world 都是字符串 s 的切片,分别包含 "hello" 和 "world"。
切片非常有用,因为它们让你能够引用集合的部分元素,而不需要复制它们的数据。这在处理大型数据集或字符串时尤其重要,因为它可以提高效率和性能。
例如,在处理字符串数据时,你可能需要检查字符串的一部分是否符合某些条件,而不需要处理整个字符串。通过使用切片,你可以轻松地只关注字符串的相关部分。
使用切片时,有几个重要的注意事项:
通过切片,Rust 在保持高效内存使用的同时,提供了一种安全且灵活的方式来处理数据集合的部分元素。这是 Rust 提供的众多强大功能之一,使得 Rust 在处理性能敏感的任务时非常有用。
Rust 提供了几种强大的集合类型来存储多个值。这些集合类型包括:Vec、String 和 HashMap。每种类型都有其用例和操作方法。
Vec 是一个可增长的数组类型,可以存储多个同类型的值。它在堆上分配空间,可以动态地增加或减少其大小。
- let mut v: Vec<i32> = Vec::new(); // 使用 Vec::new 创建一个空的向量
- let v = vec![1, 2, 3]; // 使用 vec! 宏创建并初始化一个向量
- let mut v = Vec::new();
- v.push(5); // 向向量添加元素
- v.push(6);
- let third: &i32 = &v[2]; // 使用索引直接访问,可能 panic
- let third: Option<&i32> = v.get(2); // 使用 get 方法访问,安全
- for i in &v {
- println!("{}", i);
- }
String 类型是一个可增长的 UTF-8 编码字符串。
- let mut s = String::new(); // 创建一个空的 String
- let data = "initial contents";
- let s = data.to_string(); // 从 &str 类型创建 String
- let mut s = String::from("foo");
- s.push_str("bar"); // 向 String 添加字符串切片
- s.push('!'); // 向 String 添加单个字符
+ 运算符或 format! 宏拼接字符串- let s1 = String::from("Hello, ");
- let s2 = String::from("world!");
- let s3 = s1 + &s2; // 注意 s1 被移动了,不能再次使用
-
- let s = format!("{}-{}", s2, s3); // 使用 format! 宏
HashMap 存储一个键类型 K 到值类型 V 的映射。它通过哈希函数来实现键的快速查找。
- use std::collections::HashMap;
-
- let mut scores = HashMap::new();
- scores.insert(String::from("Blue"), 10);
- scores.insert(String::from("Yellow"), 50);
- let team_name = String::from("Blue");
- let score = scores.get(&team_name); // 返回 Option<&V>
- for (key, value) in &scores {
- println!("{}: {}", key, value);
- }
Vec 用于存储多个同类型值的动态数组。String 是可变的、UTF-8 编码的字符串类型。HashMap 存储键值对,提供快速的查找。这些集合类型在 Rust 中非常常用,熟练掌握它们的使用对于编写 Rust 程序非常重要。每种类型都有其特定的用途和优化点,了解它们的工作原理和适用场景可以帮助你更高效地使用 Rust。
在 Rust 中,错误处理是通过两个主要的枚举来完成的:Option 和 Result。这两种枚举提供了一种在类型级别上处理可能的错误或缺失值的方式,而不是依赖异常或其他运行时机制。这种方法增强了程序的可靠性和可预测性,因为所有的错误都必须显式地处理,这是在编译时检查的。
Option 枚举用于表示一个可能存在或不存在的值。这在函数可能不返回值的情况下特别有用,而不是返回一个无效或特殊的值(如 null 或 -1)。
Option 有两个变体:
Some(T): 表示有一个类型为 T 的值。None: 表示没有值。- fn divide(numerator: f64, denominator: f64) -> Option<f64> {
- if denominator == 0.0 {
- None
- } else {
- Some(numerator / denominator)
- }
- }
-
- let result = divide(2.0, 3.0);
- match result {
- Some(x) => println!("Result: {}", x),
- None => println!("Cannot divide by 0"),
- }
Result 枚举用于可能成功返回结果或出现错误的操作。这对于文件操作或网络请求等可能失败的操作非常有用。
Result 有两个变体:
Ok(T): 表示操作成功,并包含操作的结果。Err(E): 表示操作失败,并包含错误信息。- use std::fs::File;
- use std::io::Error;
-
- fn open_file(path: &str) -> Result
{ - let f = File::open(path);
- f
- }
-
- match open_file("hello.txt") {
- Ok(file) => println!("File opened successfully."),
- Err(e) => println!("Failed to open the file: {:?}", e),
- }
match 语句可以显式地处理每种可能的情况。unwrap 或 expect 方法: 这些方法可以用于快速访问 Option 或 Result 中的值,但如果值为 None 或 Err,程序会 panic。? 运算符: 在返回 Result 的函数中,? 运算符可用于提早返回错误,使得错误处理更加简洁。- use std::fs::File;
-
- fn open_file(path: &str) -> Result
{ - let f = File::open(path)?;
- Ok(f)
- }
在这个例子中,如果 File::open 返回 Err,那么 Err 将会从 open_file 函数提早返回。如果 File::open 成功,其返回的 Ok 中的值将被解包,并继续执行函数的剩余部分。
Rust 的错误处理模型鼓励开发者显式地处理所有可能的错误情况,这样可以减少运行时错误和不确定的行为,提高程序的整体质量和可靠性。