Rust Borrow借用详解:不转移所有权访问数据

作者:程序员爱钓鱼日期:2026/8/9

《Rust编程实战》系列第35篇
在上一篇文章中,我们学习了Rust的Copy,了解了哪些轻量数据类型可以在赋值或传参时自动复制。本篇继续进入Rust所有权体系中非常重要的概念:

1Borrow
2

中文通常称为“借用”。
前面我们已经知道,StringVec等非Copy类型传递给另一个变量或函数时,通常会发生所有权移动:

1fn print_name(name: String) {
2    println!("{}", name);
3}
4
5fn main() {
6    let name = String::from("Tom");
7    print_name(name);
8}
9

调用print_name(name)后,name的所有权进入函数,后面不能继续使用:

1println!("{}", name);
2

如果函数只是想读取字符串,并不需要真正拥有它,那么直接转移所有权显得非常浪费。
Rust为此提供了借用机制:

1&T
2

例如:

1fn print_name(name: &String) {
2    println!("{}", name);
3}
4
5fn main() {
6    let name = String::from("Tom");
7    print_name(&name);
8    println!("{}", name);
9}
10

这次函数只是临时借用了name,所有权仍然属于main中的变量。
本文将介绍:

  • 什么是Borrow
  • 为什么需要借用
  • 引用&T
  • 借用与Move的区别
  • 多个不可变引用
  • 函数参数中的借用
  • &String&str
  • Vec和切片借用
  • 结构体借用
  • 解引用*
  • 借用作用域
  • Borrow Checker
  • 常见借用错误
  • Borrow最佳实践

什么是Borrow

Borrow表示:

临时访问一个值,但不取得这个值的所有权。
例如:

1fn main() {
2    let text = String::from("Rust");
3    let reference = &text;
4    println!("{}", reference);
5    println!("{}", text);
6}
7

这里:

1&text
2

创建了一个指向text的引用。
变量:

1reference
2

只是借用了text,并没有拥有字符串数据。
因此:

1text
2

仍然可以继续使用。

什么是引用

Rust中使用:

1&T
2

表示某个类型T的不可变引用。
例如:

1&i32
2&String
3&Vec<i32>
4&User
5

如果:

1let number = 100;
2

那么:

1let reference = &number;
2

reference的类型就是:

1&i32
2

示例:

1fn main() {
2    let number = 100;
3    let reference: &i32 = &number;
4    println!("{}", reference);
5}
6

引用可以理解为:

一个指向原数据的安全地址,但它受到Rust借用规则约束。

为什么需要借用

假设有一个函数用于计算字符串长度:

1fn get_length(text: String) -> usize {
2    text.len()
3}
4

调用:

1fn main() {
2    let text = String::from("Hello Rust");
3    let length = get_length(text);
4    println!("长度:{}", length);
5}
6

函数调用之后,text已经被移动,不能继续使用。
get_length()实际上只需要读取字符串长度,并不需要取得所有权。
更合理的写法:

1fn get_length(text: &String) -> usize {
2    text.len()
3}
4
5fn main() {
6    let text = String::from("Hello Rust");
7    let length = get_length(&text);
8    println!("长度:{}", length);
9    println!("内容:{}", text);
10}
11

这里:

1get_length(&text)
2

只是把text借给函数使用。

Borrow与Move的区别

Move:

1let first = String::from("Rust");
2let second = first;
3

结果:

1first失效
2second取得所有权
3

Borrow:

1let first = String::from("Rust");
2let second = &first;
3

结果:

1first仍然拥有数据
2second只是借用数据
3

可以简单记住:

Move是“把东西给你”。
Borrow是“借给你看看”。

函数参数中的Borrow

实际开发中,借用最常见的地方就是函数参数。
例如:

1fn print_user(name: &String) {
2    println!("用户名:{}", name);
3}
4

调用:

1fn main() {
2    let name = String::from("Alice");
3    print_user(&name);
4    print_user(&name);
5    println!("{}", name);
6}
7

同一个变量可以被反复借用,因为所有权没有离开原变量。

&String与&str

如果函数只需要读取字符串,一般更推荐:

1&str
2

而不是:

1&String
2

例如:

1fn print_name(name: &str) {
2    println!("用户名:{}", name);
3}
4
5fn main() {
6    let first = String::from("Tom");
7    let second = "Alice";
8    print_name(&first);
9    print_name(second);
10}
11

