Skip to content

Latest commit

 

History

History
357 lines (255 loc) · 12.4 KB

05.所有权.md

File metadata and controls

357 lines (255 loc) · 12.4 KB

所有权

概念

① 栈与堆

栈中所有的数据必须占用已知且固定的大小。在编译时大小未知或大小可能变化的数据,要改为存储在堆上。 堆是缺乏组织的:当向堆中放入数据时,你要请求一定大小的空间。操作系统找到一块足够的空位,并它标记为已使用,并返回一个表示该位置地址的指针。这个过程称为在堆上分配内存,有时简称为 “分配”(allocating)。将数据推入栈中并不被认为是分配。因为指针的大小是已知并且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针。

当你调用一个函数时,传递给函数的值和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。

跟踪哪部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会耗尽空间,这些问题正是所有权系统要处理的。

② 所有权规则

  • 变量负责释放它们自己的资源,每个资源只有一个所有者。这也阻止了一个资源被释放多次。
  • 并不是所有的变量都拥有资源(比如 引用)。

③ 内存与分配

就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件中。这使得字符串字面值快速且高效。不过这些特性都只得益于字符串字面值的不可变性。 对于 String 类型,为了支持一个可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容。这意味着:

  • 必须在运行时向操作系统请求内存。
  • 需要一个当我们处理完 String 时将内存返回给操作系统的方法。

第二部分的实现。在有垃圾回收(garbage collector,GC)的语言中, GC 记录并清除不再使用的内存,而我们并不需要关心它。没有 GC 的话,识别出不再使用的内存并调用代码显式释放就是我们的责任了。

Rust 采用的策略是:内存在拥有它的变量离开作用域后就被自动释放。比如:

{
    let s = String::from("hello"); // 从此处起,s 是有效的

    // 使用 s
}                                  // 此作用域已结束,
                                   // s 不再有效

这是一个将 String 需要的内存返回给操作系统的很自然的位置:当 s 离开作用域的时候。当变量离开作用域,Rust 为我们调用一个特殊的函数。这个函数叫做 drop,在这里 String 的作者可以放置释放内存的代码。Rust 在结尾的 } 处自动调用 drop。

变量与数据交互

变量与数据交互的方式(一):移动

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

变量与数据交互的方式(二):克隆

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

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

Rust 有一个叫做 Copy trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型拥有 Copy trait,一个旧的变量在将其赋值给其他变量后仍然可用。

下面是一些 Copy 的类型:

  • 所有整数类型,比如 u32。
  • 布尔类型,bool,它的值是 true 和 false。
  • 所有浮点数类型,比如 f64。
  • 字符类型,char。
  • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。

所有权与函数

将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。

返回值与作用域

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

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 并移出给调用的函数
}

引用

& 符号就是 引用,它们允许你使用值但不获取其所有权。正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。

借用
引用的过程称之为借用。通常可以通过 &TBox::new(T) 进行借用。

可变引用

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

    change(&mut s);
}

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

① 在特定作用域中的特定数据只能有一个可变引用。

比如下面的代码会报错:

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

let r1 = &mut s;
let r2 = &mut s;

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

限制的好处

  • 两个或更多指针同时访问同一数据。
  • 至少有一个指针被用来写入数据。
  • 没有同步数据访问的机制。

数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码!

可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不允许同时拥有

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

{
    let r1 = &mut s;

} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;

② 我们也不能在将要使用不可变引用的同时,获取其可变引用。

不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。比如:

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}", r1);

报错:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:29:14
   |
27 |     let r1 = &s; // 没问题
   |              -- immutable borrow occurs here
28 |     let r2 = &s; // 没问题
29 |     let r3 = &mut s; // 大问题
   |              ^^^^^^ mutable borrow occurs here
30 | 
31 |     println!("{}", r1);
   |                    -- immutable borrow later used here

注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:

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

let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用

let r3 = &mut s; // 没问题
println!("{}", r3);

③ 不能在使用可变引用的同时,获取其不可变引用

let mut s1: String = String::from("hello");
let s2: &mut String = &mut s1;
println!("{}", s1);
println!("{}", s2);

报错:

error[E0502]: cannot borrow `s1` as immutable because it is also borrowed as mutable
  --> src/main.rs:22:20
   |
21 |     let s2: &mut String = &mut s1;
   |                           ------- mutable borrow occurs here
22 |     println!("{}", s1);
   |                    ^^ immutable borrow occurs here
23 |     println!("{}", s2);
   |                    -- mutable borrow later used here

注:
如果没有 println!("{}", s2); 这一句,根据所有权策略,可变引用 &mut s1随着 s2 变量被释放了,然后发生不可变借用,是允许的。

④ 不能在将要使用引用指针的同时,重新绑定原变量

let mut x = 1;
let p = &mut x;
// 或者 let p = & x;
x = 2;
println!("{}", p);

报错:

error[E0506]: cannot assign to `x` because it is borrowed
 --> src/main.rs:6:5
  |
5 |     let p = &mut x;
  |             ------ borrow of `x` occurs here
6 |     x = 2;
  |     ^^^^^ assignment to borrowed `x` occurs here
7 |     println!("{}", p);
  |                    - borrow later used here

⑤ 在模式匹配或 let 解构中,关键词 ref 用于表示对 struct/tuple 中某个字段的引用

参考

#[derive(Debug)]
enum StringOrInt {
    Str(String),
    Int(i64),
}

fn main() {
    use StringOrInt::{Str, Int};
    let mut x = Str("Hello world".to_string());

    if let Str(ref insides) = x {
        x = Int(1);
        println!("inside is {}, x says: {:?}", insides, x);
    }
}

报错:

error[E0506]: cannot assign to `x` because it is borrowed
  --> src/main.rs:15:9
   |
14 |     if let Str(ref insides) = x {
   |                ----------- borrow of `x` occurs here
15 |         x = Int(1);
   |         ^ assignment to borrowed `x` occurs here
16 |         println!("inside is {}, x says: {:?}", insides, x);
   |                                                ------- borrow later used here

⑥ 部分移动,部分引用

当一个值的一部分字段被移动,另一部分被引用。它将无法整体被借用,而只能借用被引用的部分。

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

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

    // `name` is moved out of person, but `age` is referenced
    let Person { name, ref age } = person;

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

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

    // Error! borrow of partially moved value: `person` 部分所有权发生了移动。
    //println!("The person struct is {:?}", person);
    // println!("The person's age from person struct is {}", person.name);

    // `person.age`  可以被借用,因为它没有移动所有权
    println!("The person's age from person struct is {}", person.age);
}

借用规则

  • &mut 型借用只能指向本身具有 mut 修饰的变量,对于只读变量,不可以有 &mut 型借用。
  • 借用指针不能比它指向的变量存在的时间更长,否则会产生悬垂指针。

悬垂引用

在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态:当你拥有一些数据的引用,编译器确保数据不会在其引用之前离开作用域。

如下面的代码会产生编译时错误:

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做。

修改:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

这样就没有任何错误了。所有权被移动出去,所以没有值被释放。

指针与引用的区别

在 Rust 中,引用和智能指针的一个的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有他们指向的数据。