Rust所有权和移动

38 次浏览
2024年07月12日创建

Rust中的赋值语句会导致前数据丢失:

    let mut s = "moving".to_string();
    let t = s;
    print!("s{}",s);

在底层逻辑里,"moving"字符串所有权本属于s,但通过赋值t=s,将所有权从s变成t,此时s变成了未初始化,再通过print就会报错。但这种现象不会发生在int、float等Copy类型数据上,所以对于这种不会发生移动的类型我们需要好好捋清楚。

上面是简单的赋值语句,下面是方法入参:

fn main{
    let vec = vec![32, 40];
    print_vec(vec);
    print!("vec[0] {}", vec[0]);
}
fn print_vec(vec:Vec<i32>){
    for item in vec {
        print!("item {}", item);
    }
}

上面的代码,也会发生编译报错,原因是当vec通过print_vec函数调用,所有权就转移给了该函数,那么在vec[0]拿的时候,就报未初始化了。

有一点需要总结:什么情况下会发生转移。

什么情况下会发生转移?

  • 变量绑定,非Copy trait的赋值给另一个变量,通常会发生移动,但除了&引用和clone的赋值语句。
let x = String::from("hello");
let y = x; // x 被移动到 y
  • 函数传参,将一个非Copy trait类型的值作为参数给函数时会发生移动。
fn take_ownership(s: String) {}

let s = String::from("hello");
take_ownership(s); // s 的所有权被移动到函数内部
  • 从容器中移除元素,从 Vec 或其他集合中移除一个非 Copy 类型的元素会发生移动
let mut v = vec![String::from("hello")];
let s = v.remove(0); // v 中的元素被移动到 s
  • 替换变量的值: 使用 std::mem::replace 或类似函数替换变量的值也会导致移动。
use std::mem;

let mut s = String::from("hello");
let s2 = mem::replace(&mut s, String::new()); // s 被新值替换,原值移动到 s2
  • 模式匹配: 在模式匹配中直接解构非 Copy 类型的值。
let tuple = (String::from("hello"), 42);
let (s, num) = tuple; // tuple 中的 String 被移动到 s

在所有这些情况中,如果类型实现了 Copy trait,则不会发生移动,而是进行按位复制。对于没有实现 Copy trait 的类型,上述操作会导致原变量无效,除非使用引用(借用)或显式克隆值。

无法通过vec的索引来试图移动元素

Vec通过索引访问,试图移动非Clone trait元素时发生报错:

// 构建一个由字符串"101"、"102"......"105"组成的向量
let mut v:Vec<String> = Vec::new();
for i in 101 .. 106 {
    v.push(i.to_string());
}
let four = v.get(0);
let third = v[0];

注意代码中通过get(index)去拿,拿到Option,如果有结果,four就是对的。而third是错的,因为vec不允许这样做,它返回的注定是一个引用类型。

let four = v.get(0);  // 正确: `four` 是一个 `Option<&String>`
let third = &v[0];    // 正确: `third` 是一个 `&String`,保留了对元素的不可变借用

如果实在想获取结果:clone它,当然要这个类型实现了clone trait。

let third = v[0].clone(); // 正确: `third` 是一个新的 `String`,是 `v[0]` 的副本

为了防止出现悬垂指针和确保内存安全,Rust 不允许你通过索引操作直接移动一个 Vec 中的非 Copy 类型值。而是要求你显式地克隆值(如果你想要一个新的独立所有权的实例),或者使用引用来访问(如果你只是想要读取或借用值)。

如果是复杂对象呢?

#[test]
fn share_test(){
    let mut composers = Vec::new();
    composers.push(Person { name: Some("Palestrina".to_string()),
        birth: 1525 });

    let first_name = composers[0].name;
}

struct Person { name: Option<String>, birth: i32 }

报错,因为通过索引,无法直接移动元素,Person不是Copy trait,当然Person的name也是非Copy trait,但是怎么获取呢?

Vec非Clone trait的复杂类型的解决方案

let first_name = std::mem::replace(&mut composers[0].name, None);
assert_eq!(first_name, Some("Palestrina".to_string()));
assert_eq!(composers[0].name, None);

但是这也并没有说,vec通过索引访问一定是Copy trait类型的,其实可以是&类型的。

let v = vec![String::from("Hello"), String::from("World")];

// 获取一个对第一个元素的不可变引用
let first_element = &v[0]; // 类型为 &String

// 如果你需要可变引用,Vec必须是可变的
let mut v = vec![String::from("Hello"), String::from("World")];
let first_element_mut = &mut v[0]; // 类型为 &mut String

也就成了,想从数据结构中获取非Copy类型的值,经常需要使用clone

#[test]
fn share_test(){
    let mut composers = Vec::new();
    composers.push(Person { name: Son{name:"no".to_string(), birth: 32}, birth: 1525 });
    let first_name = composers[0].name.birth;
    println!("first_name {}", first_name);
}

struct Son {
    birth : i32,
    name : String
}

struct Person { name: Son, birth: i32 }

上面代码,一旦改成composers[0].name.name就会报错,因为string类型,不是Copy trait。

由于所有权系统的设计,当你想要从一个数据结构中获取某个非 Copy 类型的值时,你经常需要使用 clone() 方法来获取该值的副本,而不是移动原始值。这是 Rust 为了保证内存安全和数据的完整性而做出的设计选择。