&str更加通用,它既能接受:

1String
2

的借用,也能接受字符串字面量。
所以只读字符串参数通常推荐:

1fn function(text: &str)
2

多个不可变引用

Rust允许同时存在多个不可变引用。
例如:

1fn main() {
2    let text = String::from("Rust");
3    let first = &text;
4    let second = &text;
5    let third = &text;
6    println!("{}", first);
7    println!("{}", second);
8    println!("{}", third);
9}
10

这是安全的。
因为多个不可变引用只能读取数据,不会修改数据,因此不会产生数据冲突。
可以简单表示为:

1      ┌─ first
2text ─┼─ second
3      └─ third
4

所有引用都只是读取同一个值。

不可变引用不能修改数据

下面的代码会报错:

1fn main() {
2    let text = String::from("Rust");
3    let reference = &text;
4    reference.push_str(" Language");
5}
6

原因是:

1reference
2

类型为:

1&String
2

它只是不可变借用。
不可变借用的规则是:

可以读取,不能修改。
如果需要通过引用修改数据,需要使用:

1&mut T
2

这就是下一篇要重点讲解的“可变借用”。

借用Vec

Vec也可以通过引用借用:

1fn print_numbers(numbers: &Vec<i32>) {
2    println!("{:?}", numbers);
3}
4
5fn main() {
6    let numbers = vec![10, 20, 30];
7    print_numbers(&numbers);
8    println!("{:?}", numbers);
9}
10

不过函数只需要读取元素时,更推荐切片:

1fn print_numbers(numbers: &[i32]) {
2    for number in numbers {
3        println!("{}", number);
4    }
5}
6

这样既可以传入Vec:

1let numbers = vec![1, 2, 3];
2print_numbers(&numbers);
3

也可以传入数组:

1let numbers = [1, 2, 3];
2print_numbers(&numbers);
3

借用结构体

结构体同样可以通过引用传入函数。

1#[derive(Debug)]
2struct User {
3    name: String,
4    age: u32,
5}
6
7fn print_user(user: &User) {
8    println!("姓名:{}", user.name);
9    println!("年龄:{}", user.age);
10}
11
12fn main() {
13    let user = User {
14        name: String::from("Tom"),
15        age: 20,
16    };
17    print_user(&user);
18    println!("{:?}", user);
19}
20

print_user()不会取得User的所有权,因此调用后user仍然有效。
在业务项目中,大型结构体通常不会为了读取而频繁Move或Clone,而是通过:

1&User
2

借用。

引用不会拥有数据

看下面代码:

1fn main() {
2    let text = String::from("Rust");
3    let reference = &text;
4    println!("{}", reference);
5}
6

reference离开作用域时,不会释放字符串数据。
真正负责释放数据的是:

1text
2

因为它才是所有者。
可以理解为:

1text
2  |
3  └──拥有──> "Rust"
4      
5      |
6reference只是指向
7

解引用*

引用保存的是对数据的访问方式,如果想获得引用指向的值,可以使用:

1*
2

称为解引用。
示例:

1fn main() {
2    let number = 100;
3    let reference = &number;
4    println!("{}", *reference);
5}
6

这里:

1*reference
2

得到引用指向的i32值。
例如:

1fn main() {
2    let number = 10;
3    let reference = &number;
4    assert_eq!(number, *reference);
5}
6

number是:

1i32
2

reference是:

1&i32
2

*reference再次得到:

1i32
2

为什么println!经常不需要*

可能会发现:

1println!("{}", reference);
2

也能正常输出。
这是因为Rust很多场景会自动进行解引用或格式化处理,不需要每次显式写:

1*reference
2

类似地:

1user.name
2

即使user&User,Rust也通常能够自动处理字段访问。

借用的作用域

引用不能比它指向的数据活得更久。
错误示例:

1fn create_reference() -> &String {
2    let text = String::from("Rust");
3    &text
4}
5

这是不允许的。
因为函数结束时:

1text
2

会被释放。
如果允许返回:

1&text
2

那么返回的引用将指向已经被释放的内存。
这种引用叫:

1悬空引用 Dangling Reference
2

Rust编译器会直接阻止这种代码。

悬空引用

在C/C++中可能出现类似:

1创建数据
2
3取得指针
4
5释放数据
6
7继续访问指针
8

