rust学习笔记之基础:智能指针、并发、不安全的rust

智能指针

指针(pointer)是一个包含内存地址的变量。这个地址引用指向一些其他数据。Rust 中最常见的指针是引用,引用以 & 符号为标志并借用了它们所指向的值,除了引用数据外没有任何其他特殊功能,它们也没有任何额外开销,所以应用得最多。

智能指针(smart pointers)是一类数据结构,它们的表现类似指针,但是拥有额外的元数据和功能。Rust 标准库中不同的智能指针提供了多于引用的额外功能。在 Rust 中,普通引用和智能指针的一个额外的区别是引用是一类只借用数据的指针;相反,在大部分情况下,智能指针拥有它们指向的数据。StringVec<T> 都属于智能指针,它们拥有一些数据并允许你修改它们。它们也带有元数据(比如它们的容量)和额外的功能或保证(String 的数据总是有效的 UTF-8 编码)。智能指针通常使用结构体实现。智能指针区别于常规结构体的显著特性在于其实现了 Deref traitDrop traitDeref trait 允许智能指针结构体实例表现的像引用一样,这样就可以编写既用于引用、又用于智能指针的代码。Drop trait 允许我们自定义当智能指针离开作用域时运行的代码。

使用 Box 指向堆上的数据

最简单直接的智能指针是 box,其类型是 Box<T>。box 允许你将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。

除了数据被储存在堆上而不是栈上之外,box 没有性能损失。它们多用于如下场景:

  • 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
  • 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
  • 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
使用 Box 在堆上储存数据
fn num_sq<T: std::ops::Mul<Output = T> + Copy>(num: &mut Box<T>) {
    **num = **num * **num;
}

fn main() {
    let b = Box::new(5);
    *b=(*b)*(*b);
    println!("b = {}", b); // b = 25

    let mut num: Box<u32> = Box::new(3);
    num_sq(&mut num);
    assert_eq!(*num, 9);
}

这里定义了变量 b,其值是一个指向被分配在堆上的值 5 的 Box。正如任何拥有数据所有权的值那样,当像 b 这样的 box 在 main 的末尾离开作用域时,它将被释放。这个释放过程作用于 box 本身(位于栈上)和它所指向的数据(位于堆上)。

将一个单独的值存放在堆上并不是很有意义,所以像上面这样单独使用 box 并不常见。将像单个 i32 这样的值储存在栈上,也就是其默认存放的地方在大部分使用场景中更为合适。

Box 允许创建递归类型

Rust 需要在编译时知道类型占用多少空间。一种无法在编译时知道大小的类型是递归类型,其值的一部分可以是相同类型的另一个值。这种值的嵌套理论上可以无限的进行下去,所以 Rust 不知道递归类型需要多少空间。不过 box 有一个已知的大小,所以通过在循环类型定义中插入 box,就可以创建递归类型了。

cons list 的每一项都包含两个元素:当前项的值和下一项。其最后一项值包含一个叫做 Nil 的值且没有下一项。cons list 通过递归调用 cons 函数产生。代表递归的终止条件的规范名称是 Nil,它宣布列表的终止。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
}

Cons 成员将会需要一个 i32 的大小加上储存 box 指针数据的空间。Nil 成员不储存值,所以它比 Cons 成员需要更少的空间。现在我们知道了任何 List 值最多需要一个 i32 加上 box 指针数据的大小。通过使用 box ,打破了这无限递归的连锁,这样编译器就能够计算出储存 List 值需要的大小了。

通过 Deref trait 将智能指针当作常规引用处理

实现 Deref trait 允许我们重载解引用运算符 *(与乘法运算符或通配符相区别)。通过这种方式实现 Deref trait 的智能指针可以被当作常规引用来对待,可以编写操作引用的代码并用于智能指针。

fn main() {
    let x = 5;
    let y = &x; // 引用
    let z = Box::new(x); // 指向 x 值的 box 实例

    assert_eq!(5, x);
    assert_eq!(5, *y); // 解引用
    assert_eq!(5, *z); // 使用解引用运算符以 y 为引用时相同的方式追踪 box 的指针
}
自定义智能指针并实现 Deref trait
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}
函数和方法的隐式解引用强制转换

