04认识所有权

背景: 所有运行的程序都必须管理其使用的计算机内存的方式。

目前主要三种方式:

  • 垃圾回收机制
    • 程序运行时不断地寻找不再使用的内存
  • 程序员自己分配和释放内存
  • 所有权系统管理内存

栈和堆

栈:后进先出(last in first out)

增加数据 称为,进栈

移出数据 称为,出栈

栈中的所有数据都必须占用已知且固定的大小。

在编译时,大小未知或大小可能变化的数据,要存储在堆上。

在堆上分配内存*(allocating on the heap):当向堆中放入数据时,要请求一定大小的空间。内存分配器在堆的某处找到一块足够大的空间,把它标记为已使用,并返回一个表示该位置地址的指针(pointer)。

因为指针的大小是已知且固定的,你可以将指针存储在栈上,不过当需要实际数据时,必须访问指针所指数据。

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

访问堆上数据比访问栈上数据慢,因为必须通过指针来访问。

现代处理器在内存中跳转越少就越快。

当调用一个函数时,传递给函数的参数和函数的局部变量会被压入栈中。

当函数调用结束时,这些值被移出栈。

存储在栈上的数据,当离开作用域时被移出栈。

什么是所有权?

所有权规则

要谨记这些规则:

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

变量作用域

从变量被声明的点开始直到所在花括号结束。

1
2
3
4
5
{                      // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的

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

存储在栈上的数据,当离开作用域时被移出栈。

在没有GC的语言中,内存的分配和释放都必须我们来处理。Rust 为我们调用了一个特殊的函数 drop,在作用域结束处} 自动调用 drop函数。

内存与分配

在堆上分配内存,意味着:

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

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

1
2
3
4
let s1 = String::from("hello");  // 在堆上为数据 hello 分配了内存,在栈上创建了s1
let s2 = s1; // 在栈上创建s2,指向 hello

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

例如上面代码,s2=s1 时,并没有像Java语言那样发生浅拷贝,而是发生了移动(move),Rust 为了防止二次释放(double free),Rust 认为 s1 不再有效,将数据的所有权移交给了 s2,这样s1 离开作用域的时候不会清理任何内容,而s2 离开作用域时会清理绑定的数据。

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

当将堆上的数据复制时,需要使类型实现 clone的通用函数。

例如:

1
2
3
4
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2); // 因为s1.clone() 在堆上复制了一份数据,所以s1,也可以使用。

只在栈上的数据:拷贝

Rust 中有一个 Copy 的trait,如果一个类型拥有 Copy trait,则一个旧的变量在将其赋值给其他变量后,仍然可用。

发生在栈上(stack)。

注:Rust 不允许自身或其任何部分实现了Drop trait 的类型使用 Copy trait。

以下数据类型实现了 Copy 类型:

即,赋值给第二个变量后,旧的变量依然可用。所有权没有发生移动。

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

所有权与函数

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 移出作用域。不会有特殊操作

返回值与作用域

返回值也可以转移所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 并移出给调用的函数
}

总结:

变量的所有权总是遵循相同的模式:

  • 将值赋值给另一个变量时移动它。

    • 如果值的类型实现了clone这个函数,可以通过复制该值给新变量。此时在堆上是有两个值。

      也就是使用时,如果方法需要的是获取所有权,而传参是引用时,可以通过调用 clone() 复制该值,然后传递。

      1
      2
      3
      4
      let pre_block: &Block = xxx;
      Block::new("", pre_block.clone());
      // Block::new() 的定义如下
      pub fn new(data: &str, pre_block: Block)
  • 当持有堆中数据值的变量离开作用域时,其值将通过drop 被清理掉,除非数据所有权移动给了另一个变量所有。

  • 实现了Copy trait 的数据类型,赋值后所有权并不会移动。

    • 例如,常用的标量类型,分配到栈中。

引用与借用

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

注意:与使用& 引用相反的操作是解引用(dereferencing),它使用解引用运算符*。我们将会在第八章遇到一些解引用运算符,并在第十五章详细讨论解引用。

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let s1 = String::from("hello");

let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
s.len()
}

上面代码,&s1 创建了一个指向s1的引用,但是并不拥有它。所以当引用离开作用域时,其指向的值也不会被丢弃。

同理,函数签名使用&也是一样。

我们将获取引用作为函数参数称为借用(borrowing)。

可变引用

&mut <变量名>&符号后面添加mut 可变关键字,表示该引用为可变引用。

Rust 为了避免数据竞争,对可变引用做了限制。

  • 特定作用域中的特定数据有且只有一个可变引用。
    • 在不同的作用域中可以有多个可变引用
1
2
3
4
5
6
7
8
let mut s = String::from("hello");

{
let r1 = &mut s;

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

let r2 = &mut s;

引用的作用域:

  • 一个引用的作用域从声明的地方开始一直到最后一次使用为止。
1
2
3
4
5
6
7
8
9
let mut s = String::from("hello");

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

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

悬垂引用(Dangling References)

悬垂指针(dangling pointer),其指向的内存可能已经被分配给其他持有者。

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

引用规则总结:

  • 特定作用域中的特定数据
    • 要么只能有一个可变引用;
    • 要么只能有多个不可变引用;
    • 引用必须总是有效的。
  • 在作用域不重合的情况下,才有可能有多个。
    • 参考[可变引用](# 可变引用) 中的样例。

Slices(切片)

slice,是一个没有所有权的数据类型。

  • 针对集合的引用;

允许你引用集合中一段连续的元素序列,而不用引用整个集合。

  • 语法:&变量名[starting_index..ending_index]
  • 当作参数类型声明时:
    • 数组:&[T],例如:&[i32]
    • 字符串:&str
  • 如果从索引0开始,那么可以省略start_index部分,例如: &s[..5],== &s[0..5]
  • 如果取到结束,那么可以省略ending_index 部分,例如:&s[6..] == &s[6..11],这里整个字符串结尾索引为11。
  • 如果表示取整个范围,可以前后都省略,即,&s[..]

注: starting_index..ending_index 这样的语法是Rust 中的 range语法,表示一个范围。

  • 数据结构:
    • slice 中存储了
      • 开始位置的引用;
      • 长度;
    • 长度对应于ending_index - starting_index 的值;
1
2
3
let s = String::from("hello");

let slice = &s[0..2]; // 这里表示引用 he 这两个字符

字符串 slice

类声明: &str

字符串字面值就是 slice

1
let s = "Hello, world!"; // 这里的 s 就是 &str 类型

字符串字面值是不可变的,因为 &str 是一个不可变引用。

字符串 slice 作为参数

通常如果函数参数需要传递一个 String 类型操作参数,我们使用字符串slice 而不是String引用作为参数,可以使我么的函数更加通用并且不会丢失任何功能。

  • 当传递的实参为 String 类型时,我们传递整个String 的slice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let my_string = String::from("hello world");

// first_word 中传入 `String` 的 slice
let word = first_word(&my_string[..]);

let my_string_literal = "hello world";

// first_word 中传入字符串字面值的 slice
let word = first_word(&my_string_literal[..]);

// 因为字符串字面值 就是 字符串 slice,
// 这样写也可以,即不使用 slice 语法!
let word = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
// 省略
}

其他类型的 slice

字符串 slice 是针对字符串的,

其他集合类型也有slice。

例如:

1
2
let a = [1,2,4,5,6];
let a_slice = &a[1..3]; // 这个 slice 是 &[i32] 类型的

它跟字符串slice的工作方式一样。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信