Tao
Tao

rust所有权

Rust所有权(Ownership)是Rust语言中的一个核心概念,它解决了许多系统编程语言中存在的内存安全问题。所有权规则确保了在任何给定时间,只有一个可变引用或多个不可变引用可以访问数据。通过这种方式,Rust编译器可以在编译期间捕获并拒绝数据竞争等未定义行为。

rust所有权定义:一个值只能被一个变量所拥有,且同一时刻只能有一个所有者,当所有者离开作用域,其拥有的值被丢弃,内存得到释放。

  • 一个值只能被一个变量所拥有,这个变量被称为所有者(Each value in Rust has a variable that’s called its owner)。

  • 一个值同一时刻只能有一个所有者(There can only be one owner at a time),也就是说不能有两个变量拥有相同的值。所以对应刚才说的变量赋值、参数传递、函数返回等行为,旧的所有者会把值的所有权转移给新的所有者,以便保证单一所有者的约束。

  • 当所有者离开作用域,其拥有的值被丢弃(When the owner goes out of scope, the value will be dropped),内存得到释放。

rust

// 1. 所有权唯一性
let x = String::from("hello"); // x 是 "hello" 字符串的所有者

// 2. 所有权转移
let y = x; // 所有权从 x 转移到 y
// 此时 x 已经无效

// 3. 借用
let z = &y; // z 借用了 y 的不可变引用
println!("{}", z); // 打印 "hello"

let mut w = String::from("world");
{
    let p = &mut w; // p 借用了 w 的可变引用
    p.push_str("!"); // 修改了被借用的值
}
println!("{}", w); // 打印 "world!"

如果值实现了 Copy trait,那么赋值或传参会使用 Copy 语义,相应的值会被按位拷贝(浅拷贝),产生新的值。

我们先来看下如下代码(代码来源极客时间rust第一课):

rust

fn is_copy<T: Copy>() {}

fn types_impl_copy_trait() {
    is_copy::<bool>();
    is_copy::<char>();

    // all iXX and uXX, usize/isize, fXX implement Copy trait
    is_copy::<i8>();
    is_copy::<u64>();
    is_copy::<i64>();
    is_copy::<usize>();

    // function (actually a pointer) is Copy
    is_copy::<fn()>();

    // raw pointer is Copy
    is_copy::<*const String>();
    is_copy::<*mut String>();

    // immutable reference is Copy
    is_copy::<&[Vec<u8>]>();
    is_copy::<&String>();

    // array/tuple with values which is Copy is Copy
    is_copy::<[u8; 4]>();
    is_copy::<(&str, &str)>();
}

fn types_not_impl_copy_trait() {
    // unsized or dynamic sized type is not Copy
    is_copy::<str>();
    is_copy::<[u8]>();
    is_copy::<Vec<u8>>();
    is_copy::<String>();

    // mutable reference is not Copy
    is_copy::<&mut String>();

    // array / tuple with values that not Copy is not Copy
    is_copy::<[Vec<u8>; 4]>();
    is_copy::<(String, u32)>();
}

fn main() {
    types_impl_copy_trait();
    types_not_impl_copy_trait();
}

通过上面代码我们可以知道哪些数据默认实现copy trait。 总结如下:

  • 原生类型,包括函数、不可变引用和裸指针实现了Copy;
  • 数组和元组,如果其内部的数据结构实现了Copy,那么它们也实现了Copy
  • 可变引用没有实现Copy
  • 非固定大小的数据结构,没有实现Copy

Copy是浅拷贝,在你赋值或者传参时,值会自动按位拷贝。 Clone是深拷贝,在你赋值或者传参时,需要调用clone方法,

Copy Trait:

  • Copy Trait 用于那些存储在栈上的简单标量类型,如 i32f64 等,以及完全由 Copy 数据组成的复合类型。
  • 实现了 Copy Trait 的类型会在赋值或函数传递时直接进行按位内存复制,这是一个廉价的操作。
  • 对于 Copy 类型,不会发生所有权转移,原值和新值都可以同时使用。
  • Copy 只能应用于不需要分配内存或资源的类型,因为复制必须是一个完全无副作用的操作。

Clone Trait:

  • Clone Trait 用于那些存储在堆上的数据结构,如 StringVec 等,以及任何拥有资源的类型。
  • 实现了 Clone Trait 的类型会在复制时执行深层的内存分配和数据复制操作。
  • 对于 Clone 类型,复制会产生一个完全独立的新值,原值和新值都拥有各自的资源。
  • Clone 可以应用于任何类型,只要该类型提供了深度复制所需的逻辑。

示例:

rust

// Copy 示例
let x = 42;
let y = x; // 直接复制 x 的值
println!("x = {}, y = {}", x, y); // 输出 "x = 42, y = 42"

// Clone 示例
let s1 = String::from("hello");
let s2 = s1.clone(); // 克隆 s1 的内容到新的 String 实例
println!("s1 = {}, s2 = {}", s1, s2); // 输出 "s1 = hello, s2 = hello"

在第一个例子中,x 是一个整数,实现了 Copy Trait。因此当我们将 x 赋值给 y 时,只是简单地复制了 x 的内存值,而不涉及任何资源分配或所有权转移。

在第二个例子中,s1 是一个 String 类型,它存储在堆上。当我们调用 clone() 方法时,它会在堆上重新分配一块内存,并将 s1 的内容深度复制到新分配的内存中。这样,s1s2 就拥有了完全独立的内存空间,修改其中一个不会影响另一个。

