Rust 圣经 阅读 所有权和借用

发布时间:2024年01月04日

所有权

栈(Stack)与堆(Heap)

栈何和堆的核心目标就是为程序在运行时提供可供使用的内存空间。

栈按照顺序存储值并以相反顺序取出值,后进先出。
增加数据叫进栈,取出数据叫出栈。
栈中的所有数据必须占用 已知且固定大小的空间。假设数据大小是未知的,那么在取出数据时,将不能取出想要的数据。

对于大小未知或可能变化的数据,我们需要将其存储在堆上。

当向堆上存储数据时,需要请求一定大小的空间,然后操作系统在堆的某处找到符合大小的空间,将其标注为已使用,并返回一个表示该位置地址的指针。该过程被称为在堆上分配内存。有时简称为”分配(allocation)“。

返回的指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用中,可以通过指针,来获取数据在堆上的实际内存位置,进而访问数据。

堆上的数据是无组织的。

性能区别

写入方面:入栈比在堆上分配内存要快。
因为入栈只需将新数据放在栈顶,而在堆上分配内存需要找到足够存放数据空间,还要做一些记录为下次分配做准备。

读取方面:出栈比从堆上读取数据更快。
首先,栈上的数据都是存储在CPU的高速缓存上,而堆上的数据只能存储在内存中。而高速缓存和内存的访问速度差异在10倍以上。
其次,访问堆上数据,必须先访问栈再通过栈上的指针来访问内存。

所有权和堆栈

当调用一个函数,传递给函数的参数(包括指向堆的指针和函数的局部变量)一次压入栈中,当函数调用结束时,这些值将中栈中按照相反的顺序依次移除。
所有权帮助避免内存泄漏。

所有权原则

规则:

  1. Rust 中每一个值都被一个变量所拥有,该变量称为值的拥有者。
  2. 一个值同时只能被一个变量拥有,一个值只能有一个拥有者。
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)。
String 类型

动态字符串类型: String,该类型被分配到堆上,因此可以动态伸缩,可以存储在编译时大小未知的文本。

基于字符串字面量来创建 String 类型:

let s = String::from("hello");

:: 是一种调用操作符,表示调用 String 中的 from 方法,因为 String 存储在堆上的动态的,所有它可以修改。

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() 在字符串后追加字面值

println!("{}", s); // 将打印 `hello, world!`

变量绑定背后的数据交互

转移所有权
let x= 5;
let y = x;

5 绑定到变量 x;接着拷贝 x 的值赋给 y,最终 xy 都等于 5,因为整数是 Rust 基本数据类型,是固定大小的简单值,因此这两个值都是通过自动拷贝的方式来赋值的,都被存在栈中,完全无需在堆上分配内存。

只有存储在栈上的基本类型,rust 会自动拷贝。

let s1 = String::from("hello");
let s2 = s1;

String 不是基本类型,而且是存储在堆上的,因此不能自动拷贝。

实际上, String 类型是一个复杂类型,由存储在栈中的堆指针字符串长度字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,至于长度和容量,容量是堆内存分配空间的大小,长度是目前已经使用的大小。

这里有两种情况:

  1. 深拷贝:拷贝了所有数据,包含堆上的数据和栈上的数据。对性能有很大的影响。
  2. 只拷贝了 String,即仅拷贝指针、长度、容量。但是这里就涉及到了所有权的问题,因为一个值只允许有一个所有者。这样的拷贝会诞生两个所有者。多个所有权可能会导致多次释放同一个内存,会导致内存污染

Rust 在当 s1 赋予 s2 后,Rust 认为 s1 不再有效,所有在 s1 离开作用域后不会 drop 任何东西。
当赋予后,原来的所有者会成为无效的引用。

这样的浅拷贝在Rust称为移动,因为原有的拥有者失效了。

fn main() {
    let x: &str = "hello, world";
    let y = x;
    println!("{},{}",x,y);
}