解引用强制转换是 Rust 在函数或方法传参上的一种便利。解引用强制转换只能工作在实现了 Deref trait 的类型上。解引用强制转换将一种类型 A 隐式转换为另外一种类型 B 的引用,因为 A 类型实现了 Deref trait,并且其关联类型是 B 类型。比如,解引用强制转换可以将 &String 转换为 &str,因为类型 String 实现了 Deref trait 并且其关联类型是 str。

当我们将特定类型的值的引用作为参数传递给函数或方法,但是被传递的值的引用与函数或方法中定义的参数类型不匹配时,会发生解引用强制转换。这时会有一系列的 deref 方法被调用,把我们提供的参数类型转换成函数或方法需要的参数类型。解引用强制转换的加入使得 Rust 开发者编写函数和方法调用时无需增加过多显式使用 &* 的引用和解引用。这个功能也使得我们可以编写更多同时作用于引用或智能指针的代码。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = Box::new(String::from("Rust"));
    hello(&m); // Hello, Rust!
}

这里使用 &m 调用 hello 函数,其为 Box<String> 值的引用。因为 Box<T> 上实现了 Deref trait,Rust 可以通过 deref 调用将 &Box<String> 变为 &String。标准库中提供了 String 上的 Deref 实现,其会返回字符串 slice。Rust 再次调用 deref 将 &String 变为 &str,这就符合 hello 函数的定义了。

当所涉及到的类型定义了 Deref trait,Rust 会分析这些类型并使用任意多次 Deref::deref 调用以获得匹配参数的类型。这些解析都发生在编译时,所以利用解引用强制转换并没有运行时损耗!

解引用强制转换如何与可变性交互

类似于使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。

Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制转换:

  • T: Deref<Target=U> 时从 &T&U
  • T: DerefMut<Target=U> 时从 &mut T&mut U
  • T: Deref<Target=U> 时从 &mut T&U

使用 Drop Trait 运行清理代码

对于智能指针模式来说第二个重要的 trait 是 Drop,其允许我们在值要离开作用域时执行一些代码。可以为任何类型提供 Drop trait 的实现,同时所指定的代码被用于释放类似于文件或网络连接的资源。 Drop trait 功能几乎总是用于实现智能指针。例如,Box<T> 自定义了 Drop 用来释放 box 所指向的堆空间。

在其他一些语言中,我们不得不记住在每次使用完智能指针实例后调用清理内存或资源的代码。如果忘记的话,运行代码的系统可能会因为负荷过重而崩溃。在 Rust 中,可以指定每当值离开作用域时被执行的代码,编译器会自动插入这些代码。于是我们就不需要在程序中到处编写在实例结束时清理这些变量的代码,而且还不会泄漏资源。指定在值离开作用域时应该执行的代码的方式是实现 Drop traitDrop trait 要求实现一个叫做 drop 的方法,它获取一个 self 的可变引用。注意无需显式调用 drop 方法。当实例离开作用域 Rust 会自动调用 drop,并调用我们指定的代码。变量以被创建时相反的顺序被丢弃。

通过 std::mem::drop 提早丢弃值

Rust 不允许显式调用 Drop traitdrop 方法,因为 Rust 会在值离开作用域时对值自动调用 drop,这会导致一个 double free 错误,因为 Rust 会尝试清理相同的值两次。

有时可能需要提早清理某个值:例如当使用智能指针管理锁时,可能希望强制运行 drop 方法来释放锁以便作用域中的其他代码可以获取锁。当我们希望在作用域结束之前就强制释放变量的话,我们应该使用的是由标准库提供的 std::mem::drop

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer { data: String::from("some data") };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

输出

CustomSmartPointer created.
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main.

Rc 引用计数智能指针

大部分情况下所有权是非常明确的:可以准确地知道哪个变量拥有某个值。然而,有些情况单个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向相同的节点,而这个节点从概念上讲为所有指向它的边所拥有。节点直到没有任何边指向它之前都不应该被清理。

为了启用多所有权,Rust 有一个叫做 Rc<T> 的类型。其名称为引用计数(reference counting)的缩写。引用计数意味着记录一个值引用的数量来知晓这个值是否仍在被使用。如果某个值有零个引用,就代表没有任何有效引用并可以被清理。