一般来说,Copy 用于简单的值语义,而 Clone 用于复杂的数据结构和资源的复制。Copy 的性能开销更小,但它有严格的限制条件;而 Clone 更加通用,但也更昂贵。

在 Rust 中,Copy 是一个特殊的标记 Trait,编译器会自动为满足条件的类型实现它。而 Clone 则需要手动实现,或者通过 #[derive(Clone)] 自动派生。

总的来说,在需要复制值时,应该优先考虑使用 Copy。如果类型无法实现 Copy(例如包含堆分配的数据),那么就应该使用 Clone。在性能敏感的场景下,也应该尽量避免不必要的克隆操作。

赋值或者传参会导致值Move,所有权被转移,一旦所有权转移,之前的变量就不能访问。

具体来说,move 发生在以下几种情况:

  1. 变量绑定

rust

let x = String::from("hello");
let y = x; // x 的值被移动到 y,x 不再拥有字符串的所有权

在这个例子中,x 的值(一个 String实例)被移动到了 y。这是因为 String 是一个在堆上分配内存的类型,它的所有权必须是唯一的。当 x 的值被移动到 y 后,x 就失去了该值的所有权,因此无法继续使用。

  1. 作为参数传递

rust

let s = String::from("hello");
take_ownership(s); // s 的值被移动到函数中

当一个值作为参数传递给函数时,它的所有权也会被移动到函数内部。在函数结束后,该值将被丢弃。

  1. 返回值

rust

fn create_string() -> String {
    let s = String::from("hello");
    s // s 的所有权被移动到函数返回值
}

函数的返回值也会发生所有权的移动。在上面的例子中,s 的所有权被移动到了返回值中。

move 语义可以确保在任何时候,一个值都只被一个变量拥有。这样一来,就避免了发生多个变量同时访问和修改同一值的情况,从而防止了数据竞争等未定义行为。

Borrow语义允许一个值的所有权,在不发生转移的情况下,被其它上下文使用。

Rust对可变引用的使用也做了严格的约束:

  • 在一个作用域内,仅允许一个活跃的可变引用。所谓活跃,就是真正被使用来修改数据的可变引用,如果只是定义了,却没有使用或者当作只读引用使用,不算活跃。
  • 在一个作用域内,活跃的可变引用(写)和只读引用(读)是互斥的,不能同时存在。

只读借用,也称为不可变借用,允许多个不可变引用同时引用同一个值。这些引用只能读取值,而不能修改它。

rust

let x = 5;
let y = &x; // 创建一个不可变引用
let z = &x; // 可以有多个不可变引用

println!("{} {} {}", x, y, z); // 5 5 5

可变借用允许创建一个可变引用,通过该引用可以读写被引用的值。但是,在同一作用域内,只能存在一个可变引用。

rust

let mut x = 5;
let y = &mut x; // 创建一个可变引用
*y += 1; // 通过可变引用修改 x 的值

println!("{}", x); // 6

比如在rust如何实现双向链表、DAG、多个线程要访问同一块内存等?针对特殊场景,rust提供了智能指针RC、ARC、RefCell、Cell等来解决这些问题。 智能指针(Smart Pointers)是一种封装了指针并增加了额外元数据和功能的数据结构。它们在底层使用了裸指针,但提供了更高级别、更安全的抽象,帮助我们自动管理资源并防止常见的内存安全问题。Rust标准库中提供了几种常用的智能指针。

  1. Box: Box 是一个在堆上分配内存的智能指针。它拥有数据的所有权,当 Box 离开作用域时,它会自动释放所包裹的堆内存。Box 通常用于存储一些在编译期无法确定大小的数据,或者避免过多的数据拷贝。

rust

let x = Box::new(5);
println!("x = {}", x); // 使用 *x 来解引用
  1. Rc和Arc: Rc(Reference Counted)和 Arc(Atomic Reference Counted) 提供了共享所有权的功能。它们通过引用计数跟踪资源的所有者数量,当没有所有者时自动释放资源。Rc用于单线程场景,Arc则用于多线程场景(原子操作更昂贵)。

rust

use std::rc::Rc;

let x = Rc::new(vec![1, 2, 3]);
let y = x.clone(); // 增加引用计数
println!("x before = {:?}", x);
println!("y = {:?}", y);
  1. RefCell和Rc<RefCell>: RefCell 可以被认为是一种内部可变性(Interior mutability)的编译时机制。它包裹了一个可变数据,并在运行时执行借用规则检查,而不是编译时检查。通常与 Rc 结合使用,可以实现在多个位置对数据进行可变借用。

rust

use std::rc::Rc;
use std::cell::RefCell;

let x = Rc::new(RefCell::new(vec![1, 2, 3]));
x.borrow_mut().push(4); // 可变借用x中的数据
println!("x = {:?}", x);
  1. Mutex和Arc<Mutex>: Mutex(Mutual Exclusion)是一种用于线程安全的并发编程原语。它提供了一个用于互斥访问共享数据的机制。通常与 Arc 结合使用,可以在多个线程间安全地共享和修改数据。

rust

use std::sync::{Arc, Mutex};
use std::thread;

let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut children = vec![];

for _ in 0..3 {
    let data_clone = Arc::clone(&data);
    children.push(thread::spawn(move || {
        let mut value = data_clone.lock().unwrap();
        value.push(4);
    }));
}

for child in children {
    child.join().unwrap();
}

println!("data = {:?}", data);

这些智能指针不仅提供了自动资源管理和线程安全等功能,同时也满足了Rust的所有权规则和借用规则,保证了内存安全。

相关内容