Rust 借用规则:为什么可以同时共享借用和修改同一个元组的不同字段?

在学习 Rust 的所有权与借用机制时,我们通常会接触到一条重要规则:

同一份数据可以同时存在多个共享引用,但共享借用与独占的可变借用不能发生冲突。

然而,考虑下面这段代码:

fn main() {
    let mut name = (
        String::from("Ferris"),
        String::from("Rustacean"),
    );

    let first = &name.0;

    name.1.push_str(", Esq.");

    println!("{first} {}", name.1);
}

这段代码能够正常编译,输出:

Ferris Rustacean, Esq.

这里似乎存在一个矛盾:我们先通过 &name.0 创建了共享引用,随后又通过 name.1.push_str() 修改了同一个元组中的数据。为什么 Rust 没有认为它们发生借用冲突?

要理解这个问题,需要明确一个关键概念:Rust 的借用检查不仅关注整个变量,还能够区分变量内部不同的数据位置(Place)。

一、元组的不同字段是独立的数据位置

首先分析:

let mut name = (
    String::from("Ferris"),
    String::from("Rustacean"),
);

这里创建了一个元组:

(String, String)

它包含两个独立的 String 值:

name
├── name.0 → String("Ferris")
└── name.1 → String("Rustacean")

虽然两个字段属于同一个元组,但它们是不同的数据位置,各自拥有自己的字符串数据。

随后执行:

let first = &name.0;

这里创建了一个共享引用,其类型是:

&String

需要注意,first 借用的仅仅是 name.0,而不是整个 name。

接下来:

name.1.push_str(", Esq.");

push_str() 需要可变借用其接收者,因此这里发生的是对 name.1 的可变借用。

于是,两次借用分别作用于:

共享借用:name.0
可变借用:name.1

由于这两个字段互不重叠,因此不会发生借用冲突。

即使 first 在修改 name.1 后仍然需要使用,代码依然合法。

二、Rust 的借用规则针对具体的数据位置

我们通常将 Rust 的借用规则简化为:

  • 多个共享引用可以同时读取同一份数据。
  • 可变引用需要独占访问,不能与冲突的借用同时有效。
  • 借用结束后,原变量可以继续正常访问。

但是,这里的“同一份数据”尤其重要。

Rust 并不是简单地禁止同一个变量内部同时存在共享借用与可变借用。

相反,借用检查器会尝试判断不同的借用是否访问相互重叠的数据位置。

对于元组和结构体的不同字段,Rust 通常能够直接判断它们互不重叠。

因此:

let first = &name.0;

name.1.push_str(", Esq.");

完全合法。

我们可以将这一规则更准确地表述为:

Rust 禁止有效借用期间相互冲突的访问,但允许对能够证明互不重叠的数据位置分别进行共享借用与可变借用。

三、如果修改被共享借用的字段呢?

现在稍微修改原来的程序:

fn main() {
    let mut name = (
        String::from("Ferris"),
        String::from("Rustacean"),
    );

    let first = &name.0;

    name.0.push_str(", Esq."); // 编译错误

    println!("{first}");
}

这次无法通过编译。

原因在于:

let first = &name.0;

共享借用了 name.0。

而:

name.0.push_str(", Esq.");

试图可变借用同一个 name.0。

由于后面仍然需要使用 first,共享借用尚未结束,因此两个借用发生冲突。

对比来看:

操作 是否合法 原因
共享借用 name.0,修改 name.1 合法 两个字段互不重叠
共享借用 name.0,修改 name.0,之后继续使用引用 不合法 同一字段发生借用冲突
共享借用 name.0,最后一次使用引用后再修改 name.0 合法 共享借用已结束

这里还要注意一个细节:原始代码之所以合法,并不是因为 first 的借用提前结束,而是因为修改发生在另一个独立字段上。

四、这一规则同样适用于结构体

Rust 对结构体字段也采用类似的借用检查规则。

例如:

struct Person {
    first: String,
    last: String,
}

fn main() {
    let mut p = Person {
        first: String::from("Ferris"),
        last: String::from("Rustacean"),
    };

    let first = &p.first;

    p.last.push_str(", Esq.");

    println!("{first} {}", p.last);
}

这段代码同样可以正常编译。

因为:

&p.first

借用的是 first 字段,而:

p.last.push_str(", Esq.");

修改的是 last 字段。

二者互不重叠,因此不存在冲突。

不过,Rust 并不总能证明任意两个数据位置互不重叠。例如,使用变量索引访问同一个数组中的不同元素时,编译器通常无法仅凭索引表达式证明两个可变借用不重叠,需要使用 split_at_mut() 等专门的方法。

因此,不能简单地认为只要程序员知道两个位置不同,编译器就一定能够接受。

五、总结

通过这个例子,我们可以进一步完善对 Rust 借用规则的理解。

Rust 的借用检查并不是简单地针对整个变量进行限制,而是能够分析变量内部的具体数据位置。

对于元组、结构体中互不重叠的字段,即使一个字段正在被共享借用,也可以对另一个字段进行可变借用。

因此:

let first = &name.0;
name.1.push_str(", Esq.");
println!("{first} {}", name.1);

能够正常执行。

最终可以记住一条更加严谨的规则:

Rust 允许多个共享引用同时读取数据,也允许独占的可变访问,但不能让相互冲突的访问在有效借用期间重叠。对于编译器能够证明互不重叠的数据位置,可以分别进行借用。

理解这一点后,我们就能够更准确地判断元组、结构体及其他复合数据类型中的借用行为,而不会误以为“只要创建了共享引用,就不能修改整个变量的任何部分”。


参考资料:The Rust Programming Language — References and Borrowing;The Rust Reference — Place Expressions。

posted @ 2026-10-30 15:11  ZhengQC  阅读(3)  评论(0)    收藏  举报