Rc<T> 用于当我们希望在堆上分配一些内存供程序的多个部分读取,而且无法在编译时确定程序的哪一部分会最后结束使用它的时候。如果确实知道哪部分是最后一个结束使用的话,就可以令其成为数据的所有者,正常的所有权规则就可以在编译时生效。注意 Rc<T> 只能用于单线程场景。

使用 Rc 共享数据
enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

输出

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Rc::clone 只会增加引用计数,不会对所有数据进行深拷贝。Drop trait 的实现当 Rc<T> 值离开作用域时自动减少引用计数。使用 Rc<T> 允许一个值有多个所有者,引用计数则确保只要任何所有者依然存在其值也保持有效。通过不可变引用,Rc<T> 允许在程序的多个部分之间只读地共享数据。

RefCell 和内部可变性模式

内部可变性是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在内部使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。

通过 RefCell 在运行时检查借用规则

对于引用和 Box<T>,借用规则的不可变性作用于编译时,如果违反这些规则,会得到一个编译错误。对于 RefCell<T>,这些不可变性作用于运行时,如果违反这些规则程序会 panic 并退出。在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是 Rust 的默认行为。相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。类似于 Rc<T>RefCell<T> 只能用于单线程场景。

如下为选择 Box<T>Rc<T>RefCell<T> 的理由:

  • Rc<T> 允许相同数据有多个所有者;Box<T>RefCell<T> 有单一所有者。
  • Box<T> 允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T> 允许在运行时执行不可变或可变借用检查。
  • 因为 RefCell<T> 允许在运行时执行可变借用检查,所以我们可以在即便 RefCell<T> 自身是不可变的情况下修改其内部的值。

在不可变值内部改变值就是内部可变性模式。

内部可变性:不可变值的可变借用

借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:

let x = 5;
let y = &mut x;

然而,特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。RefCell<T> 是一个获得内部可变性的方法。RefCell<T> 并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现 panic 而不是编译错误。

当创建不可变和可变引用时,我们分别使用 &&mut 语法。对于 RefCell<T> 来说,则是 borrow 和 borrow_mut 方法,这属于 RefCell<T> 安全 API 的一部分。borrow 方法返回 Ref<T> 类型的智能指针,borrow_mut 方法返回 RefMut<T> 类型的智能指针。这两个类型都实现了 Deref,所以可以当作常规引用对待。RefCell<T> 记录当前有多少个活动的 Ref<T>RefMut<T> 智能指针。每次调用 borrow,RefCell<T> 将活动的不可变借用计数加一。当 Ref<T> 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,RefCell<T> 在任何时候只允许有多个不可变借用或一个可变借用。如果我们尝试违反这些规则,相比引用时的编译时错误,RefCell<T> 的实现会在运行时出现 panic。

pub trait Messenger {
    fn send(&self, msg: &str);
}

struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
    fn new() -> MockMessenger {
        MockMessenger { sent_messages: RefCell::new(vec![]) }
    }
}

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        let mut one_borrow = self.sent_messages.borrow_mut();
        let mut two_borrow = self.sent_messages.borrow_mut();

        one_borrow.push(String::from(message));
        two_borrow.push(String::from(message));
    }
}

在运行时捕获借用错误而不是编译时意味着将会在开发过程的后期才会发现错误,甚至有可能发布到生产环境才发现;还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用 RefCell 使得在只允许不可变值的上下文中编写修改自身成为可能。虽然有取舍,但是我们可以选择使用 RefCell<T> 来获得比常规引用所能提供的更多的功能。

结合 Rc 和 RefCell 来拥有多个可变数据所有者

RefCell<T> 的一个常见用法是与 Rc<T> 结合。 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T>Rc<T> 的话,就可以得到有多个所有者并且可以修改的值了!

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));
    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;
    println!("a after = {:?}", a); // a after = Cons(RefCell { value: 15 }, Nil)
    println!("b after = {:?}", b); // b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
    println!("c after = {:?}", c); // c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}

这里创建了一个 Rc<RefCell<i32>> 实例并储存在变量 value 中以便之后直接访问。接着在 a 中用包含 value 的 Cons 成员创建了一个 List。需要克隆 value 以便 a 和 value 都能拥有其内部值 5 的所有权,而不是将所有权从 value 移动到 a 或者让 a 借用 value。我们将列表 a 封装进了 Rc<T> 这样当创建列表 b 和 c 时,他们都可以引用 a。一旦创建了列表 a、b 和 c,我们将 value 的值加 10。为此对 value 调用了 borrow_mut,这里自动解引用功能来解引用 Rc<T> 以获取其内部的 RefCell<T> 值。borrow_mut 方法返回 RefMut<T> 智能指针,可以对其使用解引用运算符并修改其内部值。

