Skip to content

Latest commit

 

History

History
138 lines (101 loc) · 7.23 KB

unique.md

File metadata and controls

138 lines (101 loc) · 7.23 KB

阅读英文原版

独占指针

Rust 是一门系统编程语言,因此必定能够以原始方式访问内存。Rust 和 C++ 一样,通过指针进行这一操作。指针是一个 Rust 和 C++ 在语法和语义上差异巨大的语言特性。Rust 通过对指针进行类型检查保证内存的安全性。这是 Rust 相较其他语言的主要优势之一。虽然类型系统有点复杂,但用户可以受益于内存安全和底层硬件的性能。

我之前打算一篇文章把 Rust 的指针讲完,但又觉得这一主题太广了。因此,这篇文章只讲独占指针这一种,剩下的后面再讲。

首先是没有指针的例子:

fn foo() {
    let x = 75;

    // 对 `x` 进行操作
}

到达 foo 的结尾,x 离开作用域(Rust 这一点和 C++ 类似)。这意味着程序无法再访问变量,而变量所占的内存可以重用。

在 Rust 中,对于任意类型 T,可以写 Box<T> 以得到一个指向 T 的独占指针(owning pointer, unique pointer)。使用 Box:new(...) 在堆上分配内存,并用提供的值初始化此内存。此操作与 C++ 的 new 相似。例如:

fn foo() {
    let x = Box::new(75);
}

此处的 x 是一个指向堆的指针,对应的堆内存包含的值为 75x 的类型为 Box<i32>;我们也可以写成 let x: Box<i32> = Box::new(75);。这与 C++ 的 int* x = new int(75); 类似。与 C++ 不同的是,Rust 会为我们回收内存,因此无需调用 freedelete1。独占指针的表现和值类似,当变量离开作用域时销毁。上例中,foo 函数结束处,无法访问 xx 指向的内存可以重用。

与 C++ 相同,使用 * 解引用独占指针。例如:

fn foo() {
    let x = Box::new(75);
    println!("`x` points to {}", *x);
}

和 Rust 的原始类型一样,独占指针及其指向的数据默认是不可变的。与 C++ 不同的是,无法创造出指向不可变数据的可变指针(译者注:即 const T*),反过来(译者注:即 T* const)也不行。数据的可变性与指针一致。例如:

fn foo() {
    let x = Box::new(75);
    let y = Box::new(42);
    // x = y;         // 不允许,x 不可变
    // *x = 43;       // 不允许,*x 不可变
    let mut x = Box::new(75);
    x = y;         // 允许,x 可变
    *x = 43;       // 允许,*x 可变
}

(译者注:前面的 x 只是被覆盖了,但指向的内存没有被回收。)

独占指针可从函数返回,生存期得以延续。如果独占指针被返回,则内存不会被回收,换言之,Rust 中不会出现悬垂指针。内存不会泄漏。然而,指针最终还是会离开作用域,使得内存被回收。例如:

fn foo() -> Box<i32> {
    let x = Box::new(75);
    x
}

fn bar() {
    let y = foo();
    // 使用 y
}

上例中,foo 初始化内存,并返回给 barxfoo 中返回并存至 y,因此没有被销毁。bar 结束时,y 离开作用域,内存被回收。

独占指针是线性(linear)的,因为一块内存只能同时由一个独占指针指向。这一机制通过移动语义实现。当指针指向值时,先前的任何指针都无法访问。例如:

fn foo() {
    let x = Box::new(75);
    let y = x;
    // x 无法访问
    // let z = *x; // 错误
}

类似的是,如果独占指针传给了另一个函数,或存储至字段(field)中,此独占指针无法访问:

fn bar(y: Box<isize>) {
}

fn foo() {
    let x = Box::new(75);
    bar(x);
    // x 无法访问
    // let z = *x; // 错误
}

Rust 的独占指针和 C++ 的 std::unique_ptr 类似。和 C++ 一样,在 Rust 中,同一个值只能由一个独占指针指向,当指针离开作用域时,指向的值就被销毁。Rust 的检查大多静态进行,而非运行时进行。在 C++ 中访问值被移走的独占指针会导致运行时错误(因为指针为空)。Rust 中,此操作会产生编译错误,因此运行时不会出错。(译者注:关于此处的编译错误,请见 1 中关于移动的说明。)

之后我们会看到,Rust 中可以创建其他类型的指向独占指针的值的指针类型。这一点和 C++ 类似。然而 C++ 中,这么做允许持有指向已回收内存的指针,导致运行时错误。Rust 中不会出现(后面讲 Rust 其他的指针类型时会提到)。

从上例可见,要使用独占指针指向的值,必须进行解引用。然而,方法调用会自动解引用,因此调用方法无需 -> 运算符或使用 *。这使得 Rust 的指针既有些像 C++ 的指针,又有些像引用。例如:

fn bar(x: Box<Foo>, y: Box<Box<Box<Box<Foo>>>>) {
    x.foo();
    y.foo();
}

假设类型 Foo 带有方法 foo(),则上面两个表达式都是正确的。

用已有值调用 Box::new() 不会引用已有值,而是复制已有值,因此:

fn foo() {
    let x = 3;
    let mut y = Box::new(x);
    *y = 45;
    println!("x is still {}", x);
}

总的来说,Rust 中带有移动语义,而非复制语义(上面独占指针的例子可见)。原始类型带有复制语义,因此上例中的值 3 会被复制,而更复杂的类型会被移动。之后会详细讲。

然而,有时在编程时,我们需要对值进行多次引用。Rust 中用借用指针实现这一用途。下篇文章讲这个。

Footnotes

  1. C++11 中引入的 std::unique_ptr<T> 一定程度上和 Rust 的 Box<T> 类似,不过二者有着明显的不同。

    相同点:

    • 由 C++11 的 std::unique_ptr<T> 和 Rust 的 Box<T> 指向的内存会在自身离开作用域时自动回收。
    • C++11 的 std::unique_ptr<T> 和 Rust 的 Box<T> 都只有移动语义。

    不同点:

    1. C++11 允许 std::unique_ptr<T> 从已有的指针构造,从而允许多个独占指针指向同一块内存。Box<T> 不允许此行为。
    2. C++11 中,对移动至另一变量或函数的 std::unique_ptr<T> 进行解引用导致未定义行为。在 Rust 中,类似行为无法通过编译。(译者注:Rust 的移动语义和 C++ 的移动语义不太一样。Rust 中,对象被移走就不能再用,尝试使用会出现编译错误。C++ 中,对象被移走后仍然可以使用,例如,std::unique_ptr<T> 被移走后,自身存储的指针为空指针;如果移动操作实现不当,或用户使用不当,就可能会出现运行错误。)
    3. 可变性或不可变性无法“穿透”std::unique_ptr<T>:解引用 const std::unique_ptr<T> 返回的仍然是底层数据的可变(非 const)引用。Rust 中,不可变的 Box<T> 不允许修改指向的数据。(译者注:C++ 的标准库扩展 v2 中提供了 std::experimental::propagate_const。此设施实现了“穿透”行为,适合用 std::unique_ptr<T> 实现 pImpl 编程技巧的场合。)

    Rust 中的 let x = Box::new(75) 可解释为 C++11 中的 const auto x = std::unique_ptr<const int>{new int{75}}; 以及 C++14 中的 const auto x = std::make_unique<const int>(75); 2