惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
Google DeepMind News
Google DeepMind News
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
MongoDB | Blog
MongoDB | Blog
I
InfoQ
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
量子位
博客园 - 叶小钗
月光博客
月光博客
IT之家
IT之家
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Vercel News
Vercel News
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享

博客园 - PKICA

单片机 MCU,嵌入式,MPU,DSP,FPGA,PLC 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 博文阅读密码验证 - 博客园 rust线程-std::thread::park和unpark 配合Builder实现轻量级的线程挂起与唤醒 rust自定义线程属性std::thread::Builder rust线程 rust关联函数 rust高并发设计实践进阶 rust高并发设计实践 rust性能优化与安全边界 联合体union在rust和C语言中有什么区别 rust并发与异步编程 如何预防rust不良的代码设计总结 rust类型系统与零成本抽象 rust内存模型 告别大显存依赖!用 Rust 新一代深度学习框架 Burn 打造纯 CPU 文本分类推理引擎 rust类型系统标记 编译配置解答 git实用命令 rust底层设计理念值得注意的几个地方总结
rust可变引用作为函数参数的机理详解
PKICA · 2026-06-06 · via 博客园 - PKICA

本文其实是之前rust重借用机制与原理的细化,如果已经理解了这篇文章,可以不用再看这篇文章。

为了彻底搞懂这个问题,我们需要抛开具体的复杂业务代码,建立两个最核心的 Rust 底层世界观:

  • 引用也是一个普通的值。 它和 StringVec 没有本质区别,在内存里也占字节,也有自己的所有权
  • &mut T(可变引用)是独占的,绝对不允许被复制(Non-Copy)。

下面我们用最纯粹、最简单的抽象例子,一步步拆解“为什么可变引用直接传会触发 Move”。

1.理解为什么 &mut T 会触发 Move

在 Rust 中,所有类型在面临“赋值”或“传参”时,只有两条路可以走:要么执行 Copy(复制),要么执行 Move(移动)。

  • 如果是共享引用 &T:它实现了 Copy。因为多个只读指针指向同一个数据是绝对安全的。
  • 如果是可变引用 &mut T:它没有实现 Copy。因为 Rust 的安全基石是 “独占可变性”(同一时间只能有一个活跃的写指针)。如果 &mut T 允许被 Copy,意味着你可以完美克隆出两个一模一样的指针同时修改同一块内存,这会导致数据竞争。

因为 &mut T 不能被 Copy,所以当你把它当成一个普通变量直接传递时,它只能执行 Move。

抽象实验:

fn main() {
    let mut data = 42;
    let r1: &mut i32 = &mut data; // r1 拥有这个可变引用的所有权

    let r2 = r1; // 💥 触发了 Move!r1 的所有权转移给了 r2

    // println!("{}", r1); // ❌ 编译报错:使用了已被移动的值(use of moved value)
    println!("{}", r2); // ✅ 正常,现在 r2 是唯一合法的可变引用
}

2.为什么在函数调用时,我们很少感知到这个 Move?

既然直接传会触发 Move,那为什么我们在写普通 Rust 代码时,连续把一个可变引用传给两个函数,编译器却不报错呢?

fn print_mut(r: &mut i32) { /* ... */ }

fn foo(mut_ref: &mut i32) {
    print_mut(mut_ref); // 👈 看起来是直接传,这里为什么没被 Move?
    print_mut(mut_ref); // 👈 如果上面 Move 了,这里为什么还能用?
}

答案是:编译器在暗中帮我们做了一次“偷梁换柱”,这个机制叫 隐式再借用(Implicit Reborrow)。

当编译器看到 print_mut(mut_ref),并且发现你后面还要继续使用 mut_ref 时,它为了不让你写代码太痛苦,会悄悄把你的代码改写成:

print_mut(&mut *mut_ref); // 👈 编译器自动做的“再借用”
  • *mut_ref:先解引用,找到底层真正的数据。
  • &mut ...:对底层数据重新生成一个短命的、临时的可变引用。

在这个过程中,mut_ref 本身的所有权牢牢留在了 foo 函数里。借出去的只是一个临时的“影子引用的所有权”,等 print_mut 执行完,这个影子消失,mut_ref 自动复活。

3.什么时候隐式再借用会失效?(也就是为什么原代码要加 &mut

既然编译器这么聪明,为什么在某些特定的代码结构(比如函数参数: mut rjson: &mut RapidJson)中,直接传 rjson 会触发 Move,甚至逼得原作者必须手动写 &mut rjson 呢?

有以下几个经典场景会破坏编译器的聪明才智,导致隐式再借用失效,强行触发 Move:

场景 1:变量本身被 mut 修饰(指针重新绑定)

如果参数声明写成了 mut rjson: &mut T(注意变量名前面有 mut):
这告诉编译器:“我接下来不仅要用这个指针,我还可能在当前函数里把这个指针本身改指向别的地方(例如 rjson = &mut other_data;)。”
一旦变量本身是可变的,Rust 编译器在推导生命周期和再借用时就会变得极度保守。它为了确保万无一失,往往会直接放弃隐式再借用,将其视作纯粹的 Move。

场景 2:泛型或 FFI 边界的复杂包装

在涉及 extern "C" 跨语言边界、非标准的泛型约束、或者多重闭包(Closure)时,Rust 的生命周期分析器(Borrow Checker)无法完美穿透复杂的上下文。一旦它无法确定后续是否安全,就会立刻退化到最基础的语义:&mut T 没有 Copy 特性,一律按 Move 处理。

第四步:手动加上 &mut rjson 到底做了什么?

当隐式再借用因为上述原因失效,而你直接传 rjson 又会被 Move 导致后续无法使用时,手动加上 &mut rjson 成了一种强行的破局手段。

如果 rjson 的类型是 &mut T

  • 不加 &mut(直接写 rjson):你传递的是 &mut T 本身。因为隐式再借用失效,它发生 Move,rjson 变量从此死亡。
  • 加上 &mut rjson:你创建了一个全新的指针,类型是 &mut &mut T指向可变引用的可变引用)。
    你把这个新创建的双重指针作为值传进了下一个函数。在这个过程中,作为基石的 rjson 本身没有任何所有权转移,它安全地留在当前函数中。

虽然接收方函数可能只需要 &mut T,但 Rust 面对 &mut &mut T 传入 &mut T 的参数要求时,会触发标准的 Deref(解引用)自动降级,将其剥离为单层引用成功匹配,同时完美保住了 rjson 的所有权。

总结核心逻辑

  • rjson 本身确实是可变引用。
  • 但可变引用在 Rust 中是独占且不可复制的(Non-Copy)。
  • 纯 Rust 代码下能直接传,全靠编译器在背后偷偷做 &mut *rjson(再借用)。
  • 一旦代码复杂(如变量自身可变、FFI 边界),编译器的偷懒机制失效,直接传就变成了不可逆的 Move。
  • 手动加 &mut rjson 是通过显式创建一个临时的“二级指针”,强行把底层 rjson 的所有权锁在当前函数里。

参考资料:

1.rust重借用机制与原理