通过使用 RefCell<T>,我们可以拥有一个表面上不可变的 List,不过可以使用 RefCell<T> 中提供内部可变性的方法来在需要时修改数据。标准库中也有其他提供内部可变性的类型,比如 Cell<T>,它类似 RefCell<T> 但有一点除外:它并非提供内部值的引用,而是把值拷贝进和拷贝出 Cell<T>。还有 Mutex<T>,其提供线程间安全的内部可变性。

引用循环与内存泄漏

Rust 的内存安全性保证使其难以意外地制造永远也不会被清理的内存(被称为内存泄漏(memory leak)),但并不是不可能。与在编译时拒绝数据竞争不同, Rust 并不保证完全地避免内存泄漏,这意味着内存泄漏在 Rust 被认为是内存安全的。这一点可以通过 Rc<T>RefCell<T> 看出:创建引用循环的可能性是存在的。这会造成内存泄漏,因为每一项的引用计数永远也到不了 0,其值也永远不会被丢弃。

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
    println!("a initial rc count = {}", Rc::strong_count(&a)); // a initial rc count = 1
    println!("a next item = {:?}", a.tail()); //a next item = Some(RefCell { value: Nil })

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    println!("a rc count after b creation = {}", Rc::strong_count(&a)); // a rc count after b creation = 2
    println!("b initial rc count = {}", Rc::strong_count(&b)); // b initial rc count = 1
    println!("b next item = {:?}", b.tail()); // b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }
    println!("b rc count after changing a = {}", Rc::strong_count(&b)); // b rc count after changing a = 2
    println!("a rc count after changing a = {}", Rc::strong_count(&a)); // a rc count after changing a = 2

    // Uncomment the next line to see that we have a cycle; it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

在 main 的结尾,Rust 首先丢弃变量 b,这会使 b 中 Rc<List> 实例的引用计数减 1。然而,因为 a 仍然引用 b 中的 Rc<List>Rc<List> 的引用计数是 1 而不是 0,所以 b 中的 Rc<List> 在堆上的内存不会被丢弃。接下来 Rust 会丢弃 a,同理这会将 a 中 Rc<List> 实例的引用计数从 2 减为 1。这个实例的内存也不能被丢弃,因为其他的 Rc<List> 实例仍在引用它。这些列表的内存将永远保持未被回收的状态。如果取消最后 println! 的注释并运行程序,Rust 会尝试打印出 a 指向 b 指向 a 这样的循环直到栈溢出。这个特定的例子中,创建了引用循环之后程序立刻就结束了。这个循环的结果并不可怕。如果在更为复杂的程序中并在循环里分配了很多内存并占有很长时间,这个程序会使用多于它所需要的内存,并有可能压垮系统并造成没有内存可供使用。

创建引用循环并不容易,但也不是不可能。如果你有包含 Rc<T>RefCell<T> 值或类似的嵌套结合了内部可变性和引用计数的类型,请务必小心确保你没有形成一个引用循环;你无法指望 Rust 帮你捕获它们。另一个解决方案是重新组织数据结构,使得一部分引用拥有所有权而另一部分没有。换句话说,循环将由一些拥有所有权的关系和一些无所有权的关系组成,只有所有权关系才能影响值是否可以被丢弃。

避免引用循环:将 Rc 变为 Weak

调用 Rc::clone 会增加 Rc<T> 实例的 strong_count,只在其 strong_count 为 0 时Rc<T> 实例才会被清理。也可以通过调用 Rc::downgrade 并传递 Rc<T> 实例的引用来创建其值的弱引用(weak reference)。调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针,会将 weak_count 加 1。Rc<T> 类型使用 weak_count 来记录其存在多少个 Weak<T> 引用,类似于 strong_count。其区别在于 weak_count 无需计数为 0 就能使 Rc<T> 实例被清理。强引用代表如何共享 Rc<T> 实例的所有权,但弱引用并不属于所有权关系。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。

