我的前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关键字使它们公开并从模块外部可访问。