翻译来源
https://jvns.ca/blog/2017/11/27/rust-ref/
面向群体
读过Rust书中生命周期章节的人,原则上理解它,但仍然对很多基本的Rust事物感到困惑。
讨论内容:
- Rust中的引用究竟是什么?
- boxed pointer / string / vec 是什么?他们与引用之间的联系是什么?
- 为什么我写的struct会有生命周期问题,我该如何做?
本文不讨论所有权和borrow checker。需要了解的请阅读rust book。本人能力有限,错误在所难免。
开始
让我们从一个简单的例子开始:定义一个struct
struct Container {
my_cool_pointer: &u8,
}
当编译时,编译器给出错误提示:
2 | my_cool_pointer: &u8,
| ^ expected lifetime parameter``
为什么不能编译?
探寻为什么这个简单的程序不编译有助于帮助我了解一些有关Rust的东西。
有一个简单的答案(“它缺少一个生命周期参数”),但是这个答案没有用。 所以我们来问一个更好的问题吧!
&
意味着什么?
你可能会说:“我知道&
是什么,它在C中与指针类似。”
但你真的确定吗?
上述代码中,你有一个指针&my_cool_pointer
,它指向某块内存。而这块内存可能是以下的3个类型中的一个。
- 堆
- 栈
- 程序数据段
Rust的最重要的事情(Rust中编程的事情令人困惑)是,它需要在编译时决定程序中的所有内存都需要被释放。
所以,我写出了如下的代码:
fn blah () {
let x: Container = whatever();
return;
}
当blah
返回时,x
离开作用域。此时我们需要弄清楚如何处理my_cool_pointer
,但是Rust该如何知道my_cool_pointer
的引用是哪一种类型?它究竟是在堆上还是在栈上?
所以这段代码有问题,Rust不让他编译。
用
Box
让我们的程序编译成功
如果我们知道my_cool_pointer
在堆上,我们便知道在它离开作用域时该如何做了:释放它。而告诉Rust编译器指针指向堆就是用Box
。Box<u8>
就是指向堆上的byte。
下面的代码可以编译成功!
struct Container {
my_cool_pointer: Box<u8>,
}
这时我们可以使用我么的Container
了。
truct Container {
bytes: Box<u8>,
}
fn test() -> Container {
// this is where we allocate a byte of memory
return Container{bytes: Box::new(23)};
}
fn main() {
let x = test();
// when `main` exits, the memory we allocated gets freed!
}
Rust中box vs Java中box
我对box
也很疑惑。Java中box
是对原始类型的装箱,例如Integer
对int
装箱,你不可能拥有non-box
的指针(引用),Java中的所有指针都是指向堆的。
Rust中box
(Box<T>
)和 Java中box 不一样,在Java中装箱后的指针包含了额外的数据(word)(具体作者未作说明),在Rust中boxed pointers有时包含额外数据(“vtable pointer”,虚表)有时则没有,它由Box<T>
中T
指向的是类型还是trait决定。
无论如何,当你有一个 boxed pointer ,编译器会使用它在堆上分配的信息来决定在编译的代码中释放内存的位置。 在我们上面的例子中,编译器会在main
的末尾插入一个free
。
如果你想指向现有的内存呢?
好的,现在我们知道如何分配新内存并引用它(使用boxed pointer!)。 但是如果你想在某个地方引用一些现有的内存并指向它呢?
这里有一个很好的例子,DWARF parser– gimli。它不做任何内存分配,只是单纯地载入DWARF data到内存,然后标出它。这时你需要用到生命周期,这里不做具体解释生命周期详情。下面的代码仍然可以编译:
struct Container<'a> {
my_cool_pointer: &'a u8,
}
所以现在我们用两种不同的方式编译我们的结构体:通过向我们的结构体定义添加一个生命周期参数,并使用一个boxed pointer 而不是引用。
下面让我们来谈谈在堆上分配的东西。
我们如何知道一个变量是否分配在堆上?
到此我们知道类型为Box<T>
的变量分配在堆上,存在总是分配在堆上的变量吗?是的
,它们如下:
Vec<T>
(数组)String
(字符串)Box<T>
(指针).
以上的三种类型有对应的引用类型。&[T]
– Vec<T>
, &str
– String
, &T
–Box<T>
.
我认为了解上述三种类型与他们对应引用之间的关系对于Rust编程很重要。
将Vec<T>
转为&[T]
很简单:vec.as_ref()
, String
和Box<T>
同样都有.as_ref()
函数来转成引用类型。
但是你反向操作就不那么简单了。你需要clone并且在堆上分配新的内存
存在两种struct:有生命周期与无生命周期
至此,我们到达了Rust有用部分。
所有有意义的struct 都包含数据。有些需要生命周期
作为参数有些则不需要。
下面是一个无生命周期的struct,它有指向数组,string和byte的指针,当实例离开作用域时,所有内存都被释放。
struct NoLifetime {
x: Vec<u8>,
y: String,
z: Box<u8>
}
接下来是一个有生命周期的struct,它同样有指向数组,string和byte的指针,
但我们无从struct定义中得知,这些指针指向的是堆还是栈。
struct Lifetime<'a> {
x: &'a [u8],
y: &'a str,
z: &'a u8
}
通常struct有生命周期还是没有
同样的问题:当我写一个Rust结构体时,我会多久使用一次生命周期,结构体是否应该拥有自己的所有数据?无法回答。
https://github.com/BurntSushi/ripgrep 中的结构体大部分没有生命周期。
也有大量用生命周期的项目