因为 Weak<T> 引用的值可能已经被丢弃了,为了使用 Weak<T> 所指向的值,我们必须确保其值仍然有效。为此可以调用 Weak<T> 实例的 upgrade 方法,这会返回 Option<Rc<T>>。如果 Rc<T> 值还未被丢弃,则结果是 Some;如果 Rc<T> 已被丢弃,则结果是 None。因为 upgrade 返回一个 Option<T>,我们确信 Rust 会处理 Some 和 None 的情况,所以它不会返回非法指针。

创建树形数据结构

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });
    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); // leaf parent = None
    println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf)); // leaf strong = 1, weak = 0

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });
        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
        println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
        // leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

        println!("branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch)); // branch strong = 1, weak = 1
        println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf)); // leaf strong = 2, weak = 0
    }

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); // leaf parent = None
    println!("leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf)); // leaf strong = 1, weak = 0
}

我们希望 Node 能够拥有其子节点,同时也希望通过变量来共享所有权,以便可以直接访问树中的每一个 Node,为此 Vec<T> 的项的类型被定义为 Rc<Node>。我们还希望能修改其他节点的子节点,所以 children 中 Vec<Rc<Node>> 被放进了 RefCell<T>。这里克隆了 leaf 中的 Rc<Node> 并储存在了 branch 中,这意味着 leaf 中的 Node 现在有两个所有者:leaf 和 branch。

父节点应该拥有其子节点:如果父节点被丢弃了,其子节点也应该被丢弃。然而子节点不应该拥有其父节点:如果丢弃子节点,其父节点应该依然存在。所以 parent 使用 Weak<T> 类型而不是 Rc<T>,具体来说是 RefCell<Weak<Node>>。这样,一个节点就能够引用其父节点,但不拥有其父节点。

当内部作用域结束时,branch 离开作用域,Rc<Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏!所有这些管理计数和值的逻辑都内建于 Rc<T>Weak<T> 以及它们的 Drop trait 实现中。通过在 Node 定义中指定从子节点到父节点的关系为一个 Weak<T>引用,就能够拥有父节点和子节点之间的双向引用而不会造成引用循环和内存泄漏。

并发

安全且高效的处理并发编程是 Rust 的一个主要目标。并发编程(Concurrent programming)代表程序的不同部分相互独立的执行,而并行编程(parallel programming)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。

通过利用所有权和类型检查,在 Rust 中很多并发错误都是编译时错误,而非运行时错误。

使用线程同时运行代码

在大部分现代操作系统中,已执行程序的代码在一个进程(process)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为线程(threads)。将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:

  • 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
  • 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
  • 只会发生在特定情况且难以稳定重现和修复的 bug

Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。

编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模型有时被称为 1:1,一个 OS 线程对应一个语言线程。很多编程语言提供了自己特殊的线程实现,由编程语言提供的线程被称为绿色(green)线程,使用绿色线程的语言会在不同数量的 OS 线程的上下文中执行它们,绿色线程模式被称为 M:N 模型:M 个绿色线程对应 N 个 OS 线程,这里 M 和 N 不必相同。每一个模型都有其优势和取舍,对于 Rust 来说最重要的取舍是运行时支持。运行时代表二进制文件中包含的由语言自身提供的代码,这些代码根据语言的不同可大可小,任何非汇编语言都会有一定数量的运行时代码,更小的运行时拥有更少的功能不过其优势在于更小的二进制输出,这使其易于在更多上下文中与其他语言相结合。虽然很多语言觉得增加运行时来换取更多功能没有什么问题,但是 Rust 需要做到几乎没有运行时,同时为了保持高性能必须能够调用 C 语言,这点也是不能妥协的。绿色线程的 M:N 模型需要更大的语言运行时来管理这些线程。因此,Rust 标准库只提供了 1:1 线程模型实现。

使用 spawn 创建新线程

为了创建一个新线程,需要调用 thread::spawn 函数并传递一个闭包,并在其中包含希望在新线程运行的代码。

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }
}

注意这个函数编写的方式,当主线程结束时,新线程也会结束,而不管其是否执行完毕。thread::sleep 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程。在这里,主线程首先打印,即便新创建线程的打印语句位于程序的开头。

输出

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
使用 join 等待所有线程结束

