Rust 中的所有权
— 焉知非鱼变量绑定 #
在 Rust 中, 我们将一些值绑定到一个名字上, 称之为变量绑定。使用 let
来声明一个绑定:
fn main() {
let x = 5;
}
所有权 #
Rust 中的变量绑定有一个属性: 变量拥有它们所绑定的值的所有权。当绑定超出作用域, 变量绑定的资源就会被释放。例如:
fn foo() {
let v = vec![1,2,3,4,5];
}
当 v
进入作用域, 会在栈上创建一个新的 vector, 并在堆上为该 vector 的 5 个元素分配空间。当 v
在 foo()
的末尾离开作用域时, Rust 会释放该 vector 占用的资源。
移动语义 #
在 Rust 中, 一个值一次只能有一个所有者。
let v = vec![1,2,3,4,5]; // ① 声明一个绑定
let s = v; // ② v moved to s
println!("v[0] is: {}", v[0]); // ③ error: use of moved value: `v`
除了 ② 里面发生所有权转移(move)外, 另外一种常见的的 move 语义发生在将变量作为参数传递给函数时:
fn take(v: Vec<i32>) {
// take the ownship of `v`
}
let v = vec![1,2,3,4,5]; // ① 声明一个绑定
take(v); // ② 实参绑定给形参, 所有权转移到 take 函数内
println!("v[0] is: {}", v[0]); // ③ error: use of moved value: `v`
为什么移动了绑定之后我们不能再使用它们? #
#let v = vec![1,2,3,4,5];
#let mut s = v;
s.truncate(2);
假设 v
仍可以访问, 这会产生一个无效的 vector, 因为 v
不知道堆上的数据已经被截断了。现在 v
在栈上的部分与堆上的相应部分信息并不一致。v
仍然认为有 5 个元素, 并乐意我们访问那些不存在的元素 v[3], v[4]。这会导致下标越界错误, 更糟糕的是你可能访问了未经授权的数据。
这就是为什么 Rust 在所有权被移动后禁止使用原来的绑定 v
的原因。
Copy 类型 #
当所有权被转移给另外一个绑定以后, 就不能再使用原始绑定。但是并不都是如此, Copy trait 会改变这个行为。凡是实现了 Copy trait 的类型, 其所有权并不像你想象的那样遵循“所有权规则”被移动。例如 Rust 中的所有的基本类型都实现了 Copy
trait。
fn main() {
let a = 5;
let _y = double(a);
println!("{}", a);
}
fn double(x: i32) -> i32 {
x * 2
}
所有权之外 #
如果我们在的用完函数后,还想继续使用原始绑定, 我们不得不在函数中交还所有权:
fn foo(v: Vec<i32>, s: Vec<i32>) -> (Vec<i32>, Vec<i32>, i32) {
// do stuff with `v1` and `v2`
// 交还所有权和计算结果
(v, s, 42)
}
let v1 = vec![1,2,3];
let v2 = vec![2,4,6];
let (v1, v2, answer) = foo(v1, v2);
这样函数就变得复杂了。
引用和借用 #
我们使用「借用」来改写上面的函数:
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
// do stuff with `v1` and `v2`
// return the result
42
}
let v1 = vec![1,2,3];
let v2 = vec![2,4,6];
let answer = foo(&v1, &v2);
// we can use `v1` and `v2` here!
我们来看一个具体的例子:
fn main() {
// 借用不可变引用
fn sum_vec(v: &Vec<i32>) -> i32 {
return v.iter().fold(0, |a, &b| a + b);
}
// 借用两个 Vector 并使它们相加
// 这种借用不允许对所借引用进行更改
fn foo(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
let s1 = sum_vec(v1);
let s2 = sum_vec(v2);
// 返回答案
s1 + s2
}
let v1 = vec![1,2,3];
let v2 = vec![4,5,6];
let answer = foo(&v1, &v2);
println!("{}", answer);
}
在函数 sum_vec 和函数 foo 中, 我们接收引用 &Vec<i32>
作为参数, 而不是 Vec 作为参数。在调用函数的时候, 我们传递引用 &v1
和 &v
, 而不是直接传递 v1
和 v2
。借用变量的绑定在它离开作用域时并不释放资源。这意味着, 在调用 foo()
之后, 我们可以再次使用原始绑定。
引用是不可变的。那么在 foo()
中, vector 不能被改变:
fn foo(v: &Vec<i32>) {
v.push(6);
}
let v = vec![];
foo(&v);
&mut 引用 #
&mut T, 可变引用, 允许你改变你借用的资源。例如:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x); // 6
y 是 x 的一个可变引用, 接着把 y 指向的值加 1。注意, x 也必须被标记为 mut
, 因为不能获取一个不可变值的可变引用。
y 前面的星号(*
) 是解引用, 因为 y 是一个 &mut
引用, 需要使用星号来访问引用的内容。
但是这样写你不觉得奇怪吗?为什么还要使用额外的花括号呢?因为我们需要额外的作用域, 如果移除花括号, 则会编译报错:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
note: previous borrow of `x` occurs here; the mutable borrow prevents
subsequent moves, borrows, or modification of `x` until the borrow ends
let y = &mut x;
^
note: previous borrow ends here
fn main() {
}
^
啊,好像越来越复杂了,其实 Rust 中的借用要符合几个规则:
第一, 任何借用持续的作用域必须比所有者更小。第二, 同一作用域下, 要么只有一个对资源 A 的可变引用(&mut T), 要么有 N 个不可变引用(&T), 但不能同时存在可变和不可变的引用。
作用域 #
让我们移除上面代码中的花括号:
fn main() {
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
}
编译报错:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
^
因为我们违反了规则:在同一作用域下,不能同时存在可变和不可变引用。在这个例子中, 我们有一个指向 x 的 &mut T
, 所以我们不允许创建任何 &T
。
错误记录提示了我们应该如何理解这个错误:
note: previous borrow ends here
fn main() {
}
^
可变借用一直持续到 main 函数的末尾花括号处。我们需要可变借用在我们尝试调用 println!
之前结束并生成一个不可变借用。我们的作用域看起来像这样:
fn main() {
let mut x = 5;
let y = &mut x; // -+ &mut borrow of `x` starts here.
// |
*y += 1; // |
// |
println!("{}", x); // -+ 试图从这儿借用 `x`
} // -+ &mut borrow of `x` ends here.
作用域冲突了, 所以要加花括号:
#![allow(unused_variables)]
fn main() {
let mut x = 5;
{
let y = &mut x; // -+ &mut borrow starts here.
*y += 1; // |
} // -+ ... and ends here.
println!("{}", x); // <- Try to borrow `x` here.
}
借用避免了什么 #
避免迭代器失效 #
当你尝试改变你正迭代的集合时, Rust 的借用检查器会阻止你:
let mut v = vec![1,2,3];
for i in &v {
println!("{}", i);
}
我们对 v
进行迭代, 我们只得到了元素的引用。v 本身是不可变借用, 这意味着我们在迭代时不能改变它:
let mut v = vec![1,2,3];
for i in &v {
println!("{}", i);
v.push(34);
}
编译报错:
error: cannot borrow `v` as mutable because it is also borrowed as immutable
v.push(34);
^
note: previous borrow of `v` occurs here; the immutable borrow prevents
subsequent moves or mutable borrows of `v` until the borrow ends
for i in &v {
^
note: previous borrow ends here
for i in &v {
println!(“{}”, i);
v.push(34);
}
^
我们不能修改 v
是因为它被循环借用。
释放后使用 #
引用必须与它所引用的值存活的一样长。Rust 会检查引用的作用域:
let y: &i32; // ①
{ // ②
let x = 5; // ③
y = &x; // ④
} // ⑤
println!("{}", y); // ⑥
这会编译报错:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
{
let x = 5;
y = &x;
}
note: ...but borrowed value is only valid for the block suffix following
statement 0 at 4:18
let x = 5;
y = &x;
}
也就是说, y 只在 x 存在的作用域中有效。一旦 x 消失, y 就变成无效引用。这个错误说, x 活的不够久, 因为它在应该有效的时候(⑥)是无效的。
当引用在它所引用的变量之前声明也会导致类似的问题。这是因为同一作用域中的资源以它们被声明的相反的顺序被释放:
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
会报编译错误:
error: `x` does not live long enough
y = &x;
^
note: reference must be valid for the block suffix following statement 0 at
2:16...
let y: &i32;
let x = 5;
y = &x;
println!("{}", y);
}
note: ...but borrowed value is only valid for the block suffix following
statement 1 at 3:14
let x = 5;
y = &x;
println!("{}", y);
}
在上面的例子中, y 在 x 之前被声明, 这意味着 y 比 x 的生命周期更长, 这是 Rust 规则所不允许的。
生命周期 #
想象一下下列操作:
- ① 我获取了某种资源的句柄
- ② 我借给你一个关于这个资源的引用
- ③ 我决定不再需要这个资源了, 并且释放了它, 而你仍然持有对它的引用
- ④ 你要使用这个资源
奥! 你的引用指向一个无效的资源。如果这个资源是内存, 则这叫「释放后使用」。例如:
let r; // ⓪ 引入引用 `r`
{
let i = 1; // ① `i` 是值 1 的所有者
r = &i; // ② 把 `i` 借给 `r`
} // ③ `i` 超出作用域, 被删除
println!("{}", r); // ④ `r` 仍然指向 `i`
在 ④ 之后, 变量 i
超出作用域被释放, 而 ⑤ 中, 打印 r
指向的值, 但是 r
所借用的 i
已经在内存中被删除了。所以 r
现在指向的内存中, 已经不是 i
了。
Rust 不仅检查不同变量作用域, 还检查不同变量的生命周期。
当我们将引用作为函数的参数时, 情况变得更复杂了:
fn skip_prefix(line: &str, prefix: &str) -> &str {
// ...
# line
}
let line = "lang:en=Hello World!";
let lang = "en";
let v;
{
let p = format!("lang:{}=", lang); // -+ `p` 进入作用域
v = skip_prefix(line, p.as_str()); // |
} // -+ `p` 超出作用域
println!("{}", v);
skip_prefix
函数接收两个 &str
引用作为参数并返回一个 &str
。通过传入 line 和 p 的引用来调用 skip_prefix 函数, 但是 line 和 p 是两个具有不同生命周期的变量。现在 println!
那行代码的安全完全依赖于 skip_prefix 函数返回的引用是仍然存活的 line 还是已经被释放掉的 p。
因为存在上述歧义, Rust 会拒绝编译这段代码。为了继续我们需要向编译器提供更多关于引用的生命周期的信息。这可以通过在函数签名中显式地标明生命周期来完成:
fn skip_prefix<'a, 'b>(line: &'a str, prefix: &'b str) -> &'a str {
// ...
# line
}
函数名后面的 <'a, 'b>
引入了两个生命周期参数 'a
和 'b
。 接下来的函数签名中的每个引用都关联了一个生命周期参数, 这是通过在 &
后面加上生命周期的名字来完成的。
这样编译器能推断出 skip_prefix 函数的返回值与 line
参数有着相同的生命周期, 这样就使得之前例子中的 v
引用即使在 p
离开作用域之后也能安全使用。
生命周期注解是 descriptive
(描述性的), 这意味着引用的生命周期是由代码决定的。
语法 #
'a
读作"生命周期 a"。技术上讲, 每一个引用都有一些与之相关的生命周期。一个显式生命周期的例子:
fn bar<'a>(...)
一个函数可以在 <>
之间有"泛型参数", 生命周期也是其中一种。
我们用 <>
声明了生命周期。bar
有一个生命周期 'a
。如果我们有两个拥有不同生命周期的引用作为参数, 它应该看起来像这样:
fn bar<'a, 'b>(...)
接着在我们的参数列表中, 我们使用了我们命名的生命周期:
...(x: &'a i32)
如果我们想要一个 &mut
引用, 我们这么做:
...(x: &'a mut i32)
&mut i32
和 &'a mut i32
是一样的, 只是后者在 &
和 mut i32
之间夹了一个 'a
生命周期。&mut i32
读作"一个 i32 的可变引用", 而 &'a mut i32
读作"一个带有生命周期 ‘a 的 i32 的可变引用"。
在 struct 中 #
当处理结构体时, 你也需要显式的生命周期:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // 这等价于 `let _y = 5; let y = &_y;`
let f = Foo { x: y };
println!("{}", f.x);
}
在 struct
名字的后面声明了一个生命周期 <'a>
:
struct Foo<'a>
接着在结构体中使用它:
#struct Foo<'a> {
x: &'a i32,
#}
然而为什么这里需要一个生命周期呢?因为我们需要确保任何 Foo 的引用不能比它包含的 i32 引用活的更久。
impl 块 #
让我们给 Foo
结构体实现一个方法:
struct Foo<'a> {
x: &'a i32,
}
impl<'a> Foo<'a> {
fn x(&self) -> &'a i32 { self.x }
}
fn main() {
let y = &5;
let f = Foo { x: y };
println!("x is {}", f.x());
}
我们需要在 impl
关键字后面为 Foo
声明一个生命周期。就像在函数中那样, 我们重复了 'a
两次: impl<'a>
定义了一个生命周期, 然后 Foo<'a>
使用了它。
多个生命周期 #
如果你有多个引用, 你可以多次使用同一个生命周期:
fn x_or_y<'a>(x: &'a str, y: &'a str) -> &'a str {
# x
#}
这意味着在同一作用域内, x 和 y 的存活时间一样久, 并且返回值和 x 与 y 的存活时间一样久。如果需要 x 和 y 有不同的生命周期, 可以使用多个生命周期参数:
fn x_or_y<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
# x
#}
在这个例子中, x 和 y 有不同的有效作用域, 而返回值和 x 有着相同的生命周期。
理解作用域 #
理解生命周期的一个办法是想象出引用的有效作用域的示意图。例如:
fn main() {
let y = &5; // -+ `y` 进入作用域
// Stuff ... // |
// |
// |
} // -+ `y` 超出作用域
再加入我们的 Foo:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let y = &5; // -+ `y` 进入作用域
let f = Foo { x: y }; // -+ `f` 进入作用域
// Stuff ... // |
// |
// |
} // -+ `f` 和 `y` 先后超出作用域
我们的 f
生存在 y
的作用域之中, 所以一切正常。那么如果不是呢:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ `x` 进入作用域
// |
{ // |
let y = &5; // -+ `y` 进入作用域
let f = Foo { x: y }; // -+ `f` 进入作用域
x = &f.x; // | | 这引发错误
} // -+ `f` 和 `y` 先后超出作用域
// |
println!("{}", x); // -+ `x` 超出作用域
}
如上面的作用域示意图所示, f
和 y
的作用域小于 x
的作用域。但是, 在 x = &f.x
这里, 我们让 x
引用了将要离开作用域的变量, 这会导致"释放后使用"。
就像命名正则表达式一样, 命名作用域赋予作用域一个名字。有了名字我们就可以谈论它了。
‘static #
名字叫 static
的生命周期是特殊的。它代表某个东西具有横跨整个程序的生命周期。我们已经见到过:
let x: &'static str = "Hello, world.";
基本字符串是 &'static str
类型的, 因它的引用一直有效: 它们被写入了最终库文件的数据段。另外一个例子是全局变量:
static FOO: i32 = 5;
let x: &'static i32 = &FOO;
它在二进制文件的数据段中保存了一个 i32, 而 x 是它的一个引用。