02 所有权
2.1 所有权概念
-
所有权:Rust 程序管理其使用计算机内存的方式(不同于其他语言的垃圾回收与手动的内存分配与回收), 编译器在编译时根据规则进行检查,在运行时,所有权系统的任何功能都不会减慢程序。
-
栈(Stack)和堆(Heap):都是代码运行时可用的内存,但是结构不同。
-
栈:其上存储的数据大小固定(指针的大小是已知并且固定的),后进先出,新存储的数据无需搜索内存空间,总是在栈顶,不需要寻址。
-
堆:根据需求在堆内存中找到一块足够存放数据的地址,将其标记为已使用,并返回该地址的指针。此过程叫做分配。
访问堆上的数据比访问栈上的数据慢,因为堆上的数据必须通过指针来访问,现代处理器在内存中跳转越少就越快。
调用一个函数时,传递给函数的值(包括可能指向堆上数据的指针)和函数的局部变量被压入栈中。当函数结束时,这些值被移出栈。
-
-
所有权目的:跟踪哪部分代码正在使用堆上的数据,最大限度的减少堆上的重复数据的数量,以及清理堆上不再使用的数据确保不会消耗空间。
2.1.1 所有权规则
-
Rust 中每一个值都有一个被称为其所有者 (owner) 的变量。
-
值在任一时刻都只有一个所有者。
-
当所有者(变量)离开作用域,这个值将被丢弃。
2.1.2 String 类型
2.1.3 内存分配
2.1.4 变量与数据交互的方式(一):移动move
拷贝栈上存放的数据,并删除原地址的数据。
String
类型是可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存。将指针存储在栈上。
{
//此时x无效,x未被声明
let x = String::from("hello"); //从此处开始x有效,进入x作用域
let y = x; //此处开始,x的值移动(与深拷贝浅拷贝不同)到y,x不再效,x移出作用域并调
//用drop释放x的内存,此处y开始有效,
//y进入作用域,避免了在函数结尾drop内存时的二次释放造
//成的内存污染。
}//此处y无效,调用drop删除堆中y的内存。不需要再次释放x。
2.1.5 变量与数据交互的方式(二):克隆clone
拷贝栈和堆上的数据。clone()
被调用时,特定代码被执行,并且特别消耗资源。
{
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
2.1.6 变量与数据交互的方式(二): 拷贝copy
只拷贝栈上的数据:
{
let x = 1;//进入x作用域,x开始有效
let y = x;//将栈上的x值拷贝到y上,此时y有效,x也有效,因为类似整型这样的类型上,使用copy trait这样的特殊
//注解,如果拥有copy trait注解,则旧的变量赋值给新的变量后仍然可用。
}//y先被移出作用域,并调用drop,内存被释放;x再被移出作用域,调用drop,内存被释放。
-
Copy Trait:Rust 的特殊注解,作用是当旧变量的值重新赋值给新的变量时,旧变量仍然可用。
- 所有标量类型:整型、布尔类型、浮点数类型、字符类型
- 元组,当且仅当其包含的类型也是 Copy 的时候。
eg:(i32, i32)是 Copy 的,但(i32, String)不是 Copy 的
-
Drop Trait:Rust 不允许自身或其任何部分实现的 Drop Trait 的类型使用 Copy Trait。
2.1.6 所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 ...后面再次使用不再有效
// println!(s); // 当尝试在调用 takes_ownership 后使用 s 时,Rust 会抛出一个编译时错误。
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 移出作用域。不会有特殊操作
2.1.7 返回值与作用域
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 并移出给调用的函数
}
- 将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。
- 如果需要在传入参数之后,再次使用这个参数,可以使用元组返回多个值,将参数返回,再次转移所有权。
Rust 对此提供了一个功能,叫做 引用(references),可以避免这种繁琐的方式
fn main() {
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(); // len() 返回字符串的长度
(s, length)
}
2.2 引用与借用
2.2.1 引用
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
, &
符号就是 引用,它们允许你使用值但不获取其所有权。
与使用 &
引用相反的操作是 解引用(dereferencing) 运算符为*
。
&s1
语法让我们创建一个 指向 值 s1
的引用,但是并不拥有它。因为并不拥有这个值,所以当引用停止使用时,它所指向的值也不会被丢弃。
fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
// 所以什么也不会发生
将创建一个引用的行为称为 借用(borrowing)
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
!!!报错
: 这份代码无法编译成功。由于借用并不拥有变量的所有权,所以无法对变量进行修改。
2.2.2 可变引用
将变量声明为可变变量 mut
,然后函数形参类型改为 &mut
:
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);
!!! 这份代码会编译失败!
:在 r1 最后使用之前,不能再次让 r2 引用 s。这样是为了避免数据竞争。
数据竞争(data race) 类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
类似的规则也存在于同时使用可变与不可变引用中。
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
!!! 这份代码同样无法编译通过。
一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。
总结两个规则:
- 同一个对象的可变引用和不可变引用不能有生命周期重叠。
- 同一个对象的多个可变引用不能有生命周期重叠。
2.2.3 悬垂引用
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(dangling pointer),所谓悬垂指针是其指向的内存可能已经被分配给其它持有者。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放, 返回了一个不存在的空引用。
这份代码编译无法通过,因为 s 在结束时被释放,reference_to_nothing 成了悬垂指针。
解决方法是将 s 的所有权通过函数返回移动出去:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
2.3 Slice
2.3.1 字符串 slice
字符串 slice(string slice)是 String 中一部分值的引用,前闭后开。
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
对于从索引 0 开始的 slice,可以省略两个点前面的值:
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
同理也可以舍弃尾部的数字:
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
slice 相当于不可变引用,如果运行如下代码,就会编译失败:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // 错误!
println!("the first word is: {}", word);
}
因为 s.clear()
; 需要获取一个可变引用,但是后面又要输出 word,这边是前一个不可变引用的生命周期还未结束,此时 s.clear() 会报错。
这样设计就避免了,对字符串改动导致 slice 无效的问题。
2.3.2 字符串字面值就是 slice
let s = "Hello, world!";
这里 s 的类型是 &str
:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;
&str
是一个不可变引用。
总结:不管是变量赋值还是作为参数传递都是堆移动,栈是 copy。
2.3 Rc和Arc
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第四章
原文链接