可以通过将 thread::spawn 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。thread::spawn 的返回值类型是 JoinHandle。JoinHandle 是一个拥有所有权的值,当对其调用 join 方法时,它会等待其线程结束。诸如将 join 放置于何处这样的小细节,会影响线程是否同时运行。

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {} from the spawned thread!", i);
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {} from the main thread!", i);
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}

通过调用 handle 的 join 会阻塞当前线程直到 handle 所代表的线程结束。阻塞线程意味着阻止该线程执行工作或退出。

线程与 move 闭包

move 闭包经常与 thread::spawn 一起使用,它允许我们在一个线程中使用另一个线程的数据。在参数列表前使用 move 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {:?}", v);
    });

    handle.join().unwrap();
}

使用消息传递在线程间传送数据

一个日益流行的确保安全并发的方式是消息传递,这里线程通过发送包含数据的消息来相互沟通。Rust 中一个实现消息传递并发的主要工具是通道(channel)。通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为通道被关闭了。

use std::thread;
use std::sync::mpsc;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let val = String::from("hi");
        tx.send(val).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("Got: {}", received); // Got: hi
}

这里使用 mpsc::channel 函数创建一个新的通道;mpsc 是 多个生产者,单个消费者(multiple producer, single consumer)的缩写。简而言之,Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的发送(sending)端,但只能有一个消费这些值的接收(receiving)端。

mpsc::channel 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。通道的发送端有一个 send 方法用来发送需要放入通道的值。send 方法返回一个 Result<T, E> 类型,所以如果接收端已经被丢弃了发送操作会返回错误。通道的接收端有两个有用的方法:recv 和 try_recv。 recv 方法会阻塞主线程执行直到从通道中接收一个值。一旦接收到一个值,recv 会在一个 Result<T, E> 中返回它。当通道发送端关闭,recv 会返回一个错误表明不会再有新的值到来了。try_recv 不会阻塞,相反它立刻返回一个 Result<T, E>:Ok 值包含可用的信息,而 Err 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 try_recv 很有用:可以编写一个循环来频繁调用 try_recv,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。

通道与所有权转移

所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。我们在新建线程中的通道中发送完 val 值之后,再使用它会导致一个编译错误。

发送多个值
use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        // vals 迭代默认调用.into_iter()方法,获取项的所有权
        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }

        // println!("{:?}",vals); // 注释打开将编译报错
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

在主线程中,不再显式调用 recv 函数:而是将 rx 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。因为主线程中的 for 循环里并没有任何暂停或等待的代码,所以可以说主线程是在等待从新建线程中接收值。

将看到如下输出,每一行都会暂停一秒:

