Rust初体验:6小时探索所有权、借用和模块系统

本文记录了作者在6小时内学习Rust编程语言的体验,详细介绍了Rust的所有权系统、借用机制、结构体和枚举的使用方法,以及与Java语言的对比。文章包含具体的代码示例和实际操作步骤,适合对Rust感兴趣的开发者阅读。

我的前6小时Rust体验

TLDR; 我今天请了一天假,但大部分时间还是花在了处理差旅费报销上。说到优先级,这完全是我的错。😄

到了午餐时间,所有精力都准备好做些不同的事情。我最终探索了一些Rust的基础知识。我曾听朋友说过,如果一个知名人物(好奇是谁…)反复说"数十亿",人们就会开始相信"数十亿"。不管怎样,我被YouTube推荐完全说服了要探索一下这个语言。这个Rust到底是什么?在我继续之前,先告诉你我喜欢编程语言和解决问题。我不固守一种语言,我欣赏所有语言。除了MS Access(哎呀)。

安装

就像使用Java的sdkman一样,Rust-up是一个很好的入门库。

1
2
3
4
5
6
7
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 创建新项目
cargo new rust_example

# 运行项目
cd rust_example && cargo run

所有权!

在Java中,对象由GC(垃圾收集器)管理,我们都对这种关系有着需要又不喜欢的复杂情感(当然是我的观点)。这也意味着我们主要处理引用。引用的好处是很容易构建这种神奇的对象模型。不太好的地方是管理它和它的内存使用。因此有了"垃圾收集器👽"。

虽然Java为更好的可访问性带来了特定的引用/对象,但Rust采取了相当不同的方法,规定一次只能有一个所有者。看下面的代码示例,它使用了一个类似于StringBuilder的String:

1
2
3
4
let s1 = String::from("hello"); // s1拥有String数据
let s2 = s1;                 // 所有权从s1移动到s2
println!("s1: {}", s1);    // 编译时错误:`s1`的值已被移动
println!("s2: {}", s2);     // s2现在是有效的所有者

在Java中,s1和s2都会指向同一个引用。然而在Rust中,一旦引用有了新的所有者,你就不能这样做了。

总结:在Java中,当你传递对象时,你传递的是引用。代码的多个部分可以持有对同一对象的引用。在Rust中,情况不同。Rust中的每个值都有一个所有者。一次只能有一个所有者。当所有者超出作用域时,值将被丢弃(内存被释放)。

这是一个有趣的概念,我能看出这是有道理的。我可能需要在更广泛的项目中学习和实验这个。

引用和借用

好的,所以现在一切都是被拥有的。很好,但在项目中,定义自己的数据类型或辅助函数时,确实需要共享引用。Rust中的引用就像指针,但也保证指向有效数据,换句话说,我认为"它有一个所有者"。默认情况下,所有引用都是不可变的,除非明确定义。此外,由于一次只能有一个所有者,一次只能有一个可变引用(&mut T)和许多不可变引用(&T)。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 这个函数接受对String的不可变引用
// 它不取得所有权
fn calculate_length(some_string: &String) -> usize {
    some_string.len()
} // `some_string`超出作用域,但它指向的数据不会被丢弃

// 这个函数接受可变引用
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

你可以有一个可变引用(&mut T)或任意数量的不可变引用(&T),但不能同时拥有两者。这是Rust的关键安全保证之一。

结构体和枚举

大多数应用程序需要某种逻辑和数据类型。在Rust中可以定义结构体,例如:

1
2
3
4
5
6
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

这就像只有字段的Java类。本质上,结构体就像键值对。

值可以这样使用或赋值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    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!("User email: {}", user1.email);
}

Rust也有枚举,但与Java有很大不同。枚举是一种可以是几种可能变体的类型。最重要的枚举可能是Option,它处理可空性的概念。虽然Java枚举有一组固定的常量,但Rust在这方面更灵活,支持更复杂的数据结构。这也使得它能够进行模式匹配。例如看下面的代码。枚举中有两种类型Car、Bicycle。match检查available_transport。在Java 21中,模式匹配也被引入作为标准功能,Java 25还有进一步的增强。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enum Transport {
    Car(String),
    Bicycle(String),
}

