rust所有权和借用中的一些case

2024-09-02 16:39:22 浏览数 (2)

前言

学习rust有一段时间了,也用rust写了两个小项目,过程中发现一些rust教程在所有权和引用这一章节的讲解还是不够丰富,有很多case没有讲到,对所有权和引用的理解不够深入,这就导致实际应用时经常卡在所有权和引用,后面查阅一些资料在社区请教一些大佬后才理解,因此将最近练习过程中遇到的一些所有权和引用方面的问题总结成本文,分享给大家,帮大家踩踩坑。

1. 所有权

代码语言:javascript复制
let a = 1;
let b = a; // a拷贝给b
println!("{}", a); // 不会报错

a的值被拷贝给了b,a和b被存储在栈上,无需在堆上分配内存

代码语言:javascript复制
let a = String::from("a");
let b = a;
println!("{}", a); // 会报错,上一行a的所有权转移给了b,a不能再使用了

新手在这里可能会产生疑问?当执行形如let b = a;这样的代码时,到底什么情况下发生拷贝,什么情况下转移所有权呢?问题的答案其实非常简单:

只要a实现了Copy trait,那么就会拷贝,如果没有实现则转移所有权

那么为什么不能拷贝呢?我们可以以String这个类型为例,String是一个复杂类型,由存储在栈上的堆指针、字符串长度、字符串容量组成。

我们假设这里也是拷贝,那么a和b都会持有这个堆指针,当变量离开作用域后,rust会自动清理堆内存,由于a和b都指向了同一位置,那么会释放两次,这就导致了bug。

因此rust这样解决问题:当a赋值给b后,rust认为a不再有效,因此a离开作用域之后不会二次释放,这就是把所有权从a转移到了b。a被赋值给b之后就失效了,因此不能再使用。

如果String实现了Copy trait,拷贝a给b时,把堆指针指向的数据也复制一遍,同时将新的堆指针给b,那么a和b就不会指向同一个位置,就不会二次释放,自然就不会发生二次释放的bug了。

以下类型实现了Copy trait * 所有整数类型,比如 u32 * 布尔类型,bool,它的值是 true 和 false * 所有浮点数类型,比如 f64 * 字符类型,char * 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是 * 不可变引用 &T,注意: 可变引用 &mut T 是不可以 Copy的(如果Copy相当于两个指针指向一个位置,又会出现上面的二次释放的问题了)

1.1 结构体

结构体所有权问题比较复杂,这里单独拿出来分析。

先看一个简单的

代码语言:javascript复制
struct User {
    age:
}

let user1 = User {
    age: 100,
};

let user2 = user1;
println!("{:}", user1); // 会报错,因为User没有实现Copy trait,所以user1的所有权转移给了user2
println!("{:}", user1.sign_in_count); // 会报错,user1已经无法使用了

这里要注意,虽然user1分配在栈上,但它没有实现Copy trait,仍然会发生所有权的转移

再看看一个复杂的

代码语言:javascript复制
struct User {
    username: String,
    age: i128,
}

let user1 = User {
    username: String::from("user1"),
    age: 100,
};

let user2 = User {
    username: user1.username,
    age: user1.age
};

println!("{}", user1.age); // 不会报错,age发生了copy,而非所有权转移,可以继续使用
println!("{}", user1.username); // 会报错,username发生了所有权的转移
println!("{:}", user1); // 会报错

这里需要注意的是结构体内部的字段发生所有权转移后,会导致结构体本身也无法继续使用。但是其内部发生copy的值还是可以继续使用的,也就是user1.age还能继续使用不会报错的原因。

1.2 Option 所有权转移问题

我们先明确一个规则: 只要Option<T>中的T实现了Copy trait,那么Option<T>就实现了Copy trait

代码语言:javascript复制
let a = Some(String::from("hello world!"));
let b = a.unwrap();
let c = a.unwrap(); // 这里会报错

我们分析下报错的原因,首先看unwrap的源码

代码语言:javascript复制
pub const fn unwrap(self) -> T {
    match self {
        Some(val) => val,
        None => unwrap_failed(),
    }
}

从上面可以看到,调用unwrap时,因为Option<String>没有实现Copy trait,所以a发生了所有权转移,a的所有权转移到了unwrap里,所以第二次调用unwrap时就会报错。

解决办法就是调用as_ref/as_mut或者将Option<String>换成Option<&String>,rust中引用默认实现了Copy trait,所以Opiton<&String>不会发生所有权转移 看下as_ref的源码:

代码语言:javascript复制
pub const fn as_ref(&self) -> Option<&T> {
    match *self {
        Some(ref x) => Some(x),
        None => None,
    }
}

2. 引用

2.1 可变引用

只能可变的引用一个可变变量

代码语言:javascript复制
let a = 1;
let b = &mut a; // 会报错,无法可变引用一个不可变变量

同一时刻只能存在一个可变引用

代码语言:javascript复制
let mut a = 1;
let b = &mut a;
*b = 2;
println!("{}", a); // 会报错,可以将a理解成1的一个引用,因为下一行println!("{}", b);所以b这个可变引用的生命周期还未结束,那么此时如果使用a,则违反了可变引用与不可变引用不能同时存在的规则
println!("{}", b);

2.2 解引用

结构体解引用

代码语言:javascript复制
let user = String::from("user");
let user_ref = &user;
let _user_1 = *user_ref; // 报错

第三行会报错:

代码语言:javascript复制
error[E0507]: cannot move out of `*user_ref` which is behind a shared reference
  --> src/main.rs:30:19
   |
30 |     let _user_1 = *user_ref;
   |                   ^^^^^^^^^ move occurs because `*user_ref` has type `String`, which does not implement the `Copy` trait

这个报错看到有解释说不能解引用获取到所有权(String没有实现Copy trait只能将user的所有权转移给_user_1),但是这里将user的所有权转移给_user_1也并不会造成什么错误,所以我猜测是rust编译器限制了不能通过解引用间接转移所有权,只能直接转移。

这里还有个case:let _user_1 = &(*user_ref); 这种写法可以编译通过,猜测是编译器优化直接拷贝的引用,而不是先转移所有权再取引用。

3. 参考资料

  • 有关解引用会获得所有权的理解
  • 所有权 - Rust语言圣经(Rust Course)

0 人点赞