使用 clone() 会创建数据的一个完整副本,这可能涉及分配新的内存(例如,在 String 类型的情况下),因此在性能敏感的代码中,过度使用 clone() 可能会导致性能问题。

为了避免不必要的 clone() 调用,你可以:

  • 使用引用(&T 或 &mut T)来访问值,而不是获取它的所有权。
  • 如果可能,使用 Copy 类型(比如基础数字类型,char,或者小的自定义类型)。
  • 重新思考你的数据结构设计或算法,以减少对所有权转移的需求。
  • 使用 Rc<T> 或 Arc<T> 来共享所有权,这些智能指针允许多个所有者,但会增加引用计数的开销。
  • 当合适时,利用迭代器和其他的零成本抽象来操作数据而不需要复制它。
  • Rust 的这种设计强制开发者在编写代码时就要认真考虑所有权和借用的问题,有助于编写出更安全、更可靠的程序,尽管有时候它会增加编程的复杂性。

那么其中之一的问题,Copy trait到底有哪些?

Copy trait

标准的 Copy 类型包括所有机器整数类型和浮点数类型、char 类型 和 bool 类型,以及某些其他类型。Copy 类型的元组或固定大小的数组本身也是 Copy 类型。

String 不是 Copy 类型,因为它拥有从堆中分配 的缓冲区。出于类似的原因,Box<T> 也不是 Copy 类型,因为它拥 有从堆中分配的引用目标。代表操作系统文件句柄的 File 类型不是 Copy 类型,因为复制这样的值需要向操作系统申请另一个文件句 柄。类似地,MutexGuard 类型表示一个互斥锁,它也不是 Copy 类型:复制这种类型毫无意义,因为每次只能有一个线程持有互斥 锁。

根据经验,任何在丢弃值时需要做一些特殊操作的类型都不能是 Copy 类型:Vec 需要释放自身元素、File 需要关闭自身文件句 柄、MutexGuard 需要解锁自身互斥锁,等等。对这些类型进行逐位 复制会让我们无法弄清哪个值该对原始资源负责。

如果 结构体的所有字段本身都是 Copy 类型,那么也可以通过将属性 # [derive(Copy, Clone)] 放置在此定义之上来创建 Copy 类型。

为什么一个用户自定义的类型符合Copy条件,为什么系统不自动成为Copy类型?

如果以后 确有必要将其改为非 Copy 类型,则使用它的大部分代码可能需要进行调整。

Rc和Arc

Rc 类型和 Arc 类型非常相似,它们之间唯一的区别是 Arc 可以安 全地在线程之间直接共享,而普通 Rc 会使用更快的非线程安全代码 来更新其引用计数。如果不需要在线程之间共享指针,则没有理由为 Arc 的性能损失“埋单”,因此应该使用 Rc,Rust 能防止你无意间 跨越线程边界传递它。这两种类型在其他方面都是等效的,

use std::rc::Rc;

#[test]
fn rc_test(){
    let s: Rc<String> = Rc::new("shirataki".to_string());
    let t: Rc<String> = s.clone();
    let u: Rc<String> = s.clone();
}

注意上面代码中,clone后,并没有在堆中再开辟内存来存储shirataki字符串:

可以在Rc<String>上使用String上的任何方法。

s、t 和 u 是 Rc<String> 类型的变量,它们都指向堆上同一个 String 对象。当 s.clone() 被调用时,它创建了 t,这是一个新的 Rc<String> 指向相同的 String,并且增加了引用计数。同理,s.clone() 的第二次调用创建了 u,再次增加了引用计数。

当 rc_test 测试结束时,s、t 和 u 都会出栈,并且它们的 Drop trait 实现会被调用,这将分别减少引用计数。当最后一个 Rc<String> 被销毁时,引用计数变为 0,Rc 会负责自动清理堆上的 String 数据。

这是 Rust 自动内存管理的一个示例,其中 Rc<T> 通过引用计数来确保资源在不再被需要时被正确释放。

什么时候使用Rc和Arc?

Rc<T>(引用计数类型)和 Arc<T>(原子引用计数类型)是 Rust 中的两种智能指针,用于在多个所有者之间共享数据,但它们各自适用于不同的场景?

使用 Rc<T> 的情况:

单线程环境:Rc<T> 不是线程安全的,因此只能用于单线程场景。

共享所有权:当你想要在程序的多个部分之间共享对某个数据的读取权时,但该数据不需要修改。

循环引用:在创建数据结构时(如图或树),某些元素需要拥有对其他元素的引用。

性能要求:由于 Rc<T> 不是线程安全的,它不需要像 Arc<T> 那样使用原子操作来维护引用计数,因此它的性能略好一些。

使用 Arc<T> 的情况:

多线程环境:Arc<T> 是线程安全的,可以安全地在多个线程之间共享数据。

共享所有权:与 Rc<T> 类似,当数据需要有多个所有者,并且需要跨线程共享时。

并发数据结构:在并发或并行编程中,当多个线程需要读取相同数据时,Arc<T> 是一个很好的选择。

总的来说,如果你的代码是单线程的,那么 Rc<T> 就足够用了。如果你需要在多个线程之间共享数据,那么你应该使用 Arc<T>。需要注意的是,无论是 Rc<T> 还是 Arc<T>,它们都只允许对内部数据进行不可变借用。如果你需要在多个线程之间共享可变数据,那么你可能需要结合使用 Arc<T> 和某种形式的互斥锁,如 Mutex<T> 或 RwLock<T>。

先保留这张图片