改进rust代码的35种具体方法-类型(十五)-借用检查器

上一篇文章


“摧毁一个事物的力量,就是对它的绝对控制。” ——弗兰克·赫伯特

Rust 中的值有一个所有者,但该所有者可以将值借出到代码中的其他位置。这种借用 机制涉及创建和使用参考文献,遵守监管规则借用检查器

在幕后,这使用了相同类型的指针值,这些值在C或C++ 代码,但带有规则和限制,以确保避免 C/C++ 的罪恶。快速比较一下:

  • 与 C/C++ 指针一样,Rust 引用是使用 & 符号创建的:&value
  • 就像 C++ 引用一样,Rust 引用永远不可能nullptr
  • 与 C/C++ 指针或引用一样,Rust 引用可以在创建后进行修改以引用不同的内容。
  • 与 C++ 不同,从值生成引用总是涉及显式 ( &) 转换 - 如果您看到类似 的代码f(value),您就知道f正在接收该值的所有权1 .
  • 与 C/C++ 不同,新创建的引用的可变性始终是显式的 ( & mut);如果您看到类似的代码 f(&value),您就知道它value不会被修改(即const在 C/C++ 术语中)。仅表达2喜欢f(&mut value)有改变内容的可能性value

C/C++ 指针和 Rust 引用之间最重要的区别由术语表示借用:您可以获取某个项目的引用(指针),但您必须将其归还。特别是,您必须 在基础项的生命周期到期之前将其归还,如编译器所跟踪并在Item 14中探讨的那样。

这些对引用使用的限制是 Rust 内存安全保证的核心,但它们确实意味着您必须接受借用规则的认知成本 - 接受它将改变您设计软件的方式,特别是其数据结构。

访问控制

可以通过三种不同的方式访问 Rust 项:通过项的所有者item)、通过引用 ( &item) 或通过可变引用&mut item)。

访问该项目的每种不同方式都对该项目具有不同的权力。把东西放进去 CRUD术语:

  • 项目的所有者可以创建它、读取它、更新它并删除它 (CRUD)。
  • 可变引用可用于从基础项读取并更新(_RU_)。
  • (普通)引用只能用于从底层项 (_R__) 读取

这些数据访问规则有一个重要的特定于 Rust 的扩展:只有项目的所有者才能移动该项目。如果您将移动视为创建(在新位置)和删除项目内存(在旧位置)的某种组合,那么这有道理

对于具有对某个项目的可变引用的代码,这可能会导致一些奇怪的情况。例如,可以覆盖 Option

fn overwrite(item: &mut Option<Item>, val: Item) {
    *item = Some(val);
}

但是返回先前值的修改违反了移动限制:

    pub fn replace(item: &mut Option<Item>, val: Item) -> Option<Item> {
        let previous = *item; // move out
        *item = Some(val); // replace
        previous
    }
error[E0507]: cannot move out of `*item` which is behind a mutable reference
  --> borrows/src/main.rs:27:24
   |
27 |         let previous = *item; // move out
   |                        ^^^^^ move occurs because `*item` has type `Option<Item>`, which does not implement the `Copy` trait
   |
help: consider borrowing the `Option`'s content
   |
27 |         let previous = *item.as_ref(); // move out
   |                             +++++++++
help: consider borrowing here
   |
27 |         let previous = &*item; // move out
   |                        ~~~~~~

从可变引用读取是有效的,写入可变引用也是有效的,因此同时执行这两项操作的能力是由std::mem::replace 标准库中的函数。这使用unsafe代码一次性执行交换:

    pub fn replace(item: &mut Option<Item>, val: Item) -> Option<Item> {
        std::mem::replace(item, Some(val)) // returns previous value
    }

为了Option特别是类型,这是一种非常常见的模式,其本身也有一个 replace方法Option

    pub fn replace(item: &mut Option<Item>, val: Item) -> Option<Item> {
        item.replace(val)
    }

借用规则

