MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust深入理解Rust的基本概念

2021-08-014.0k 阅读

Rust 基本概念之变量与数据类型

变量绑定与可变性

在 Rust 中,使用 let 关键字来声明变量。例如:

let x = 5;

这里,x 被绑定到值 5。默认情况下,Rust 中的变量是不可变的。这意味着一旦绑定了一个值,就不能再改变它。如果试图改变不可变变量的值,编译器会报错:

let x = 5;
x = 6; // 编译错误:不能重新赋值给不可变变量 `x`

要创建可变变量,需要在声明时使用 mut 关键字:

let mut y = 5;
y = 6; // 合法,`y` 是可变变量

不可变变量有助于编写更安全、可预测的代码,因为它们的值不会在程序的其他地方意外改变。而可变变量则在需要修改值的场景下非常有用,比如在循环中更新计数器。

数据类型

Rust 有丰富的数据类型,大致分为两类:标量类型和复合类型。

  1. 标量类型
    • 整数类型:Rust 提供了多种整数类型,根据有无符号和位宽区分。例如,i32 是 32 位有符号整数,u8 是 8 位无符号整数。
let a: i32 = 42;
let b: u8 = 255;
- **浮点类型**:`f32` 和 `f64` 分别表示 32 位和 64 位浮点数。在 Rust 中,默认的浮点类型是 `f64`,因为在现代硬件上它的性能与 `f32` 相近,且精度更高。
let c: f32 = 3.14159;
let d: f64 = 2.71828;
- **布尔类型**:`bool` 类型只有两个值,`true` 和 `false`。
let is_true: bool = true;
let is_false: bool = false;
- **字符类型**:`char` 类型表示一个 Unicode 标量值,占用 4 个字节。它可以表示任意的 Unicode 字符,包括字母、数字、符号等。
let ch: char = 'A';
let emoji: char = '😀';
  1. 复合类型
    • 元组:元组是一个固定大小的、可以包含不同类型元素的有序集合。元组的长度在声明时就确定,不能动态改变。
let tup: (i32, f64, bool) = (500, 6.4, true);

可以通过模式匹配或索引来访问元组中的元素:

let tup = (500, 6.4, true);
let (x, y, z) = tup; // 模式匹配
println!("The value of y is: {}", y);

let tup = (500, 6.4, true);
let second = tup.1; // 通过索引访问
println!("The second element of the tuple is: {}", second);
- **数组**:数组也是一个固定大小的集合,但数组中的所有元素必须是相同类型。
let arr: [i32; 5] = [1, 2, 3, 4, 5];

数组的长度是类型的一部分,这里 [i32; 5] 表示包含 5 个 i32 类型元素的数组。可以通过索引访问数组元素:

let arr = [1, 2, 3, 4, 5];
let third = arr[2];
println!("The third element of the array is: {}", third);

Rust 基本概念之所有权系统

所有权规则

  1. 每个值都有一个所有者:在 Rust 中,每个值在内存中都有一个对应的所有者。当值的所有者离开其作用域时,该值会被自动清理。
  2. 同一时刻,一个值只能有一个所有者:这确保了 Rust 能够精确地管理内存,避免了诸如悬空指针和双重释放等常见的内存错误。
  3. 当所有者离开作用域时,值会被丢弃:Rust 有一个特殊的函数 drop,当值的所有者离开作用域时,drop 函数会被自动调用,用于释放值所占用的资源。

所有权转移

当将一个值赋给另一个变量时,所有权会发生转移。例如:

let s1 = String::from("hello");
let s2 = s1; // s1 的所有权转移到 s2
// println!("{}", s1); // 这行代码会报错,因为 s1 不再拥有所有权

在上述代码中,String::from("hello") 创建了一个堆上的字符串值,s1 成为这个值的所有者。当 s2 = s1 执行时,s1 的所有权转移到 s2s1 不再能访问这个字符串值。如果试图在所有权转移后使用 s1,编译器会报错。

借用

有时,我们希望在不转移所有权的情况下访问一个值。这时可以使用借用的概念。借用允许我们创建一个对值的引用,而不是获取所有权。引用使用 & 符号。例如:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

calculate_length 函数中,s 是一个对 String 的引用。函数只借用 s1,而不获取所有权,所以在函数调用结束后,s1 仍然拥有字符串的所有权,可以继续使用。

可变借用

除了不可变借用,Rust 还支持可变借用。可变借用允许我们修改被借用的值,但有一些严格的规则。可变借用使用 &mut 符号。例如:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

