贷款的代数(Rust语言实现)
The Algebra of Loans in Rust

原始链接: https://nadrieril.github.io/blog/2025/12/21/the-algebra-of-loans-in-rust.html

Rust 的借用检查器正在发展,超越了简单的共享/可变借用,引入了新的引用类型来表达更细微的所有权和初始化状态。这些提案旨在提供更大的灵活性,同时保持 Rust 的安全保证。 主要新增内容包括:**`&own`**(表示所有权并允许移除值)、**`&uninit`**(表示已分配但未初始化的内存位置,仅允许写入)和 **`&pin`**(强制值不能在被丢弃之前被移动或释放——对于安全的 pinning 至关重要,例如在异步 Rust 中)。 表格详细说明了这些引用之间的交互方式,定义了允许哪些重新借用和操作。例如,`&own` 引用可以被重新借用为 `&mut`,但不能被重新借用为 `&pin`。 在借用过期后,仍然可能存在限制——特别是对于 pinning,即使 `&pin` 引用消失后仍然存在。 这些变化不仅仅是理论上的;它们能够实现新的 API,例如 `Vec::pop_own()`(返回最后一个元素的所有权)和使用 `&uninit` 简化初始化模式。 虽然这些想法仍在推测中,但它们正在获得关注,并有望提供更具表现力和更强大的 Rust 借用系统。

一个 Hacker News 的讨论强调了一篇博客文章,详细介绍了高级 Rust 借用和生命周期概念,用于管理借用数据。文章探讨了 Rust 中不常用的功能,超越了基本的引用 (`&` 和 `&mut`),以提供对数据所有权和可变性的更精确控制。 一位经验丰富的 Rust 开发者评论称,这些功能是相对于 Python 和 JavaScript 等语言的一个显著优势,因为后者的变异行为可能不太可预测。他还提到发现了其他有用的、但鲜为人知的 Rust 功能,例如命名循环作用域。 另一位 Rust 新手发现博客文章中的概念有些令人困惑,特别是关于提议的“ref 类型”,例如 `&own` 和 `&uninit`。这场讨论强调了 Rust 所有权系统中的深度和细微之处,即使对于经验丰富的程序员来说也是如此。
相关文章

原文

The heart of Rust borrow-checking is this: when a borrow is taken, and until it expires, access to the borrowed place is restricted. For example you may not read from a place while it is mutably borrowed.

Over the recent months, lively discussions have been happening around teaching the borrow-checker to understand more things than just “shared borrow”/”mutable borrow”. We’re starting to have a few ideas floating around, so I thought I put them all down in a table so we can see how they interact. I’ll start with the tables and explain the new reference types after.

To clarify the vocabulary I’ll be using:

  • A “place” is a memory location, represented by an expression like x, *x, x.field etc;
  • “Taking a borrow” is the action of evaluating &place, &mut place, &own place etc to a value;
  • A “loan” happens when we take a borrow of a place. The loan remembers the place that was borrowed and the kind of reference used to borrow it. When we take that borrow, the resulting reference type gets a fresh lifetime; as long as that lifetime is present in the type of a value somewhere, the loan is considered “live”;
  • While a loan is live, further operations on the borrowed place are restricted;
  • After the loan expires, operations on the borrowed place may be restricted too.

Note: These tables are not enough to understand everything about these new references. E.g. after you write to a &uninit you can reborrow it as &own, and vice-versa; this is not reflected in the tables. Another example: dropping the contents of a place removes any pinning restriction placed on it.

Table 1: Given a reference, what actions are possible with it

How to read this table: the reference I have gives me the column, the action I want to do with it gives me the row (the reference types mean that the action is a reborrow with that type).

For example, if I have a &own T I can reborrow it into a &mut T but not a &pin own T. If I have a &mut T I may write a new value to it but not drop its contents.

“Move out” means “move the value out without putting a new one in”. “Drop” means “run the drop code in-place”.

  & &mut &own &pin &pin mut &pin own &uninit
&
&mut
&own
&pin
&pin mut
&pin own
&uninit
Read
Write
Move out
Drop

Table 2: If a loan was taken and is live, what can I still do to the place