/// 检查车库并确定哪种交通工具可用
fn check_garage() -> Transport {
    let car_model = String::from("Tesla Model Y");
    Transport::Car(car_model)
}

fn main() {
    let available_transport = check_garage();

    // `match`语句是Rust处理枚举的标准方式
    // 它确保你处理每个可能的变体,使代码更安全
    match available_transport {
        Transport::Car(model) => {
            println!("你今天要开车!是一辆{}。", model);
        }
        Transport::Bicycle(brand) => {
            println!("看来你今天要骑你的{}自行车了。", brand);
        }
    }
}

Self的概念

self类似于Java中的this。在Rust中,它的处理方式显然不同。例如,它是方法中的一个特殊第一个参数,代表调用方法的结构体、枚举或特征对象的具体实例。关键区别在于所有权。

1> &self 作为不可变借用

在这种情况下,方法可以读取实例的数据,但不能修改它。这也意味着调用者保留实例的完全所有权。调用完成后,实例可以像以前一样使用。在下面的例子中,结构体不能被赋值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // 这个方法不可变地借用Rectangle
    fn area(&self) -> u32 {
        self.width = 5; // 这会导致编译错误!无法修改借用数据
        self.width * self.height
    }
}

2> &mut self 作为可变借用

在这种情况下,方法可以读取和修改实例的数据。调用者保留所有权。

如果你想在Java中像setter一样,你会添加&mut来获得所有权。在下面的例子中,我使用Rust标准库中的可增长数组类型Vec创建了一个Stack。对于像push和pop这样的写操作,我使用&mut,但对于像is_empty这样的读操作,我不需要所有权,因此只需&self就足够了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
struct Stack<T> {
    items: Vec<T>,
}

impl<T> Stack<T> {
    // 一个"静态方法"来创建新的空栈
    // 在Rust中,这些被称为关联函数
    fn new() -> Self {
        Stack { items: Vec::new() }
    }

    // 将项目推入栈的方法
    // `&mut self`是实例的可变借用,类似于Java中的`this`
    fn push(&mut self, item: T) {
        self.items.push(item);
    }

    // 弹出项目的方法
    // 它返回Option<T>因为栈可能为空!
    fn pop(&mut self) -> Option<T> {
        self.items.pop()
    }

    // 不可变借用以查看顶部项目
    fn peek(&self) -> Option<&T> {
        self.items.last()
    }

    // 检查栈是否为空
    fn is_empty(&self) -> bool {
        self.items.is_empty()
    }
}

fn main() {
    let mut my_stack = Stack::new();

    my_stack.push(1);
    my_stack.push(2);
    my_stack.push(3);

    println!("顶部项目是: {:?}", my_stack.peek()); // Some(3)

    let popped = my_stack.pop();
    println!("弹出的项目: {:?}", popped); // Some(3)

    println!("栈是否为空? {}", my_stack.is_empty()); // false

    my_stack.pop();
    my_stack.pop();
    let last_pop = my_stack.pop();
    println!("最后一次弹出: {:?}", last_pop); // None

    println!("栈是否为空? {}", my_stack.is_empty()); // true
}

另一个有趣的事情是let popped = my_stack.pop()这一行。我原以为为了安全应该写let popped: Option<i32> = my_stack.pop();但事实并非如此。编译器查看Stack的pop方法签名:fn pop(&mut self) -> Option<T>。由于它知道T是i32,它知道这个特定的pop()调用将返回一个Option类型的值。因此,它自动推断变量popped也必须是Option类型。

好的,这很好,但编译器并不总是能解决这个问题。

何时需要添加类型?

函数签名:Rust要求你声明所有函数参数的类型和函数的返回值。这是一个深思熟虑的设计选择,以确保接口稳定和清晰。

1
2
3
4
5
6
fn process_popped_item(item: Option<i32>) {
    match item {
        Some(number) => println!("处理数字: {}", number),
        None => println!("栈上没有要处理的内容。"),
    }
}

同样的规则也适用于将整个Stack传递给函数时。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 一个只检查栈顶的函数
fn inspect_stack(stack: &Stack<i32>) {
    match stack.peek() {
        Some(top_item) => println!("顶部项目是{}。", top_item),
        None => println!("栈是空的。"),
    }
    // `stack`只是一个引用。`main`中的原始`my_stack`未被触及
}