在上述代码中,s 被声明为可变变量,change 函数接受一个可变引用 &mut String。通过可变引用,函数可以修改字符串 s 的内容。不过,Rust 规定在同一时刻,对于同一个数据,要么只能有一个可变引用(以避免数据竞争),要么只能有多个不可变引用(因为不可变引用不会修改数据,所以可以同时存在多个)。

Rust 基本概念之生命周期

生命周期标注

在 Rust 中,生命周期标注用于明确引用的生命周期。生命周期标注的语法使用单引号('),例如 'a。函数参数和返回值中的生命周期标注需要与实际使用的引用的生命周期相匹配。例如:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

longest 函数中,'a 是一个生命周期参数,它标注了 xy 和返回值的生命周期。这意味着函数返回的引用的生命周期至少要和传入的两个引用中生命周期较短的那个一样长。

生命周期省略规则

在很多情况下,Rust 可以根据一些规则自动推断出引用的生命周期,这些规则被称为生命周期省略规则。

  1. 输入生命周期标注:每个函数参数的生命周期是一个单独的生命周期参数。
  2. 输出生命周期标注:如果函数只有一个输出引用,那么输出引用的生命周期与第一个输入引用的生命周期相同。
  3. 多个输出引用:如果函数有多个输出引用,那么需要显式标注生命周期。

例如,在下面的函数中,虽然没有显式标注生命周期,但 Rust 可以根据规则推断出来:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

这里 s 是输入引用,返回值也是引用。由于函数只有一个输入引用和一个输出引用,Rust 可以推断出输出引用的生命周期与输入引用 s 的生命周期相同。

Rust 基本概念之函数

函数定义

在 Rust 中,使用 fn 关键字来定义函数。函数定义包括函数名、参数列表、返回值类型(如果有)和函数体。例如:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

在这个 add 函数中,接受两个 i32 类型的参数 ab,返回它们的和,返回值类型也是 i32。函数体中的最后一个表达式的值会作为函数的返回值,这里不需要显式的 return 关键字。如果需要提前返回,可以使用 return 关键字:

fn subtract(a: i32, b: i32) -> i32 {
    if a < b {
        return 0;
    }
    a - b
}

函数参数

函数参数是函数定义中括号内声明的变量。每个参数都需要指定类型。例如:

fn print_number(n: f64) {
    println!("The number is: {}", n);
}

print_number 函数中,n 是一个 f64 类型的参数,函数会打印出这个数字。

函数调用

调用函数时,需要提供与函数定义中参数列表匹配的参数值。例如:

fn main() {
    let result = add(3, 5);
    println!("The result of addition is: {}", result);
    print_number(3.14);
}

这里先调用 add 函数计算 3 和 5 的和,并将结果打印出来,然后调用 print_number 函数打印 3.14

Rust 基本概念之控制流

if 语句

if 语句用于根据条件执行不同的代码块。条件必须是 bool 类型。例如:

let num = 5;
if num > 3 {
    println!("The number is greater than 3");
} else {
    println!("The number is less than or equal to 3");
}

在上述代码中,num > 3 是条件表达式,根据这个条件的结果,会执行不同的代码块。if 语句也可以没有 else 块:

let num = 2;
if num > 3 {
    println!("The number is greater than 3");
}

此外,if 语句还可以用于赋值。例如:

let condition = true;
let number = if condition { 5 } else { 6 };
println!("The number is: {}", number);

这里 if 语句的结果被赋值给 number 变量,注意 ifelse 块返回的值类型必须相同。

循环

  1. loop 循环loop 用于创建一个无限循环,直到使用 break 关键字退出。例如:
let mut counter = 0;
loop {
    counter += 1;
    if counter == 3 {
        break;
    }
    println!("Counter: {}", counter);
}

在这个例子中,loop 循环会不断增加 counter 的值,当 counter 等于 3 时,使用 break 退出循环。 2. while 循环while 循环根据条件判断是否继续循环。例如:

let mut num = 5;
while num > 0 {
    println!("{}", num);
    num -= 1;
}

这里 num > 0 是循环条件,只要条件为 true,就会执行循环体中的代码,每次循环结束后 num 的值减 1,直到 num 不大于 0 时循环结束。 3. for 循环for 循环用于遍历可迭代对象,如数组、向量等。例如:

let arr = [1, 2, 3, 4, 5];
for num in arr.iter() {
    println!("{}", num);
}

在这个例子中,arr.iter() 返回一个迭代器,for 循环会依次从迭代器中取出元素并赋值给 num,然后执行循环体。for 循环还可以用于指定范围的循环:

for i in 1..4 {
    println!("{}", i);
}

这里 1..4 表示从 1 到 3 的范围,for 循环会依次将 1、2、3 赋值给 i 并执行循环体。

Rust 基本概念之结构体

结构体定义

结构体是一种自定义的数据类型,它允许将不同类型的数据组合在一起。使用 struct 关键字定义结构体。例如:

struct Point {
    x: i32,
    y: i32,
}

Point 结构体中,定义了两个字段 xy,类型都是 i32。要创建结构体的实例,可以使用以下方式:

let p1 = Point { x: 10, y: 20 };

也可以通过指定字段顺序来创建实例:

let p2 = Point { y: 30, x: 40 };

结构体方法

结构体可以有自己的方法。方法是定义在结构体上下文中的函数。要定义结构体方法,需要使用 impl 块。例如:

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

在上述代码中,Rectangle 结构体有两个字段 widthheight。在 impl 块中定义了一个 area 方法,该方法计算并返回矩形的面积。&self 表示方法接收一个对结构体实例的不可变引用,这样方法可以访问结构体的字段。要调用结构体方法,可以这样做:

let rect = Rectangle { width: 10, height: 20 };
let area = rect.area();
println!("The area of the rectangle is: {}", area);

结构体与所有权

结构体中的字段可以包含具有所有权的值,如 String。例如:

struct User {
    username: String,
    email: String,
}

let user1 = User {
    username: String::from("john_doe"),
    email: String::from("john@example.com"),
};

user1 离开作用域时,usernameemail 字段所占用的内存会被自动释放,因为 user1 拥有这些 String 值的所有权。

Rust 基本概念之枚举

枚举定义

枚举是一种自定义数据类型,它允许定义一组命名的值。使用 enum 关键字定义枚举。例如:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

在这个 Coin 枚举中,定义了四个可能的值:PennyNickelDimeQuarter。要创建枚举的实例,可以这样做:

let my_coin = Coin::Penny;

带数据的枚举

枚举值可以携带数据。例如:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

在这个 Message 枚举中,Quit 不携带数据,Move 携带一个包含 xy 字段的结构体数据,Write 携带一个 String 数据,ChangeColor 携带三个 i32 数据。创建实例的方式如下:

let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("Hello, world!"));
let m4 = Message::ChangeColor(255, 0, 0);

