文章目录
三、所有权
1、所有权概念
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者(owner)的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
String 类型
这里使用 String
作为例子,并专注于 String
与所有权相关的部分。
String
是分配在堆上的数据,可以存储在编译时位置大小的文本。
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
println!("{}", s); // 将打印 `hello, world!`
}
内存分配
在堆上进行内存分配,意味着:
- 必须在运行时向内存分配器(memory allocator)请求内存。
- 需要一个当我们处理完
String
时将内存返回给分配器的方法。
第二步在一些带 GC 的语言中,不需要担心。不带 GC 的语言则需要手动释放内存。
Rust采用了一种不同的策略:内存在拥有它的变量离开作用域后就被自动释放。
{
let s = String::from("hello"); // 从此处起,s 是有效的
// 使用 s
} // 此作用域已结束,
// s 不再有效
在变量离开作用域时,会调用drop
函数,释放内存。
下面是复杂场景的一些探索:
变量与数据交互的方式(一):移动
let x = 5;
let y = x;
这里“将 5
绑定到 x
;接着生成一个值 x
的拷贝并绑定到 y
”。因为整数是有已知固定大小的简单值,所以这两个 5
被放入了栈中。
下面是 String
版本:
let s1 = String::from("hello");
let s2 = s1;
实际上是 s2
从栈上拷贝了 s1
的指针、长度和容量,并没有复制指针指向的堆上数据。
这种模式与上面的整数直接拷贝数据不同,没有对堆内存进行拷贝,降低对运行时性能的影响。
这里就会出现一个问题:当 s2
和 s1
离开作用域,他们都会尝试释放相同的内存。这是一个叫做 二次释放(double free)的错误,也是之前提到过的内存安全性 bug 之一。
Rust的处理方式是:认为 s1
已经无效了,不需要清理。同时也就意味着无法使用 s1
。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
这段代码无法编译通过,因为 Rust 禁止使用无效的引用!
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error
**!!!**这种操作被称为 移动(move),而不是浅拷贝。
另外,**这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 “深拷贝”。**因此,任何 自动 的复制可以被认为对运行时性能影响较小。
变量与数据交互的方式(二):克隆
相当于是深拷贝的方法
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
Rust 有一个叫做 Copy
trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上(第十章详细讲解 trait)。如果一个类型实现了 Copy
trait,那么一个旧的变量在将其赋值给其他变量后仍然可用。Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。如果我们对其值离开作用域时需要特殊处理的类型使用 Copy
注解,将会出现一个编译时错误。
作为一个通用的规则,任何一组简单标量值的组合都可以实现 Copy
,任何不需要分配内存或某种形式资源的类型都可以实现 Copy
。如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
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 移出作用域。不会有特殊操作
当尝试在调用 takes_ownership
后使用 s
时,Rust 会抛出一个编译时错误。
返回值与作用域
返回值也可以转移所有权。
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
被清理掉,除非数据被移动为另一个变量所有。
如果需要在传入参数之后,再次使用这个参数,可以使用元组来返回多个值,将参数返回,再次转移所有权:
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)
}
输出为:The length of ‘hello’ is 5.
实现了 s1 作为参数之后,再次使用。
Rust 对此提供了一个功能,叫做 引用(references),可以避免这种繁琐的方式
2、引用与借用
引用
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");
}
这份代码无法编译成功。
可变引用
将变量声明为可变变量 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);
这份代码同样无法编译通过。
!!!一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。
总结两个规则:
- 同一个对象的可变引用和不可变引用不能有生命周期重叠。
- 同一个对象的多个可变引用不能有生命周期重叠。
悬垂引用
在具有指针的语言中,很容易通过释放内存时保留指向它的指针而错误地生成一个 悬垂指针(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
}
3、slice
字符串 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 无效的问题。
字符串字面值就是 slice
let s = "Hello, world!";
这里 s
的类型是 &str
:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str
是一个不可变引用。
最后吐槽一下,csdn 居然连 Rust 的语法高亮都没做。。。。。。