这可能造成严重内存错误。
Rust通过借用检查确保:

引用永远不能比被引用的数据活得更久。
因此Rust从编译阶段避免大量悬空指针问题。

借用作用域可以提前结束

看下面代码:

1fn main() {
2    let text = String::from("Rust");
3    let reference = &text;
4    println!("{}", reference);
5    println!("{}", text);
6}
7

现代Rust借用检查器通常会判断:

1reference
2

最后一次使用的位置。
当:

1println!("{}", reference);
2

结束后,如果后面不再使用reference,这次借用也可以认为已经结束,而不一定要等整个代码块结束。
这让Rust的借用规则更加灵活。

Borrow Checker是什么

Rust编译器中负责检查借用是否合法的机制通常称为:

1Borrow Checker
2

借用检查器主要保证:

  • 引用指向的数据仍然有效
  • 不会出现悬空引用
  • 不会发生不安全的同时读写
  • 可变访问不会产生数据竞争
    例如下面的代码无法通过编译:
1fn main() {
2    let reference;
3    {
4        let text = String::from("Rust");
5        reference = &text;
6    }
7    println!("{}", reference);
8}
9

因为text在内部代码块结束时已经被释放,但reference还想继续访问它。

借用与所有权的关系

假设:

1let text = String::from("Rust");
2let reference = &text;
3

关系是:

1text
2拥有String
3|
4└── reference借用text
5

借用不会改变真正的所有者。
所以:

1drop(reference);
2

不会释放String
最终仍然由text负责释放内存。
需要注意,对于Copy类型的引用,显式drop(reference)通常没有实际意义,因为引用本身可以直接复制。

借用与Clone的选择

假设函数只需要打印字符串。
方案一:

1fn print_text(text: String) {
2    println!("{}", text);
3}
4
5fn main() {
6    let text = String::from("Rust");
7    print_text(text.clone());
8}
9

虽然可以工作,但为了打印一次就复制整个字符串并不划算。
更好的方式:

1fn print_text(text: &str) {
2    println!("{}", text);
3}
4
5fn main() {
6    let text = String::from("Rust");
7    print_text(&text);
8}
9

原则:

只读取数据时优先Borrow。
真正需要独立副本时再Clone。

实战:计算订单总价

1#[derive(Debug)]
2struct Order {
3    product: String,
4    price: f64,
5    count: u32,
6}
7
8fn calculate_total(order: &Order) -> f64 {
9    order.price * order.count as f64
10}
11
12fn main() {
13    let order = Order {
14        product: String::from("Rust Book"),
15        price: 99.0,
16        count: 3,
17    };
18    let total = calculate_total(&order);
19    println!("订单:{:?}", order);
20    println!("总价:{:.2}", total);
21}
22

函数只读取订单,所以使用:

1&Order
2

而不是取得整个Order的所有权。

实战:查找最大值

1fn find_max(numbers: &[i32]) -> Option<&i32> {
2    numbers.iter().max()
3}
4
5fn main() {
6    let numbers = vec![10, 88, 35, 99, 20];
7    match find_max(&numbers) {
8        Some(max) => println!("最大值:{}", max),
9        None => println!("没有数据"),
10    }
11    println!("{:?}", numbers);
12}
13

函数接收:

1&[i32]
2

并返回:

1Option<&i32>
2

整个过程中不需要复制Vec中的数据。

常见错误

函数只读却取得所有权

不推荐:

1fn print_user(user: User)
2

如果只查看用户信息,更推荐:

1fn print_user(user: &User)
2

为了继续使用原变量到处clone

不推荐:

1process(data.clone());
2

如果process()只是读取数据,应改成接受引用。

返回局部变量的引用

错误:

1fn get_name() -> &String {
2    let name = String::from("Tom");
3    &name
4}
5

局部变量离开函数后会被释放,不能返回指向它的引用。

试图通过不可变引用修改

错误:

1let text = String::from("Rust");
2let reference = &text;
3reference.push('!');
4

需要修改时必须使用可变借用。

把引用误认为新的所有者

1let second = &first;
2

并不会转移或复制底层数据,只是借用。

Borrow最佳实践

只读函数参数优先引用

字符串:

1fn print(text: &str)
2

集合:

1fn sum(values: &[i32])
2

结构体:

1fn show(user: &User)
2

不需要独立数据时不要Clone