Got: hi
Got: from
Got: the
Got: thread
通过克隆发送者来创建多个生产者
use std::thread;
use std::sync::mpsc;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    thread::spawn(move || {
        let vals = vec![
            String::from("hi"),
            String::from("from"),
            String::from("the"),
            String::from("thread"),
        ];

        for val in vals {
            tx1.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    thread::spawn(move || {
        let vals = vec![
            String::from("more"),
            String::from("messages"),
            String::from("for"),
            String::from("you"),
        ];

        for val in vals {
            tx.send(val).unwrap();
            thread::sleep(Duration::from_secs(1));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

在创建新线程之前,我们对通道的发送端调用了 clone 方法。这样就会有两个线程,每个线程将通过不同的发送端向通道的接收端发送不同的消息。

共享状态并发

在某种程度上,Rust 的通道类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。智能指针使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。

互斥器一次只允许一个线程访问数据

互斥器(mutex)是 mutual exclusion 的缩写,也就是说任意时刻其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的锁来表明其希望访问数据。锁是作为互斥器一部分,记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统保护其数据。

互斥器使用规则:

  • 在使用数据之前尝试获取锁。
  • 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
Mutex
use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {:?}", m); // m = Mutex { data: 6, poisoned: false, .. }
}

像很多类型一样,我们使用关联函数 new 来创建一个 Mutex<T>。使用 lock 方法获取锁,以访问互斥器中的数据。这个调用会阻塞当前线程,直到我们拥有锁为止。如果另一个线程拥有锁,并且那个线程 panic 了,则 lock 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 unwrap 并在遇到这种情况时使线程 panic。

一旦获取了锁,就可以将返回值视为一个其内部数据的可变引用了。类型系统确保了我们在使用 m 中的值之前获取锁:Mutex<i32> 并不是一个 i32,所以必须获取锁才能使用这个 i32 值。Mutex<T> 是一个智能指针。更准确的说,lock 调用返回一个叫做 MutexGuard 的智能指针。这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域时自动释放锁,这正发生于内部作用域的结尾,锁的释放是自动发生的。

在线程间共享 Mutex
use std::sync::{Mutex, Arc};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();

            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Arc<T> 类似 Rc<T> ,却可以安全的用于并发环境。字母 “a” 代表原子性(atomic),所以这是一个原子引用计数类型。

RefCell/Rc 与 Mutex/Arc 的相似性

counter 是不可变的,不过可以获取其内部值的可变引用,这意味着 Mutex<T> 提供了内部可变性,就像 Cell 系列类型那样。正如使用 RefCell<T> 可以改变 Rc<T> 中的内容那样,同样的可以使用 Mutex<T> 来改变 Arc<T> 中的内容。

另一个值得注意的细节是 Rust 不能避免使用 Mutex<T> 的全部逻辑错误。使用 Rc<T> 就有造成引用循环的风险,这时两个 Rc<T> 值相互引用,造成内存泄漏。同理,Mutex<T> 也有造成死锁的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。

使用 Sync 和 Send trait 的可扩展并发

在 Rust 中,Send 和 Sync 是两种重要的标记 trait(标记 trait 是没有方法的 trait,仅用于标记类型具有某些特性),它们与线程安全密切相关。

‌Send trait 表示类型的所有权可以在线程间安全转移,如果一个类型实现了 Send,则该类型的值可以安全地从一个线程传递到另一个线程。大多数类型都自动实现了 Send,但包含裸指针的类型或某些特殊类型(如 Rc<T>)没有实现。

Sync trait 表示类型的引用可以安全地在多个线程间共享,如果一个类型 T 实现了 Sync,则 &T 是线程安全的。这意味着多个线程可以同时持有对该类型的不可变引用。基本类型(如 i32)、不可变类型和用适当同步原语保护的类型(如 Mutex<T>)都实现了 Sync。

通过 Send 允许在线程间转移所有权

Send 标记 trait 表明类型的所有权可以在线程间传递。几乎所有的 Rust 类型都是 Send 的,不过有一些例外,包括 Rc<T>:这是不能 Send 的,因为如果克隆了 Rc<T> 的值并尝试将克隆的所有权转移到另一个线程,这两个线程都可能同时更新引用计数。为此,Rc<T> 被实现为用于单线程场景,这时不需要为拥有线程安全的引用计数而付出性能代价。因此,Rust 类型系统和 trait bound 确保永远也不会意外的将不安全的 Rc<T> 在线程间发送。当尝试在这么做的时候,会得到错误 the trait Send is not implemented for Rc<Mutex<i32>>。而使用标记为 Send 的 Arc<T> 时,就没有问题了。

任何完全由 Send 的类型组成的类型也会自动被标记为 Send。几乎所有基本类型都是 Send 的,除了裸指针。

Sync 允许多线程访问

Sync 标记 trait 表明一个实现了 Sync 的类型可以安全的在多个线程中拥有其值的引用。换一种方式来说,对于任意类型 T,如果 &T 是 Send 的话 T 就是 Sync 的,这意味着其引用就可以安全的发送到另一个线程。类似于 Send 的情况,基本类型是 Sync 的,完全由 Sync 的类型组成的类型也是 Sync 的。

智能指针 Rc<T> 也不是 Sync 的,出于其不是 Send 相同的原因。RefCell<T>Cell<T> 系列类型不是 Sync 的。RefCell<T> 在运行时所进行的借用检查也不是线程安全的。Mutex<T> 是 Sync 的,可以被用来在多线程中共享访问。

手动实现 Send 和 Sync 是不安全的

通常并不需要手动实现 Send 和 Sync trait,因为由 Send 和 Sync 的类型组成的类型,自动就是 Send 和 Sync 的。因为他们是标记 trait,甚至都不需要实现任何方法。他们只是用来加强并发相关的不可变性的。

手动实现这些标记 trait 涉及到编写不安全的 Rust 代码;在创建新的由不是 Send 和 Sync 的部分构成的并发类型时需要多加小心,以确保维持其安全保证。

不安全 Rust

不安全 Rust(unsafe Rust)在编译时不会强制执行 Rust 的内存安全保证。它与常规 Rust 代码无异,但是会提供额外的超能力。不安全 Rust 之所以存在,是因为静态分析本质上是保守的。这必然意味着有时代码可能是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么”。这么做的缺点就是你只能靠自己了:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。

另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互、甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。

不安全的超能力

可以通过 unsafe 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力”。 这些超能力是:

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait
  • 访问 union 的字段

有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。再者,unsafe 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于开发者将会确保 unsafe 块中的代码以有效的方式访问内存。通过要求这五类操作必须位于标记为 unsafe 的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe 块内。保持 unsafe 块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。

为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意。标准库的一部分被实现为不安全代码之上的安全抽象。这个技术防止了 unsafe 泄露到所有你或者用户希望使用由 unsafe 代码实现的功能的地方,因为使用其安全抽象是安全的。

解引用裸指针

不安全 Rust 有两个被称为裸指针(raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T*mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变意味着指针解引用之后不能直接赋值。

裸指针与引用和智能指针的区别在于:

  • 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
  • 不保证指向有效的内存
  • 允许为空
  • 不能实现任何自动清理功能

通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。

let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

注意创建裸指针时没有引入 unsafe 关键字。可以在安全代码中创建裸指针,只是不能在 unsafe 块之外解引用裸指针和读取其指向的数据。创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。这里使用 as 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。

接下来会创建一个不能确定其有效性的裸指针,指向任意内存地址。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误。通常没有好的理由编写这样的代码,不过却是可行的:

let address = 0x012345usize;
let r = address as *const i32;

裸指针的一个主要的应用场景便是调用 C 代码接口,另一个场景是构建借用检查器无法理解的安全抽象。

调用不安全函数或方法

不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe。在此上下文中,关键字 unsafe 表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

必须在一个单独的 unsafe 块中调用 dangerous 函数。如果尝试不使用 unsafe 块调用 dangerous,则会得到一个错误。不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块

创建不安全代码的安全抽象

仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。

use std::slice;

fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();

    assert!(mid <= len);

    // 编译报错,Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。
    // (&mut slice[..mid],&mut slice[mid..])

    let ptr = slice.as_mut_ptr();  // 获取slice的可变裸指针
    // slice::from_raw_parts_mut 函数使用一个裸指针和一个长度来创建一个 slice
    unsafe {
        (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    }
}

slice::from_raw_parts_mut 函数是不安全的因为它获取一个裸指针,并必须确信这个指针是有效的。裸指针上的 add 方法也是不安全的,因为其必须确信此地址偏移量也是有效的指针。因此必须将 slice::from_raw_parts_mutadd 放入 unsafe 块中以便能调用它们。通过观察代码和增加 mid 必然小于等于 len 的断言,我们可以说 unsafe 块中所有的裸指针将是有效的 slice 中数据的指针。这是一个可以接受的 unsafe 的恰当用法。

注意无需将 split_at_mut 函数的结果标记为 unsafe,并可以在安全 Rust 中调用此函数。我们创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了 unsafe 代码。

使用 extern 函数调用外部代码

有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字 extern,有助于创建和使用外部函数接口 FFI 。外部函数接口是一个编程语言用以定义函数的方式,其允许不同编程语言调用这些函数。

下面例子展示了如何集成 C 标准库中的 abs 函数。extern 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是开发者的责任:

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

访问或修改可变静态变量

全局变量在 Rust 中被称为静态(static)变量。

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

静态变量类似于常量。静态变量的名字按惯例采用 SCREAMING_SNAKE_CASE。静态变量只能存储具有 "静态寿命" 的引用,这意味着 Rust 编译器可以计算出其寿命,我们不需要明确注释。访问一个不可变的静态变量是安全的。常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是不安全的。

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

任何读写 COUNTER 的代码都必须位于 unsafe 块中。这段代码可以编译并如期打印出 COUNTER: 3,因为这是单线程的。拥有多个线程访问 COUNTER 则可能导致数据竞争。拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。

实现不安全 trait

unsafe 的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变量时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe:

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

通过 unsafe impl,我们承诺将保证编译器所不能验证的不变量。

访问联合体中的字段

union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。

何时使用不安全代码

使用 unsafe 来进行这五个操作之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。

posted @ 2025-07-30 15:21  carol2014  阅读(51)  评论(0)    收藏  举报