Rust 3.4 指针
17 Jan 2015 by LelouchHe
Rust的指针是它一个独特而强大的功能,同样也是Rust新手最为困过的一个问题.它同样让来自其他支持指针语言的人,比如C++程序员们感到困惑.这里,将帮助你理解整个功能.
要谨慎对待Rust中的非引用指针:要用于特定的目的,而不仅仅是为了能编译通过.每个指针类型都解释了使用它们的正确方法.除非是这些情况,否则还是默认使用引用.
你也许对小抄很感兴趣,那个是一个关于指针类型,名称和目的的综述.
介绍
如果你对指针的概念不是很熟悉,我们先简单介绍下.指针在系统编程语言中,是一个基础的概念,所以理解它很重要.
指针基础
当你创建一个新的变量绑定时,你给存储在栈上的值绑定了一个名字.(如果你对堆和栈的概念不熟悉,请参考这里,因为后面都会假定你了解这些概念).比如:
let x = 5is;
let y = 8is;
(译者: 可能的栈结构.由于Mardown无法输入表格,暂时以列表形式.前面是地址,后面是值):
- 0xd3e030: 5
- 0xd3e028: 8
这里的内存地址是我们编的,仅仅是示例而已.不论怎样,重点是我们使用的变量名x
对应的内存地址是0xd3e030
,该地址中的值是5
.当引用x
时,就得到了对应的值.因此,x
是5
.
现在引入指针.某些语言中,只有一种指针类型,但在Rust中,有很多指针类型.此处,我们使用Rust的引用(reference),这是最简单的一种指针.
let x = 5is;
let y = 8is;
let z = &y;
- 0xd3e030: 5
- 0xd3e028: 8
- 0xd3e020: 0xd3e028
看到区别了么?指针的值,并不是真实值,而是内存中的一个地址.此处,就是y
的内存地址.x
和y
的类型是isize
,但z
的类型是&isize
.我们可以使用{:p}
来打印地址:
let x = 5is;
let y = 8is;
let z = &y;
println!("{:p}", z);
这个会输出0xd3e028
,就是那个虚构的内存地址.
因为isize
和&isize
是不同的类型,所以我们无能相加:
let x = 5is;
let y = 8is;
let z = &y;
println!("{}", x + z);
会有如下错误:
error: mismatched types: expected `isize` but found `&isize`
我们可以通过星号操作符(“*“)对指针进行解引用(dereference).解引用一个指针,可以获取指针指向的内存位置中的值.下面的代码可以运行:
let x = 5is;
let y = 8is;
let z = &y;
println!("{}", x + *z);
这次输出13
.
搞定!这就是指针:它们指向某些内存地址.没别的了.已经讨论了指针是什么,接下来讨论使用指针的原因.
指针使用
Rust的指针是很有用的,不过和其他系统编程语言有所不同.后面我们会介绍每种指针的正确用法,这里,介绍下指针在其他语言中的用法:
C语言中,字符串是一个指向char
集合的指针,并以’\0’结尾.字符串唯一的使用方法就是通过使用指针.
经常使用指针来指向栈范围之外的内存地址.比如,在上面的例子里,使用了2个栈上变量,所以可以用变量名来表示他们.但如果分配的是堆上的内存空间,就没有合适的变量名称了.在C中,malloc
用来分配堆上内存,返回指向该内存地址的指针.
更通用的来看,当你想要一个大小可变的结构时,你就需要一个指针.你没法在编译期知道要分配的内存大小,所以你只能在运行时分配一块内存,并使用一个指针指向它.
指针在一些只能传值而不能传址的语言中也很有用.基本上讲,语言有2种选择(下面是编造的语法,不是Rust):
func foo(x) {
x = 5
}
func main() {
i = 1
foo(i)
// i的值是多少?
}
在传值的语言中,foo
得到的是i
的一个拷贝,所以原先的i
不会被修改.此时,i
还是1.在传址的语言里,foo
得到的是i
的一个引用,因此就能改变原来i
的值.此时,i
就变成了5.
这个和指针有什么关系呢?因为指针指向内存地址:
func foo(&int x) {
*x = 5
}
func main() {
i = 1
foo(&i)
// i的值是多少?
}
就算在传值的语言里,i
的值也会变成5的.因为参数x
是一个指针,我们确实给foo
传了一个拷贝,但这个拷贝是指向内存中的地址(译者: 即i
的地址),所以,当赋值时,原先的i
也被修改了.这种模式称为”按值传引用”(pass reference by value).非常取巧!
常见指针问题
已经讨论了指针,也说了指针的好处.那么坏处是什么呢?Rust就是试图减少这些问题的,但在其他语言里,这些问题确实都存在:
未初始化的指针会有问题.比如,下面程序会怎样?
&int x;
*x = 5; // 额...
谁知道呢?声明了一个指针,但没有指向任何地方,然后就把它指向地址的值修改成5了.但是那个地址是什么?没人知道.这也许没事,但也可能有大问题.
指针和函数一起使用,有时会让指针指向非法的内存地址.比如:
func make_pointer(): &int {
x = 5;
return &x;
}
func main() {
&int i = make_pointer();
*i = 5; // 额...
}
x
是make_pointer
函数的局部变量,所以当make_pointer
返回后,x
就失效了.但我们返回了一个指向这个地址的指针,返回main
后,接着又使用了这个指针.这个的问题和刚才的那个类似.赋值一个非法内存地址是有问题的.
最后一个问题是别名(alias).2个指针指向同一个地址,它们就构成了彼此的别名.像这样:
func mutate(&int i, int j) {
*i = j;
}
func main() {
x = 5;
y = &x;
z = &x; // y 和 z 相互别名了
run_in_new_thread(mutate, y, 1);
run_in_new_thread(mutate, z, 100);
// x的值是多少?
}
上面的例子中,run_in_new_thread
新建了一个线程,执行参数中的函数.现在有2个线程,都会修改x
的别名,而且不知道哪个会先执行,所以,x
最后的值是不确定的.更糟的是,万一它们中的某个将该别名指向的地址失效了怎么办?此时,就像上面几个例子一样,在非法地址上进行修改.
结语
上面是从通用概念上的指针概述.如我们所讲,Rust有不止一种的指针类型,同时也能避免上述所有问题.这意味着Rust的指针比其他语言更复杂些,但能避免简单指针带来的诸多问题,也是比较值当的.
引用 reference
Rust中最基本的指针类型是引用(reference).Rust中的引用是类似下面的:
let x = 5is;
let y = &x;
println!("{}", *y);
println!("{:p}", y);
println!("{}", y);
此时,y
是指向x
的一个引用.第一个println!
使用解引用操作输出y
指向的值.第二个使用指针格式,输出y
指向的地址.第三个也会输出y
指向的值,因为println!
会自动的解引用.
下面的函数参数是引用:
fn succ(x: &isize) -> isize {
*x + 1
}
你也可以通过&
操作符创建一个引用,所以我们能通过下面2种方式调用该函数:
fn succ(x: &isize) -> isize {
*x + 1
}
fn main() {
let x = 5is;
let y = &x;
println!("{}", succ(y));
println!("{}", succ(&x));
}
2行都会输出6.
当然,如果是真实的代码的话,没必要传引用,直接这样:
fn succ(x: isize) -> isize {
x + 1
}
引用默认是不可变(immutable):
let x = 5is;
let y = &x;
*y = 5; // error: cannot assign to immutable dereference
添加mut
使其可变,但只有该引用指向也可变才行.比如下面:
let mut x = 5is;
let y = &mut x;
但下面这个就不行了:
let x = 5is;
let y = &mut x; // error: cannot borrow immutable local variable `x` as mutable
不可变指针允许别名:
let x = 5is;
let y = &x;
let z = &x;
但可变指针则不行:
let mut x = 5is;
let y = &mut x;
let z = &mut x; // error: cannot borrow `x` as mutable more than once at a time
除去绝对安全之外,引用的运行时表示和C语言中的指针完全相同.完全没有任何额外开销.编译器在编译时就完成了所有的安全检查.其理论支持称为区域指针(region pointer).区域指针就是后来我们说的生命期(lifetime).
下面是一个简单的解释:这个能编译过么?
fn main() {
println!("{}", x);
let x = 5;
}
显然不行.因为只有在声明位置之后,出当前作用域之前,x
才是有效的.此处,当前作用域的结束是main
函数的结束.所以上面的代码编译不通过.我们把这个区间称为生命期.看下更复杂的例子:
fn main() {
let x = &mut 5is;
if *x < 10 {
let y = &x;
println!("Oh no: {}", y);
return;
}
*x -= 1;
println!("Oh no: {}", x);
}
这里,我们在if
中借用(borrow)了x的指针(译者: 就是那个y
).但编译器知道,在x
被修改之前,那个指针就已经出了作用域了,所以不会有任何问题的.但下面这个就不行了:
fn main() {
let x = &mut 5is;
if *x < 10 {
let y = &x;
*x -= 1;
println!("Oh no: {}", y);
return;
}
*x -= 1;
println!("Oh no: {}", x);
}
会给出以下错误:
error: cannot assign to `*x` because it is borrowed
这种分析对人来说是非常复杂的,对计算器来说更复杂.我们会有一整章来详细解释这个话题,如果你想看细节的话,就去看看那里.
最佳实践
通常来讲,比起堆对象,要优先使用栈对象.任何时候都要优先使用指向栈内存的引用.因此,引用是你应该使用的默认的指针类型,除非你有一个很好的理由不用它.其他类型的指针会在它们的章节中提到具体的最佳实践方式.
想用指针,但不要得到所有权(ownership)时,就是用引用.引用仅仅是借用所有权,当你不要得到时,这样更安全.也就是说,要优先这样:
fn succ(x: &isize) -> isize {
*x + 1
}
而不是:
fn succ(x: Box<isize>) -> isize {
*x + 1
}
(译者: Box是在堆上的一种指针,后面会讲)
推广开来,引用允许你接受其他类型的指针,所以,你就不用给每种指针类型单独写一个函数了.也就是说,要优先这样:
fn succ(x: &isize) -> isize {
*x + 1
}
而不是:
use std::rc::Rc;
fn box_succ(x: Box<isize>) -> isize {
*x + 1
}
fn rc_succ(x: Rc<isize>) -> isize {
*x + 1
}
(译者: Rc也是一种指针,后面会讲)
只使用引用的话,你需要对调用的方式稍微修改下:
use std::rc::Rc;
fn succ(x: &isize) -> isize {
*x + 1
}
let ref_x = &5is;
let box_x = Box::new(5is);
let rc_x = Rc::new(5is);
succ(ref_x);
succ(&*box_x);
succ(&*rc_x);
先是通过”*“解引用指针,然后在”&”取其引用.
Box
Box<T>
是Rust的”boxed”指针类型.Box提供了最基本的堆上的指针功能.可以如下创建一个box:
let x = Box::new(5is);
Box是堆上分配的,当出了作用域之后,会被Rust自动回收:
{
let x = Box::new(5is);
// stuff happens
}
// x被析构了,内存被释放
但box没有使用引用计数(reference count)或垃圾回收(garbage collection).Box类型被称为”affine”类型.Rust编译器在编译时就已经确定了box对象何时被声明,何时出作用域,因此会在合适的位置插入正确的调用.而且,box是一种称为区域(region)的特殊”affine”类型.你可以在这篇论文中了解更多信息.
但为了使用box,你不必完全掌握”affine”类型或区域类型的所有内容.你可以这样理解,下面的Rust代码:
{
let x = Box::new(5is);
// stuff happens
}
和下面的C代码:
{
int *x;
x = (int *)malloc(sizeof (int));
*x = 5;
// stuff happens
free(x);
}
当然,这只是粗略的类似.这里省略了析构函数之类的其他东西.但基本概念上是对的:你获得了malloc/free
的语义,而且还有一些改进:
- 不可能分配错误大小的内存,因为Rust会自动从类型中获取大小
- 不会忘记调用
free
,因为Rust自动帮你做了 - Rust保证在正确的时间调用
free
,也就是真正没有用的时候(译者: 就是出了作用域).free
后就不可能再使用了 - Rust保证整个指针没有其他可变别名,修改一个非法指针也就不可能了(译者: 这个是说Box里面的内容不会有别名了,即上面的那个
5is
,但Box类型和其他类型一样,是可以有的.但此处感觉貌似不对.比如可以let y = &mut *x;
,就获得了这个的可变别名)
可以参看这章学习更多生命期有关的东西.
box和引用一起使用是非常普遍的.比如:
fn add_one(x: &isize) -> isize {
*x + 1
}
fn main() {
let x = Box::new(5is);
println!("{}", add_one(&*x));
}
这里,Rust知道x
被add_one
借用了,但由于add_one
只是读取其值,所以可以编译通过.
可以多次借用x
,只要不是同时(译者: 意思是非多线程):
fn add_one(x: &i32) -> i32 {
*x + 1
}
fn main() {
let x = Box::new(5);
println!("{}", add_one(&*x));
println!("{}", add_one(&*x));
println!("{}", add_one(&*x));
}
或者是可变借用.下面的会编译错误:
fn add_one(x: &mut i32) -> i32 {
*x + 1
}
fn main() {
let x = Box::new(5);
println!("{}", add_one(&*x)); // error: cannot borrow immutable dereference of `&`-pointer as mutable
}
注意,我们改变了add_one
的签名,其参数是一个可变引用.
最佳实践
Box适用于2个情景:循环数据结构,或者不常见的返回值.
循环数据结构
有时,你需要你个循环数据结构(recursive data structure).最简单的是构造列表(cons list):
#[derive(Show)]
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
fn main() {
let list: List<i32> = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("{:?}", list);
}
这个会打印:
Cons(1i32, Box(Cons(2i32, Box(Cons(3i32, Box(Nil))))))
Cons
中的另一个List
结构的引用,必须是box
类型,因为我们不知道列表的长度.因为不知道长度,所以也就不知道其总的大小,所以,这个结构必须在堆上分配.
返回值
这个完全值得单独拿一章来说.不过简单来讲,一般情况下,最好别返回指针,就算有时候你会在C或C++中做的那样.
可以参看下面的返回指针.
Rc和Arc
待续
最佳实践
待续
原始指针
待续
最佳实践
待续
返回指针
在带有指针的语言里,有的函数有时会返回指针而不是其值,来避免较大数据结构的复制.
struct BigStruct {
one: i32,
two: i32,
// etc
one_hundred: i32,
}
fn foo(x: Box<BigStruct>) -> Box<BigStruct> {
return Box::new(*x);
}
fn main() {
let x = Box::new(BigStruct {
one: 1,
two: 2,
// etc
one_hundred: 100,
});
let y = foo(x);
}
这里的关键是,通过返回box,就可以仅仅复制一个指针,而不是组成BigStruct
的100个i32
值.
但这个不是Rust惯用模式.相反,使用如下:
struct BigStruct {
one: i32,
two: i32,
// etc
one_hundred: i32,
}
fn foo(x: Box<BigStruct>) -> BigStruct {
return *x;
}
fn main() {
let x = Box::new(BigStruct {
one: 1,
two: 2,
// etc
one_hundred: 100,
});
let y = Box::new(foo(x));
}
这样,就兼具灵活性和性能了.
你也许会觉得这样性能很差:返回一个值,然后立即box起来?这个岂不是最差的模式了么?Rust比这个聪明多了.最后的代码里根本没有复制.main
为box
分配足够的大小,把指向这片内存的指针作为x
传给foo
,然后foo
把返回值直接写到了Box<T>
.
有句话值得不断的重复:指针不是用来为返回值优化的.要允许调用方选择如何使用你的返回结果.
创建自定义指针
待续
最佳实践
待续
模式(pattern)和引用(ref)
当你试图匹配存储在指针中的值时,直接匹配也许不是最好的方式.看看下面是如何使用的:
fn possibly_print(x: &Option<String>) {
match *x {
// BAD: 不能把&移除掉
Some(s) => println!("{}", s),
// GOOD: 获取Option中内存的引用即可
Some(ref s) => println!("{}", *s),
None => {}
}
}
ref s
意思是s
的类型是&String
,而不是String
.
当类型存在析构函数时,这一点很重要,你不需要移动(mov)它,仅仅需要一个它的引用即可.
作弊单
下面是Rust指针的简单总结(译者: 各项为类型,名称和用法总结):
&T
(引用): 多个,可读&mut T
(可变引用): 一个,读写Box<T>
(Box类型): 堆上分配,单一所有者,可读写Rc<T>
(Ref Counted指针): 堆上分配,多个可读Arc<T>
(Atomic Ref Counted指针): 同上,但线程安全*const T
(原始指针): 非安全,可读*mut T
(可变原始指针): 非安全,可读写
相关资源
- Box类型的API文档
- 所有权教程
- 有关区域的论文,启发了Rust的生命期系统