Home

Rust 3.4 指针

17 Jan 2015 by LelouchHe

原文链接

Rust的指针是它一个独特而强大的功能,同样也是Rust新手最为困过的一个问题.它同样让来自其他支持指针语言的人,比如C++程序员们感到困惑.这里,将帮助你理解整个功能.

要谨慎对待Rust中的非引用指针:要用于特定的目的,而不仅仅是为了能编译通过.每个指针类型都解释了使用它们的正确方法.除非是这些情况,否则还是默认使用引用.

你也许对小抄很感兴趣,那个是一个关于指针类型,名称和目的的综述.

介绍

如果你对指针的概念不是很熟悉,我们先简单介绍下.指针在系统编程语言中,是一个基础的概念,所以理解它很重要.

指针基础

当你创建一个新的变量绑定时,你给存储在栈上的值绑定了一个名字.(如果你对堆和栈的概念不熟悉,请参考这里,因为后面都会假定你了解这些概念).比如:

let x = 5is;
let y = 8is;

(译者: 可能的栈结构.由于Mardown无法输入表格,暂时以列表形式.前面是地址,后面是值):

这里的内存地址是我们编的,仅仅是示例而已.不论怎样,重点是我们使用的变量名x对应的内存地址是0xd3e030,该地址中的值是5.当引用x时,就得到了对应的值.因此,x5.

现在引入指针.某些语言中,只有一种指针类型,但在Rust中,有很多指针类型.此处,我们使用Rust的引用(reference),这是最简单的一种指针.

let x = 5is;
let y = 8is;
let z = &y;

看到区别了么?指针的值,并不是真实值,而是内存中的一个地址.此处,就是y的内存地址.xy的类型是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; // 额...
}

xmake_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的语义,而且还有一些改进:

  1. 不可能分配错误大小的内存,因为Rust会自动从类型中获取大小
  2. 不会忘记调用free,因为Rust自动帮你做了
  3. Rust保证在正确的时间调用free,也就是真正没有用的时候(译者: 就是出了作用域).free后就不可能再使用了
  4. 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知道xadd_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比这个聪明多了.最后的代码里根本没有复制.mainbox分配足够的大小,把指向这片内存的指针作为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指针的简单总结(译者: 各项为类型,名称和用法总结):

相关资源