Rust 中借用引用的第一条规则是任何引用的范围必须小于它所引用的项的生命周期。然而,编译器比仅仅假设引用一直持续到被删除更聪明——非词法生命周期功能允许缩短引用生命周期,以便它们在最后一次使用时结束,而不是在封闭块。

借用参考的第二条规则是,除了项目的所有者之外,还可以有

  • 对该项目的任意数量的不可变引用,或者
  • 对该项目的单个可变引用

但不是两者兼而有之。

因此,采用多个不可变引用的方法可以提供对同一项目的引用:

    fn both_zero(left: &Item, right: &Item) -> bool {
        left.contents == 0 && right.contents == 0
    }

    let item = Item { contents: 0 };
    assert!(both_zero(&item, &item));

但采用可变引用的引用不能:

    fn zero_both(left: &mut Item, right: &mut Item) {
        left.contents = 0;
        right.contents = 0;
    }

    let mut item = Item { contents: 42 };
    zero_both(&mut item, &mut item);
error[E0499]: cannot borrow `item` as mutable more than once at a time
   --> borrows/src/main.rs:115:26
    |
115 |     zero_both(&mut item, &mut item);
    |     --------- ---------  ^^^^^^^^^ second mutable borrow occurs here
    |     |         |
    |     |         first mutable borrow occurs here
    |     first borrow later used by call

对于两者的混合也类似:

    fn copy_contents(left: &mut Item, right: &Item) {
        left.contents = right.contents;
    }

    let mut item = Item { contents: 42 };
    copy_contents(&mut item, &item);
error[E0502]: cannot borrow `item` as immutable because it is also borrowed as mutable
   --> borrows/src/main.rs:140:30
    |
140 |     copy_contents(&mut item, &item);
    |     ------------- ---------  ^^^^^ immutable borrow occurs here
    |     |             |
    |     |             mutable borrow occurs here
    |     mutable borrow later used by call

借用规则允许编译器做出更好的决策 别名:跟踪两个不同的指针何时可能引用或不引用内存中的同一底层项目。如果编译器可以确定(如 Rust 中那样)不可变引用集合所指向的内存位置不能通过别名可变引用进行更改,那么它可以生成如下代码:

  • 更好的优化:值可以(例如)缓存在寄存器中,同时知道底层内存内容不会改变,这是安全的
  • 更安全:线程之间对内存的不同步访问引起的数据竞争是不可能的。

所有权管理

围绕引用存在的规则的一个重要后果是,它们还会影响项目所有者可以执行的操作。为了帮助理解这一点,请考虑涉及所有者的操作,就好像他们一路上使用引用一样。

例如,在存在引用时尝试通过其所有者更新该项目会失败,因为存在瞬态的第二个可变引用:

    let mut item = Item { contents: 42 };
    let r = &item;
    item.contents = 0;
    // ^^^ Changing the item is roughly equivalent to:
    //   (&mut item).contents = 0;
    println!("reference to item is {:?}", r);
error[E0506]: cannot assign to `item.contents` because it is borrowed
   --> borrows/src/main.rs:164:5
    |
163 |     let r = &item;
    |             ----- borrow of `item.contents` occurs here
164 |     item.contents = 0;
    |     ^^^^^^^^^^^^^^^^^ assignment to borrowed `item.contents` occurs here
...
167 |     println!("reference to item is {:?}", r);
    |                                           - borrow later used here

另一方面,由于允许多个不可变引用,因此当存在不可变引用时,所有者可以从该项目中读取内容:

    let item = Item { contents: 42 };
    let r = &item;
    let contents = item.contents;
    // ^^^ Reading from the item is roughly equivalent to:
    //   let contents = (&item).contents;
    println!("reference to item is {:?}", r);

但如果存在可变引用则不然:

    let mut item = Item { contents: 42 };
    let r = &mut item;
    let contents = item.contents; // i64 is `Copy`
    r.contents = 0;
error[E0503]: cannot use `item.contents` because it was mutably borrowed
   --> borrows/src/main.rs:194:20
    |
193 |     let r = &mut item;
    |             --------- borrow of `item` occurs here