// 在main中:
// let mut my_stack = ...;
inspect_stack(&my_stack); // 我们使用'&'传递引用

类型不明确时:有时,编译器无法自行解决。一个非常常见的例子是迭代器上的collect()方法,它可以创建许多不同类型的集合。

1
2
3
4
5
6
7
let numbers = (0..10); // 数字迭代器

// 这不会编译。应该是什么类型的集合?
let collected = numbers.collect();

// 这是正确的。我们告诉编译器我们想要Vec<i32>
let collected: Vec<i32> = numbers.collect();

为人类读者提供清晰度:特别是对于复杂类型或长函数链,添加类型注释可以帮助其他开发人员(或未来的自己😄)更快地理解代码,即使编译器不需要它。

3> 使用self获得完全所有权

这种形式的self获得实例的完全所有权。实例被移动到方法中。方法调用后,调用者不能再使用原始实例,因为它已被移动。假设你想创建一个构建器模式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn build(self) -> Result<Pizza, String> {
    if self.size.is_empty() {
        return Err("Size是必填字段。".to_string());
    }

    // 使用构建器的状态创建最终的Pizza
    Ok(Pizza {
        size: self.size,
        // 使用`unwrap_or`为可选字段提供默认值
        has_stuffed_crust: self.has_stuffed_crust.unwrap_or(false),
        toppings: self.toppings.unwrap_or_else(Vec::new), // 默认为空vec
    })
}

// 使用示例
let cheese_pizza = PizzaBuilder::new("Large").build().unwrap();

大写的Self不是变量而是类型别名。

公共和私有

此时我很想了解如何在main.rs之外创建自己的数据类型。要做到这一点,首先需要将我的类型放在另一个文件中。例如,该文件名为bank_account.rs。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pub struct BankAccount {
    owner: String,
    balance: f64,
}

impl BankAccount {
    pub fn new(owner: String) -> Self {
        BankAccount {
            owner, 
            balance: 0.0,
        }
    }

    pub fn deposit(&mut self, amount: f64) {
        if amount > 0.0 {
            self.balance += amount;
            println!("存入: ${}", amount);
        } 
    }

    pub fn balance(&self) -> f64 {
        self.balance
    }
}

此时,如果我不在任何函数前添加pub关键字,我将无法使用它们。所以默认情况下这里的一切都是私有的。然后我可以添加以下行到我想使用这个模块的地方。

1
2
mod bank_account;
use bank_account::BankAccount;

这里看到一个区别。bank_account.rs与mod名称相同,但其中的类型是BankAccount。可能有惯例可以使用,但Rust并不真正限制类型名称。所以在定义Java中的公共类时有点不同。

总结

我确信还有更多内容,但是嘿!学习几个小时作为开始还不错。可以说我喜欢这里的一些语义。它也某种程度上重新连接了我的大脑。最终,语言应该是用于正确工作的正确工具。以下是我今天的一些收获。

通过所有权进行内存管理:与Java的垃圾收集器(GC)不同,Rust使用所有权系统管理内存。每个值都有一个单一所有者,当所有者超出作用域时,内存被释放。将值分配给新变量会移动所有权,使原始变量无效。

借用和引用:为了在不转移所有权的情况下访问数据,Rust使用引用,这个概念被称为"借用"。编译器强制执行一个关键规则:在任何给定时间,你可以有一个可变引用&mut T或任意数量的不可变引用&T,但不能同时拥有两者。

使用结构体和枚举进行数据结构化:Rust使用结构体创建具有命名字段的自定义数据类型,类似于只有成员变量的Java类。它的枚举比Java的更强大,因为变体可以保存数据。这使得使用match语句进行强大的模式匹配成为可能,并且是处理可空性的Option枚举等功能的基础。

方法接收器类型和self:方法中的self参数,类似于Java中的this,明确定义了方法与实例数据的交互方式。它可以是不变借用&self、可变借用&mut self,或者可以取得完全所有权self,这将实例移动到方法中。

模块化和默认隐私:代码被组织到模块中,用mod关键字声明。默认情况下,模块中的所有项(结构体、函数、字段等)都是私有的。必须使用pub关键字使它们公开并从模块外部可访问。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计