这段代码不会出问题,是因为x并不是字符串的拥有者,它只是引用了这个字面量。仅仅是对该引用进行了拷贝,二者都引用了同一个字符串。

克隆(深拷贝)

Rust 不会自动创建数据的深拷贝。 任何自动的复制都不是深拷贝。
可以使用 clone 方法来深度复制 string 中堆上的数据,而不仅仅是栈上的数据。

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

克隆完整地复制了 s1 的数据,他们是两个不同的数据的所有者,所以不会出现错误。
频繁地使用克隆会降低程序的性能。

拷贝(浅拷贝)

浅拷贝只发生在栈上,因此性能很高。
基本类型在编译时是已知大小的,会被存储在栈上,这里的深浅拷贝没有区别。

copy

Rust 有一个叫做 copy 的特征,可以用在类型整型这样在栈上存储的变量。
当一个类型拥有 copy 特征,一个旧的变量被赋给其他变量后依然可用。

具体一个类型是否可以 copy 需要查看给定的文档来确认。
通用规则是 任何基本类型的组合可以copy,不需要分配内存或某种形式资源的类型可以 copy
下面是一些可以 copy 的类型:

  • 所有整数类型,比如 u32
  • 布尔类型, bool
  • 所有浮点数类型,比如 f64
  • 字符类型, char
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如, (i32, i32)Copy 的,但 (i32, String) 就不是。
  • 不可变引用 &T,但是 可变引用 &mut T 是不可以 Copy 的

函数传值与返回

将值传递给函数,一样会发生 移动复制

fn main() {
    let s = String::from("hello");  // s 进入作用域

    takes_ownership(s);             // s 的值移动到函数里 ...
                                    // ... 所以到这里不再有效

    let x = 5;                      // x 进入作用域

    makes_copy(x);                  // x 应该移动函数里,
                                    // 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域
    println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域
    println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作

函数返回值也有所有权。

fn main() {
    let s1 = gives_ownership();         // gives_ownership 将返回值
                                        // 移给 s1

    let s2 = String::from("hello");     // s2 进入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移动到
                                        // takes_and_gives_back 中,
                                        // 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
  // 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给
                                             // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string                              // 返回 some_string 并移出给调用的函数
}

// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string  // 返回 a_string 并移出给调用的函数
}

所有权很强大,避免了内存的不安全性,但是也带来了一个新麻烦: 总是把一个值传来传去来使用它。 传入一个函数,很可能还要从该函数传出去,结果就是语言表达变得非常啰嗦,幸运的是,Rust 提供了新功能解决这个问题。

原来的可变变量在转移所有权时,是否可变取决于新的变量。


fn main() {
    let mut s = String::from("hello, ");
    s.push_str("world");
    println!("{:?}",s);
    
    // 只修改下面这行代码 !
    let  s1 = s; // 需要添加 mut

    s1.push_str("world");
    println!("{:?}",s1);
}

可以使用 ref 来引用一个变量,引用的不是值,而是变量,而不是转移所有权。


fn main() {
    #[derive(Debug)]
    struct Person {
        name: String,
        age: Box<u8>,
    }

    let person = Person {
        name: String::from("Alice"),
        age: Box::new(20),
    };

    // 通过这种解构式模式匹配,person.name 的所有权被转移给新的变量 `name`
    // 但是,这里 `age` 变量却是对 person.age 的引用, 这里 ref 的使用相当于: let age = &person.age 
    let Person { name, ref age } = person;

    println!("The person's age is {}", age);

    println!("The person's name is {}", name);

    // Error! 原因是 person 的一部分已经被转移了所有权,因此我们无法再使用它
    //println!("The person struct is {:?}", person);

    // 虽然 `person` 作为一个整体无法再被使用,但是 `person.age` 依然可以使用
    println!("The person's age from person struct is {}", person.age);
}

![请添加图片描述](https://img-blog.csdnimg.cn/direct/c7f3d6a453674aae992f125bf7af6c2d.png)
文章来源:https://blog.csdn.net/sha_mo_li/article/details/135320362
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。