match 语句与枚举

match 语句是 Rust 中用于模式匹配的强大工具,它与枚举配合使用非常方便。例如:

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

value_in_cents 函数中,match 语句根据 coin 的值进行模式匹配,并返回相应的分值。match 语句要求必须覆盖所有可能的枚举值,否则编译器会报错,这确保了代码的完整性和安全性。

Rust 基本概念之模块系统

模块定义

模块用于组织代码,将相关的代码分组在一起。使用 mod 关键字定义模块。例如,在一个文件中可以这样定义模块:

mod my_module {
    pub fn my_function() {
        println!("This is my function in my_module");
    }
}

这里定义了一个名为 my_module 的模块,模块内有一个 my_function 函数。默认情况下,模块内的项(函数、结构体等)是私有的,外部代码无法访问。要使模块内的项可被外部访问,需要使用 pub 关键字。

模块的使用

要使用模块内的项,需要通过模块路径来访问。例如:

fn main() {
    my_module::my_function();
}

这里通过 my_module::my_function() 调用了 my_module 模块内的 my_function 函数。模块可以嵌套定义,例如:

mod outer_module {
    pub mod inner_module {
        pub fn inner_function() {
            println!("This is inner_function in inner_module");
        }
    }
}

要调用 inner_function,可以使用完整路径:

fn main() {
    outer_module::inner_module::inner_function();
}

use 关键字

use 关键字用于导入模块中的项,这样可以在当前作用域内直接使用这些项,而不需要每次都使用完整路径。例如:

use outer_module::inner_module::inner_function;
fn main() {
    inner_function();
}

通过 use 导入后,就可以直接调用 inner_function 了。use 还支持重命名导入的项,例如:

use outer_module::inner_module::inner_function as new_name;
fn main() {
    new_name();
}

这样就将 inner_function 重命名为 new_name 进行使用。

通过深入理解这些 Rust 的基本概念,开发者能够更好地编写安全、高效且易于维护的 Rust 程序,为进一步掌握 Rust 的高级特性打下坚实的基础。