跳到主要内容

Hello Rust [2] - 所有权

所有权

栈和堆

所有权的目的主要目的就是为了管理堆数据。

  • 栈:后进先出,必须占用已知且固定大小空间。
  • 堆:缺乏组织,需要内存分配器分配内存。可以保存大小变化或者位未知的数据。

栈与堆的比较:

  • 入栈的速度比堆的分配内存更快,因为不需要搜索内存空间。
  • 栈的访问比堆快。

所有权规则

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

变量作用域

fn main() {
{ // s 在这里无效, 它尚未声明
let s = "hello"; // 从此处起,s 是有效的

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

  • s进入作用域的时候,是有效的,直到持续到它离开作用域。

String,内存,分配

{
// 从String命名空间声明一个可变的字符串s
let mut s = String::from("hello");

// 使用 s
} // 作用域结束,s失效。

对于这个String类型:

  • 需要在运行时,向内存分配器请求内存(memory allocator)方法
  • 处理完数据需要释放(GC)

RUST的GC是基于所有权的。所以s离开作用域的时候,rust会调用一个特殊的函数dropString的作者可以防止释放内存的代码,Rust会在结尾的}自动调用drop

信息

在C++中,这种“在生命周期结束时释放资源的模式”有时候被称为资源获取即初始化(RAII)模式。

移动

当两个数据指针指向同一个堆地址,可能会尝试释放相同的内存,这就是 二次释放(double free) 错误。所以,Rust会失效掉比较早的一个数据指针。这就是所谓的 移动(move)。

let s1 = String::from("hello");
let s2 = s1; // s1 被指向了 s2

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

不同于深拷贝和浅拷贝, RUST会使第一个变量无效。同时只有s2是生效的。
同时,Rust永远也不会自动创建数据的“深拷贝”, 自动的复制对运行时的影响都比较小。

克隆 & 拷贝

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

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

在堆中的数据,你需要调用clone克隆他们,在栈中,则会自动进行拷贝。

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y); // x, y 都是可以访问的。

如果一个类型实现了Copy trait,则会在复制的时候进行拷贝。
默认的,我们有这些类型实现了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 移出作用域。没有特殊之处Ï

takes_ownership后调用s会抛出一个编译错误。

返回值与作用域

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

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("yours"); // 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 并移出给调用的函数
}

变量的所有权都是遵循相同的模式:赋值给听一个变量的时候移动它。当持有堆中数据量的变量离开作用域的时候,其值将通过drop被清理掉。除非,被移动到另一个变量。
除了这种方式,Rust还是提供了另一个方式来不用获取所有权就可以使用值的能力:引用(references)。

引用与借用

  • 引用(reference):创建一个指向变量的指针。
  • 借用(borrowing):创建一个引用的行为叫做借用。
fn main() {
// 声明了一个堆中的字符串变量
let s1 = String::from("hello");
// 我们给了s1的引用,但是不转移所有权。
let len = calculate_length(&s1);

println!("The length of '{}' is {}.", s1, len);
}
// 函数定义这里也需要获取 &String,而不是String
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开了作用域,但是因为它是一个引用,所以这里什么也不会做

引用允许我们使用值,但是不获取其所有权。
image.png

信息

与引用相反还有一个解引用(dereferencing),使用解引用运算符 *

借用的值,是不允许修改的。如果需要修改引用的值,那么我们就需要可变引用。

可变引用

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);

防止同一时间对同一数据进行多个可变引用的限制允许可变性,但以一种受限的方式允许。
好处在于:可以在编译时,避免数据竞争

提示

数据竞争:

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

数据竞争会导致未定义行为,难以在运行时追踪,诊断以及修复。

在可变引用和不可变引用中存在类似规则:

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

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

println!("{}, {}, and {}", r1, r2, r3);Ï

不可以在拥有不可变引用的同事去拥有可变引用。
但是当引用的作用域结束以后,就可以重新使用了。比如:

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

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

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

编辑器在作用域结束之前判断不再使用的引用的能力被称为 "非词法作用域生命周期"。

悬垂引用

悬垂指针是指 某个指针指向的内存已经被分配给其他持有者。 在Rust中,编译器会确保引用永远不会变成悬垂状态。

提示

小结,在Rust中:

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

Slice 引用

  • slice允许我们引用集合中一段连续的元素序列,而不用引用整个集合。
  • slice是一种引用
fn main() {
let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];
}

它在堆中的指向类似于这样:
image.png

  • ..是rust的range语法,语法类似:
let s = String::from("hello");
// 省略开头
let slice = &s[0..2];
let slice = &s[..2];
// 省略尾部
let slice = &s[3..len];
let slice = &s[3..];

slice获取的字符串, 在原始s被释放以后就不能被获取了。因为:

  • RUST不允许同时存在一个可变引用和一个不可变引用。

比如这个例子:

fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

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

let word = first_word(&s);

s.clear(); // 错误!

println!("the first word is: {}", word);
}

这里在printLn中就会抛出一个编译错误

字符串字面值就是一个slice

从slice的角度来理解为什么字符串字面量:

let s = "Hello, world";

这里的s类型是&str:一个指向二进制程序特定位置的slice。
所以字符村字面量是不可变的,&str是一个不可变引用。