Borrow通常比复制大型String、Vec、HashMap更高效。

业务实体负责拥有数据

例如:

1struct User {
2    name: String,
3}
4

User拥有用户名,而临时处理函数使用:

1&User
2

或:

1&str
2

访问。

API通过类型表达意图

1fn save(user: User)
2

通常表达“函数接管User”。

1fn show(user: &User)
2

表达“函数只临时访问User”。
良好的Rust API应该让所有权意图一眼就能看出来。

本章小结

Borrow让Rust可以在不转移所有权的情况下安全访问数据。
本文学习了:

  • Borrow表示临时借用数据
  • 使用&T创建不可变引用
  • 借用不会改变数据所有者
  • 多个不可变引用可以同时存在
  • 不可变引用只能读取数据
  • 函数参数可以通过引用避免Move
  • 字符串参数通常推荐&str
  • 集合参数通常推荐切片&[T]
  • 结构体可以通过&Struct借用
  • 使用*进行解引用
  • 引用不能比原数据活得更久
  • Rust通过Borrow Checker防止悬空引用
  • Borrow通常比无意义的Clone更加高效
    可以记住:

Ownership决定谁拥有数据。
Move负责转移所有权。
Borrow允许临时访问数据而不取得所有权。
如果函数只是读取数据,优先考虑借用而不是Move或Clone。

下一篇预告

下一篇我们将继续学习:

Rust可变借用详解:&mut T与安全修改数据

内容包括:

  • 什么是可变借用
  • &mut T基本语法
  • 通过可变引用修改数据
  • 为什么同一时间只能有一个可变引用
  • 不可变借用与可变借用冲突
  • 可变借用作用域
  • 函数参数中的&mut
  • String、Vec和Struct的可变借用
  • Reborrow重新借用
  • Borrow Checker常见报错
  • 可变借用实战与最佳实践

Rust Borrow借用详解:不转移所有权访问数据》 是转载文章,点击查看原文


相关推荐


Rust图像处理第22节-从RGB到YCbCr: 让亮度和颜色分家
花褪残红青杏小2026/7/31

🦀 Rust + WASM 实战系列 第 22 篇 阅读时间:约 8 分钟 | 实战可运行 📌 写在前面 前面 21 篇,所有图像都是拿 R、G、B 三个数表示的。这一篇换一套坐标系:一个亮度 + 两个色度。 手机里的每一张照片、你看的每一个视频,存进文件之前都会先做这个转换。原因很简单:人眼对亮度极其敏感,对颜色极其迟钝,把两者分开之后,颜色那部分可以随便砍。 数学上,它就是一个 3×3 矩阵(19节的矩阵 × 向量)加它的逆矩阵(21节的 try_inverse) 🚀 TL;DR


🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚!
ReBound2026/7/23

🤯 面试被问 AI Workflow 和 Agent 有啥区别?3 张图 + 2 段代码讲清楚! 摘要:Workflow 和 Agent 到底有啥区别?为什么 Coze 工作流和 ReAct Agent 看起来都在"执行任务",本质却完全不同?本文从面试翻车经历出发,结合 Anthropic 官方定义、ReAct 框架、完整代码示例和实战选型指南,帮你彻底搞懂这对最容易混淆的 AI 概念。 📌 前言 上周面试,面试官笑眯眯地问了一句: "你说说 AI Workflow 和 Agent


大模型入门:从“猜词游戏“到“超级大脑“,一篇读懂 AI 大模型
修己xj2026/7/15

2023 年初,ChatGPT 横空出世,"大模型"三个字一夜之间刷爆了所有人的朋友圈。有人拿它写代码,有人拿它写情书,还有人拿它辅导孩子做数学——而且它居然真的会。 可当你真正想搞懂"大模型到底是什么"时,迎面而来的却是满屏的"Transformer""自注意力""千亿参数",瞬间劝退。 别慌。这篇文章,我们用打比方的方式,把大模型从里到外讲清楚。 一、什么是大模型?先搞懂"大"在哪 今天大家口中的大模型,通常特指大语言模型(LLM,Large Language Model)。ChatGPT


【Java实习面试算法冲刺】双指针
ZenithSourceQuest2026/7/6

