rust[2]-所有权

Rust 的核心功能(之一)是 所有权(ownership);

rust 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

堆 、栈

(LIFO)中的所有数据都必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。

是缺乏组织的:当向堆放入数据时,你要请求一定大小的空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的 指针(pointer)。这个过程称作 在堆上分配内存(allocating on the heap),有时简称为 “分配”(allocating)

入栈比在堆上分配内存要快,因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。

所有权规则:

  • Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
  • 值有且只有一个所有者。
  • 当所有者(变量)离开作用域,这个值将被丢弃。

内存与分配

字符串自变量String的核心区别在于: 两个类型对内存的处理上

自变量形式声明字符串值被硬编码进程序里。字符串字面值是很方便的,不过他们并不适合使用文本的每一种场景。不可变的并不是所有字符串的值都能在编写代码时就知道

String 被分配到堆上,所以能够存储在编译时未知大小的文本。

// 字符串自变量的形式声明
fn scope_fn() {
    let s = "hello";
    println!("{}", s);
}


fn second_string() {
    // :: 表示from是string下面的方法
    let mut s = String::from("hello");
    s.push_str(", world");
    println!("{}", s);
}
复制代码

Rust 采取了一个不同的策略:内存在拥有它的变量离开作用域后就被自动释放,rust就会调用drop方法。

fn main() {
 {
    let s = String::from("hello"); // 从此处起,s 是有效的
    // 使用 s
  }                                  // 此作用域已结束,
}                                   // s 不再有效
复制代码

变量与数据交互的方式:1:移动

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

println!("{}, world!", s1);

复制代码

rust 中将变量s1移动到了s2中,这样子也就不存在了内存二次释放的bug。因此s2就是有效的,其离开作用域,释放自己的内存,完毕。

image.png

如果你在其他语言中听说过术语 浅拷贝(shallow copy)深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动(move),而不是浅拷贝。

变量与数据交互的方式:2:拷贝

fn main() {
    let x = 5;
    let y = x;
    println!("x = {}, y = {}", x, y);
}

复制代码

像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y 后使 x 无效。

Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上,类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。

  • 任何简单标量值的组合可以是 Copy 的,不需要分配内存或某种形式资源的类型是 Copy
  • 所有整数类型,比如 u32
  • 布尔类型,bool,它的值是 truefalse
  • 所有浮点数类型,比如 f64
  • 字符类型,char
  • 元组,当且仅当其包含的类型也都是Copy的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。

所有权与函数

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 移出作用域。不会有特殊操作
复制代码

有返回值的函数可以转移所有权, 但是rust里面这部分实现是使用引用来实现的。

fn main() {
    // let (x, y): (usize, isize) = (1, 2);
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    // 返回一个元组,
    (s, length);
}
复制代码

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

引用和借用

引用(Reference) 是 C++ 开发者较为熟悉的概念。如果你熟悉指针的概念,你可以把它看作一种指针。实质上"引用"是变量的间接访问方式。

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 is {}, s2 is {}", s1, s2); // hello hello 
}
复制代码

&运算符可以取变量的 "引用"。当一个变量的值被引用时,变量本身不会被认定无效。因为"引用"并没有在栈中复制变量的值

image.png

fn main() {
    let s1 = String::from("hello");
    
    // &s1 语法让我们创建一个 指向 值 s1 的引用,但是并不拥有它。
    // 因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
    let len = calculate_length(&s1); 
    println!("The length of '{}' is {}.", s1, len);
}

// 函数签名使用 `&` 来表明参数 `s` 的类型是一个引用
fn calculate_length(s: &String) -> usize {
    s.len()
}
复制代码

这些 & 符号就是引用,它们允许你使用值但不获取其所有权

  • 引用不会获得值的所有权。
  • 引用只能租借(Borrow)值的所有权。
  • 引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权
  • 禁止修改租借的值,除了mut修饰过的。也就是可变引用。

可变引用:

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!(">>{}", s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

复制代码

限制:在特定作用域中的特定数据有且只有一个可变引用,这个限制的好处是 Rust 可以在编译时就避免数据竞争。**数据竞争(data race)**类似于竞态条件。

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 异常

println!("{}, {}", r1, r2);

复制代码

借用:

我们将获取引用作为函数参数称为 借用(borrowing),正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

引用总结:、

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的

悬垂引用(Dangling References)

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer)所谓悬垂指针是其指向的内存可能已经被分配给其它持有者

相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String { // dangle 返回一个字符串的引用
    let s = String::from("hello"); // s 是一个新字符串
    &s // 返回字符串 s 的引用
    
    // 解决的办法就是直接返回 return s;
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!
复制代码

slice 切片

切片(Slice) 是对数据值的部分引用。

在 Rust 中有两种常用的字符串类型:strString

strRust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现 &str ,凡是用双引号包括的字符串常量整体的类型性质都是 &str

let s = "hello";
复制代码

Rust 中的字符串类型实质上记录了字符在内存中的起始位置和其长度。

fn main() {
    let s = String::from("broadcast");
    let part1 = &s[0..5]; // 前闭后开的一个结构
    let part2 = &s[5..9];
    println!("{}={}+{}", s, part1, part2);
}
复制代码

一些常见的省略写法:

..y 等价于 0..y
x.. 等价于位置 x 到数据结束
.. 等价于位置 0 到结束
复制代码

被切片引用的字符串禁止更改其值, 因为只是部分引用, 切片结果必须是引用类型,但开发者必须自己明示这一点:

fn main() {
    let s = String::from("broadcast");
    let slice = &s[0..3];
    s.push_str('aa'); // 错误异常
    println!("slice>>> {}", slice);
}
复制代码

非字符串切片:

除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:

fn main() {
    let arr = [1, 2, 3, 4, 5, 6];
    let part = &arr[..3];
    for i in part.iter() {
        println!(">>>{}", i);
    }
}
复制代码