194 |     let contents = item.contents; // i64 is `Copy`
    |                    ^^^^^^^^^^^^^ use of borrowed `item`
195 |     r.contents = 0;
    |     -------------- borrow later used here

最后,任何类型的引用的存在都会阻止该项目的所有者移动或移动删除该项目,正是因为这意味着该引用现在引用了无效的项目。

    let item = Item { contents: 42 };
    let r = &item;
    let new_item = item; // move
    println!("reference to item is {:?}", r);
error[E0505]: cannot move out of `item` because it is borrowed
   --> borrows/src/main.rs:151:20
    |
150 |     let r = &item;
    |             ----- borrow of `item` occurs here
151 |     let new_item = item; // move
    |                    ^^^^ move out of `item` occurs here
152 |     println!("reference to item is {:?}", r);
    |                                           - borrow later used here

战胜检查器

Rust 的新手(甚至更有经验的人!)常常会觉得他们正在花时间与借用检查器作斗争。什么样的事情可以帮助你赢得这些战斗?

本地代码重构

第一个策略是关注编译器的错误消息,因为 Rust 开发人员已经付出了大量的努力来使它们尽可能有用。

/// If `needle` is present in `haystack`, return a slice containing it.
pub fn find<'a, 'b>(a: &'a str, b: &'b str) -> Option<&'a str> {
    a.find(b).map(|i| &a[i..i + b.len()])
}

// ...

    let found = find(&format!("{} to search", "Text"), "ex");
    if let Some(text) = found {
        println!("Found '{}'!", text);
    }
error[E0716]: temporary value dropped while borrowed
   --> borrows/src/main.rs:312:23
    |
312 |     let found = find(&format!("{} to search", "Text"), "ex");
    |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^       - temporary value is freed at the end of this statement
    |                       |
    |                       creates a temporary which is freed while still in use