第2类题型:双指针 为什么双指针题看起来不难,你一到面试就容易写乱 很多同学第一次刷双指针时,会觉得这类题比哈希表还“直观”。因为代码通常不长,变量也常常只有 left、right、slow、fast 四个名字。但真正到了面试现场,双指针反而很容易暴露出两类问题: 你会套模板,但说不清两个指针各自代表什么。你知道要移动某一边,却解释不出“为什么这样移动不会漏解”。你能把 三数之和 写个大概,却总在去重和边界上翻车。你把“会写代码”当成“理解题型”,结果一换题面就不稳。 如果你


论虚拟线程与 Kotlin IO 协程:资源开销、时长、高并发表现及适用场景与技术选型思考
zimoyin2026/6/28

在现代并发编程中,虚拟线程(由 Java 20+ 引入)和 Kotlin IO 协程(基于 Dispatchers.IO)是两种高效处理异步任务的技术框架。在资源开销、时长(特别是长时间 IO)、高并发场景表现、以及何时选择合适方案等方面各有特点。本文将逐一展开对比分析。 1. 资源开销对比 虚拟线程和协程的核心开销差异源于其底层设计原理: 维度虚拟线程Kotlin IO 协程关键差异内存开销固定栈机制:默认约 1MB1\text{MB}1MB 占⽤[注1]动态内存分配:2KB∼512KB2


当 AI 学会「自己催自己」:对 Loop Engineering 的理解与思考
莫西很trouble2026/6/19

先说一个让我「咯噔」一下的瞬间 前段时间看到 Claude Code 负责人 Boris Cherny 说了句话,大意是:我已经不写 Prompt 了,我只写 Loop 我的第一反应是:啊?Prompt 不是刚学会怎么写好吗?怎么就又过时了? 但仔细想了下这句话背后的意思,突然意识到它不是在讲什么新技术,而是在讲一个我们早就该意识到的问题 ——如果每次用 AI,你都要在它身边喊「继续」「还是报错」「你改了啥」「回滚」,那说明你其实不是在用工具,你是在当监工 而 Loop Engineer


限流:从单机QPS计数器到分布式三层防御体系
程序员小策2026/6/11

大家好,我是程序员小策。 先说一个反直觉的事实:加了限流之后,你系统的成功请求数量反而可能变多。 听起来很荒诞对吧?限流的字面意思就是"拦住一部分请求",拦住了怎么可能变多? 但数据不会骗人: 场景总请求数成功数成功率不限流5000000%加了限流50000500010% 不限流的时候,50000 个请求全部涌入数据库,连接池打满,超时重试又制造了一倍流量,雪崩导致所有接口全部失败——包括那些只想来浏览商品页的正常用户。 加了限流之后,50000 个请求里被拦掉了 45000 个,但这


Java学习笔记之泛型
飞翔网2026/6/4

前言 写 Java 代码时,你一定见过 List<String>、Map<Integer, String> 这种尖括号写法。这就是泛型(Generics)——Java 5 引入的最重要的语言特性之一。在没有泛型的时代,集合里塞什么都可以,取出来必须强制转型,稍不注意就 ClassCastException(类转型异常)。泛型的出现让类型安全从运行时提到了编译期。 但这只是泛型的冰山一角。泛型真正的难点在于类型擦除、通配符、PECS 原则——理解了这些,你才算真正掌握了泛型。 一、概念:什么是泛


【SpringBoot+Elasticsearch 内容搜索系统实战】:架构设计与全流程实现
fengxin_rou2026/5/29

🔥你好我是fengxin_rou这是我的个人主页fengxin_rou的主页 ❄️欢迎查看我的专栏我的专栏 《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》 目录 前言 一、Elasticsearch 索引设计与初始化 1.1 核心概念类比 1.2 索引初始化实现 1.3 字段设计要点 二、搜索索引数据写入与同步机


深入理解 Kotlin 协程 (六):进退有度,解密协程取消响应与异常分发机制
雨白2026/5/7

协程的取消机制 取消协程需要协程内部配合,这点和线程一样,本质上也是协作式的取消,就是将状态设置为取消,协程内部根据状态的变化来响应。 完善 Job 的状态流转与取消通知 我们基于上一篇博客中的代码,来完善协程的取消逻辑。 首先支持协程取消回调的注册: // [AbstractCoroutine.kt] override fun invokeOnCancel(onCancel: OnCancel): Disposable { // 1. 创建回调包装对象,以便后续可以手动解绑 v

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读