How to read this table: a borrow of place p was taken and is still live; the kind of borrow gives me the column. The operations I may still do on the place (without going through that borrow) give me the row.

For example, if a &-borrow was taken and is live, I may still read the place.

  & &mut &own &pin &pin mut &pin own &uninit
&
&mut
&own
&pin
&pin mut
&pin own
&uninit
Read
Write
Move out
Drop

Unsurprisingly, most of the time we can’t do anything else to the place, because the borrow is exclusive.

Table 3: If a loan was taken and expired, what can I now do to the place

How to read this table: a borrow of place p was taken and has expired; the kind of borrow gives me the column. The operations I may now do on the place give me the row.

For example, if a &-borrow was taken and expired, I may no longer read the place.

  & &mut &own &pin &pin mut &pin own &uninit
&
&mut
&own
&pin
&pin mut
&pin own
&uninit
Read
Write
Move out
Drop

&own and &uninit both have the property that when they expire the place is considered to have been uninitialized. So we the only actions available are those that work on uninitialized places: writing and &uninit borrows.

The other point to note are the pinning loans: after a pinning loan expires, non-pinning mutable loans can’t be taken of that same place until the value is dropped.

All of these are speculative ideas, but at this point they’ve been circulating a bunch so should be pretty robust.

The owning reference &own T

&own T (which has also been called &move T) is a reference that says “I have full ownership over this value, in particular I am responsible for dropping it”. It feels like Box except that we don’t control the allocation the value resides in. In particular, we can move the T out of a &own T.

Like Box, &own T drops the contained value when it is dropped.

When I owning-borrow &own x a place, I have given up full ownership of the value, and thus must assume that the place is uninitialized when the borrow expires.

It makes for some interesting APIs:

impl<T> Vec<T> {
    // Pop the last value and return a reference to it (instead of moving it out directly).
    fn pop_own(&mut self) -> Option<&own T> { .. }
    // Iterate over the contained values, emptying the Vec as we go.
    fn drain_own(&mut self) -> impl Iterator<Item = &own T> { .. }
}

The uninitialized reference &uninit T

&uninit T (which has also been called &out T) is a reference to an allocated but not-yet-initialized location. Much like when we do let x;, the only thing one can do with that reference is write to it. Once written to, it can be reborrowed into everything we want.

// Typical usage is to initialize a value:
impl MyType {
    fn init(&uninit self) -> &own Self {
        *self = new_value();
        &own *self
    }
}
let x: MyType;
let ptr: &own MyType = MyType::init(&uninit x);
// `ptr` can be used much like a `Box` would. It cannot be returned from the
// current function though.

It has a nice synergy with &own T: we can get from &uninit T to &own T by writing a value into the reference, and get from &own T to &uninit T by moving the value out of the reference.

Both have the property that when they expire, the original place is considered uninitialized.

The pinning references &pin T/&pin mut T/&pin own T

Pinning is a notoriously subtle notion, if you’re not familiar with it I recommand the std docs on the topic. If you are familiar with it you may instead enjoy this blog post of mine that shines an original light on the notion.

Pinning references are variants of the existing reference types that also add a “pinning requirement” to the borrowed place. This requirement forbids moving the value out or deallocating the place without running Drop on the value first. This applies even after the pinning borrow has expired.

&pin mut T is the most common of these, it exists in today’s Rust as Pin<&mut T> and is crucial to our async story. &pin T would be Pin<&T>; it’s less clear how useful it is but I can imagine usecases.

&pin own T finally would be the owning variant. This one has the tricky requirement that it must not be passed to mem::forget as that would break the drop invariant. This isn’t possible in today’s Rust, but there have been proposals over the years as such non-forgettable types are needed for other things.

One funky aspect of &pin own T is that I think it’s ok to reborrow a &own T into a &pin own T: since the only way for the borrow to expire entails dropping the pointed-to value, there’s no way to break the pin guarantee so it doesn’t matter how we got that reference.

You’ll notice I didn’t list &pin uninit T. That’s because pinning is a property of a value, and &pin uninit T doesn’t point to a value. To pin-initialize a value, one can just write the value to a &uninit T then reborrow &uninit T -> &own T -> &pin own T.

联系我们 contact @ memedata.com