313 |     if let Some(text) = found {
    |                         ----- borrow later used here
    |
    = note: consider using a `let` binding to create a longer lived value
    = note: this error originates in the macro `format` (in Nightly builds, run with -Z macro-backtrace for more info)

错误消息的第一部分是重要的部分,因为它描述了编译器认为您违反了哪些借用规则以及原因。当您遇到足够多的此类错误时(您一定会遇到这种错误),您可以对借用检查器建立一种直觉,该直觉与上述规则中封装的更理论的版本相匹配。

错误消息的第二部分包括编译器关于如何解决问题的建议,在本例中很简单:

    let haystack = format!("{} to search", "Text");
    let found = find(&haystack, "ex");
    if let Some(text) = found {
        println!("Found '{}'!", text);
    }
    // `found` now references `haystack`, which out-lives it

这是两个简单代码调整之一的实例,可以帮助安抚借用检查器:

  • 生命周期扩展:将临时变量(其生命周期仅延伸到表达式的末尾)转换为新的命名局部变量(其生命周期延伸到块的末尾)let捆绑。
  • 生命周期减少:在引用的使用周围添加一个附加块{ ... },以便其生命周期在新块的末尾结束。

后者不太常见,因为存在非词法生命周期:编译器通常可以在块末尾的正式放置点之前发现不再使用引用。但是,如果您确实发现自己在类似的小代码块周围重复引入人工块,请考虑是否应该将该代码封装到其自己的方法中。

编译器建议的修复对于更简单的问题很有帮助,但是当您编写更复杂的代码时,您可能会发现这些建议不再有用,并且对损坏的借用规则的解释更难以理解。

    let x = Some(Rc::new(RefCell::new(Item { contents: 42 })));

    // Call function with signature: `check_item(item: Option<&Item>)`
    check_item(x.as_ref().map(|r| r.borrow().deref()));
error[E0515]: cannot return reference to temporary value
   --> borrows/src/main.rs:257:35
    |
257 |     check_item(x.as_ref().map(|r| r.borrow().deref()));
    |                                   ----------^^^^^^^^
    |                                   |
    |                                   returns a reference to data owned by the current function
    |                                   temporary value created here

在这种情况下,暂时引入一系列局部变量会很有帮助,每个局部变量对应复杂转换的每个步骤,并且每个变量都有显式类型注释。

    let x: Option<Rc<RefCell<Item>>> =
        Some(Rc::new(RefCell::new(Item { contents: 42 })));

    let x1: Option<&Rc<RefCell<Item>>> = x.as_ref();
    let x2: Option<std::cell::Ref<Item>> = x1.map(|r| r.borrow());
    let x3: Option<&Item> = x2.map(|r| r.deref());
    check_item(x3);
error[E0515]: cannot return reference to function parameter `r`
   --> borrows/src/main.rs:269:40
    |
269 |     let x3: Option<&Item> = x2.map(|r| r.deref());
    |                                        ^^^^^^^^^ returns a reference to data owned by the current function

这缩小了编译器抱怨的精确转换范围,从而允许重组代码:

    let x: Option<Rc<RefCell<Item>>> =
        Some(Rc::new(RefCell::new(Item { contents: 42 })));

    let x1: Option<&Rc<RefCell<Item>>> = x.as_ref();
    let x2: Option<std::cell::Ref<Item>> = x1.map(|r| r.borrow());
    match x2 {
        None => check_item(None),
        Some(r) => {
            let x3: &Item = r.deref();
            check_item(Some(x3));
        }
    }

一旦根本问题明确并得到解决,您就可以自由地将局部变量重新合并在一起,这样您就可以假装您一直都做对了:

    let x = Some(Rc::new(RefCell::new(Item { contents: 42 })));

    match x.as_ref().map(|r| r.borrow()) {
        None => check_item(None),
        Some(r) => check_item(Some(r.deref())),
    };

数据结构设计

有助于对抗借用检查器的下一个策略是在设计数据结构时考虑借用检查器。万能药是您的数据结构可以拥有它们使用的所有数据,避免任何引用的使用以及随之而来的传播生命周期注释在上一篇中描述。

然而,对于现实世界的数据结构来说,这并不总是可能的。任何时候,数据结构的内部连接形成一个比树模式(aRoot拥有多个Branches,每个 es 拥有多个s 等)更相互连接的图Leaf,那么简单的单一所有权是不可能的。

举一个简单的例子,想象一个简单的登记册,按照客人到达的顺序记录客人的详细信息。

#[derive(Clone)]
struct Guest {
    name: String,
    phone: PhoneNumber,
    address: String,
    // ... many other fields
}

#[derive(Default, Debug)]
struct GuestRegister(Vec<Guest>);

impl GuestRegister {
    fn register(&mut self, guest: Guest) {
        self.0.push(guest)
    }
    fn nth(&self, idx: usize) -> Option<&Guest> {
        if idx < self.0.len() {
            Some(&self.0[idx])
        } else {
            None
        }
    }
}

如果此代码需要能够按到达时间和按姓名字母顺序有效地查找客人,那么从根本上讲,涉及两种不同的数据结构,并且只有其中之一可以拥有该数据。

如果涉及的数据既小又不可变,那么只需复制一份就可以提供快速解决方案。

#[derive(Default, Debug)]
struct ClonedGuestRegister {
    by_arrival: Vec<Guest>,
    by_name: BTreeMap<String, Guest>,
}

impl ClonedGuestRegister {
    fn register(&mut self, guest: Guest) {
        self.by_arrival.push(guest.clone()); // requires `Guest` to be `Clone`
        self.by_name.insert(guest.name.clone(), guest);
    }
    fn named(&self, name: &str) -> Option<&Guest> {
        self.by_name.get(name)
    }
    fn nth(&self, idx: usize) -> Option<&Guest> {
        // snip
    }
}

如果数据可以修改,这种复制方法就很难应对——如果Guest 需要更新a的电话号码,您必须找到两个版本并确保它们保持同步。

另一种可能的方法是添加另一层间接层,将 视为Vec<Guest>所有者并使用该向量的索引进行名称查找。

这种方法可以很好地应对不断变化的电话号码 - (单一)Guest由 拥有Vec,并且始终会在幕后以这种方式到达:

    let new_number = PhoneNumber::new(123456);
    ledger.named_mut("Bob").unwrap().phone = new_number;
    assert_eq!(ledger.named("Bob").unwrap().phone, new_number);

然而,它不能很好地应对另一种修改:如果客人可以取消注册会发生什么:

    // Deregister the `Guest` at position `idx`, moving up all subsequent guests.
    fn deregister(&mut self, idx: usize) -> Result<(), Error> {
        if idx >= self.by_arrival.len() {
            return Err(Error::new("out of bounds"));
        }
        self.by_arrival.remove(idx);
        // Oops, forgot to update `by_name`.
        Ok(())
    }

现在可以Vec对 进行改组,by_name其中的索引实际上就像指针一样,并且我们重新引入了一个世界,其中这些“指针”可以指向任何内容(超出界限Vec)或可以指向不正确的数据。

    ledger.register(alice);
    ledger.register(bob);
    ledger.register(charlie);
    println!("Register starts as: {:?}", ledger);

    ledger.deregister(0).unwrap();
    println!("Register after deregister(0): {:?}", ledger);

    let also_alice = ledger.named("Alice");
    // Alice still has index 0, which is now Bob
    println!("Alice is {:?}", also_alice);

    let also_bob = ledger.named("Bob");
    // Bob still has index 1, which is now Charlie
    println!("Bob is {:?}", also_bob);

    let also_charlie = ledger.named("Charlie");
    // Charlie still has index 2, which is now beyond the Vec
    println!("Charlie is {:?}", also_charlie);
Register starts as: {
  by_arrival: [{n: 'Alice', ...}, {n: 'Bob', ...}, {n: 'Charlie', ...}]
  by_name: {"Alice": 0, "Bob": 1, "Charlie": 2}
}
Register after deregister(0): {
  by_arrival: [{n: 'Bob', ...}, {n: 'Charlie', ...}]
  by_name: {"Alice": 0, "Bob": 1, "Charlie": 2}
}
Alice is Some({n: 'Bob', ...})
Bob is Some({n: 'Charlie', ...})
Charlie is None

无论采用哪种方法,都需要修复代码以确保数据结构保持同步。然而,处理底层数据结构的更好方法是使用 Rust 的智能指针。转向Rc和 的组合RefCell避免了使用索引作为伪指针的无效问题:

#[derive(Default)]
struct RcGuestRegister {
    by_arrival: Vec<Rc<RefCell<Guest>>>,
    by_name: BTreeMap<String, Rc<RefCell<Guest>>>,
}

impl RcGuestRegister {
    fn register(&mut self, guest: Guest) {
        let name = guest.name.clone();
        let guest = Rc::new(RefCell::new(guest));
        self.by_arrival.push(guest.clone());
        self.by_name.insert(name, guest);
    }
    fn deregister(&mut self, idx: usize) -> Result<(), Error> {
        if idx >= self.by_arrival.len() {
            return Err(Error::new("out of bounds"));
        }
        self.by_arrival.remove(idx);
        // Oops, still forgot to update `by_name`.
        Ok(())
    }
    // snip
}
Register starts as: {
  by_arrival: [{n: 'Alice', ...}, {n: 'Bob', ...}, {n: 'Charlie', ...}]
  by_name: [("Alice", {n: 'Alice', ...}), ("Bob", {n: 'Bob', ...}), ("Charlie", {n: 'Charlie', ...})]
}
Register after deregister(0): {
  by_arrival: [{n: 'Bob', ...}, {n: 'Charlie', ...}]
  by_name: [("Alice", {n: 'Alice', ...}), ("Bob", {n: 'Bob', ...}), ("Charlie", {n: 'Charlie', ...})]
}
Alice is Some(RefCell { value: {n: 'Alice', ...} })
Bob is Some(RefCell { value: {n: 'Bob', ...} })
Charlie is Some(RefCell { value: {n: 'Charlie', ...} })

输出现在有效,但 Alice 的一个挥之不去的条目仍然存在,直到我们确保两个集合保持同步:

    fn deregister_fixed(&mut self, idx: usize) -> Result<(), Error> {
        if idx >= self.by_arrival.len() {
            return Err(Error::new("out of bounds"));
        }
        let guest: Rc<RefCell<Guest>> = self.by_arrival.remove(idx);
        self.by_name.remove(&guest.borrow().name);
        Ok(())
    }
Register after deregister(0): {
  by_arrival: [{n: 'Bob', ...}, {n: 'Charlie', ...}]
  by_name: [("Bob", {n: 'Bob', ...}), ("Charlie", {n: 'Charlie', ...})]
}
Alice is None
Bob is Some(RefCell { value: {n: 'Bob', ...} })
Charlie is Some(RefCell { value: {n: 'Charlie', ...} })

智能指针

上一节的最后一个变体是更通用方法的示例:将 Rust 的智能指针用于互连数据结构

熟悉引用和指针类型描述了最常见的Rust 标准库提供的智能指针类型。

  • Rc允许共享所有权,多个事物引用同一项目。经常与…结合使用
  • RefCell允许内部可变性,以便可以在不需要可变引用的情况下修改内部状态。这是以将借用检查从编译时转移到运行时为代价的。
  • Arc是相当于多线程Rc
  • Mutex(和RwLock) 允许多线程环境中的内部可变性,大致相当于 RefCell.
  • Cell允许Copy类型的内部可变性。

对于从 C++ 适应 Rust 的程序员和设计来说,最常用的工具是Rc<T>(及其线程安全表兄弟Arc<T>),通常与RefCell(或线程安全替代方案Mutex) 结合使用。共享指针的幼稚翻译(甚至std::shared_ptrs) 到Rc<RefCell<T>>实例通常会给出一些在 Rust 中工作的东西,而借用检查器不会有太多抱怨。然而,这种方法意味着您错过了 Rust 为您提供的一些保护;特别是,当另一个引用存在时,同一项目被可变地借用(通过borrow_mut())的情况会导致运行时panic!而不是编译时错误。

例如,打破树状数据结构中所有权单向流的一种模式是存在从一项返回到拥有它的事物的“所有者”指针:

// C++ code (with lackadaisical pointer use)
struct Tree {
  std::string id() const;

  std::string tree_id_;
  std::vector<Branch*> branches_; // `Tree` owns `Branch` objects
};

struct Branch {
  std::string id() const;  // hierarchical identifier for `Branch`

  std::string branch_id_;
  std::vector<Leaf*> leaves_; // `Branch` owns `Leaf` objects
  Tree* owner_; // back-reference to owning `Tree`
};

struct Leaf {
  std::string id() const;  // hierarchical identifier for `Leaf`

  std::string leaf_id_;
  Branch* owner_; // back-reference to owning `Branch`
};

std::string Branch::id() const {
  if (owner_ == nullptr) {
    return "<unowned>." + branch_id_;
  } else {
    return owner_->id()+ "." + branch_id_;
  }
}

在 Rust 中实现等效模式可以利用Rc<T>更具尝试性的合作伙伴, Weak<T>

struct Tree {
    tree_id: String,
    branches: Vec<Rc<RefCell<Branch>>>,
}

struct Branch {
    branch_id: String,
    leaves: Vec<Rc<RefCell<Leaf>>>,
    owner: Option<Weak<RefCell<Tree>>>,
}

struct Leaf {
    leaf_id: String,
    owner: Option<Weak<RefCell<Branch>>>,
}

Weak引用不会增加引用计数,因此必须显式检查底层项目是否已消失:

impl Branch {
    fn add_leaf(branch: Rc<RefCell<Branch>>, mut leaf: Leaf) {
        leaf.owner = Some(Rc::downgrade(&branch));
        branch.borrow_mut().leaves.push(Rc::new(RefCell::new(leaf)));
    }
    fn id(&self) -> String {
        match &self.owner {
            None => format!("<unowned>.{}", self.branch_id),
            Some(t) => {
                let tree = t.upgrade().expect("internal error: owner gone!");
                format!("{}.{}", tree.borrow().id(), self.branch_id)
            }
        }
    }
}

如果 Rust 的智能指针似乎没有涵盖您的数据结构所需的内容,那么总是有最后的后备写入unsafe使用原始(并且绝对不智能)指针的代码。然而,根据下一篇文章,这应该是最后的手段 - 其他人可能已经在安全接口内实现了您想要的语义,并且如果您搜索标准库并且crates.io你可能会找到它。

例如,假设您有一个函数,有时返回对其输入之一的引用,但有时需要返回一些新分配的数据。根据第一篇文章,enum对这两种可能性进行编码是在类型系统中表达这一点的自然方式,然后您可以实现熟悉引用和指针类型中描述的各种指针特征。但你不必这样做:标准库已经包含了 std::borrow::Cow类型熟悉引用和指针类型一旦您知道它存在,它就完全涵盖了这种情况。

自引用数据结构

一种特定的数据结构风格总是阻碍程序员从其他语言转向 Rust:尝试创建自引用数据结构,其中包含自有数据以及对该自有数据内的引用的混合。

struct SelfRef {
    text: String,
    // The slice of `text` that holds the title text.
    title: Option<&str>,
}

在语法级别,此代码将无法编译,因为它不符合熟悉引用和指针类型中描述的生命周期规则:引用需要生命周期注释,但我们不希望将该生命周期注释传播到包含的数据结构,因为其目的不是指任何外部的东西。

值得在更语义的层面上思考这种限制的原因。 Rust 中的数据结构可以移动:从堆栈到堆,从堆到堆栈,以及从一个地方到另一个地方。如果发生这种情况,“内部” title指针将不再有效,并且无法保持同步。

对于这种情况,一个简单的替代方法是使用之前探讨过的索引方法;的一系列偏移量text 不会因移动而失效,并且对于借用检查器来说是不可见的,因为它不涉及引用:

struct SelfRefIdx {
    text: String,
    // Indices into `text` where the title text is.
    title: Option<Range<usize>>,
}

然而,这种索引方法仅适用于简单的示例。当编译器处理时,会出现更一般版本的自引用问题async代码4 .粗略地说,编译器将待处理的代码块捆绑async到 lambda 中,并且该 lambda 的数据可以包括值和对这些值的引用。

这本质上是一种自引用数据结构,因此async支持是该项目的主要动机 Pin在标准库中输入。该指针类型将其值“固定”到位,强制该值保留在内存中的同一位置,从而确保内部自引用保持有效。

SoPin可作为自引用类型的可能性,但正确使用很棘手(正如其官方文档所明确的那样):

  • 内部引用字段需要使用原始指针,或近亲(例如NonNull)其中。
  • 被固定的类型不需要实现 Unpin 标记特征。几乎每种类型都会自动实现此特征,因此这通常涉及添加类型的(零大小)字段 PhantomPinnedstruct 定义5 .
  • 该项目仅在出现后才会被固定堆并通过 保存Pin;换句话说,只有类似内容的内容Pin<Box<MyType>>才会被固定。这意味着内部引用字段只能在此时安全地填充,但由于它们是原始指针,如果您在 调用之前Box::pin错误地设置它们,编译器将不会向您发出警告。

在可能的情况下,避免自引用数据结构或尝试找到为您封装困难的库包(例如ouroborous)。


1 :但是,如果的类型是,则它可能是该项目副本的所有权valueCopy;。

2:m!(value) :请注意,所有涉及此类的表达式都将被取消宏,因为它可以扩展到任意代码。

3Cow代表写时复制;仅当需要对其进行更改(写入)时才会创建基础数据的副本。

4:处理async代码超出了本书的范围;要了解更多关于自引用数据结构的需求,请参阅第 8 章Rustaceans 的Rust乔恩·格詹塞特.

5将来可能可以直接声明 impl !Unpin for MyType {}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值