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

《Rust编程实战》系列第35篇

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

复制代码
Borrow

中文通常称为"借用"。

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

javascript 复制代码
fn print_name(name: String) {
    println!("{}", name);
}

fn main() {
    let name = String::from("Tom");
    print_name(name);
}

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

arduino 复制代码
println!("{}", name);

如果函数只是想读取字符串,并不需要真正拥有它,那么直接转移所有权显得非常浪费。

Rust为此提供了借用机制:

r 复制代码
&T

例如:

rust 复制代码
fn print_name(name: &String) {
    println!("{}", name);
}

fn main() {
    let name = String::from("Tom");
    print_name(&name);
    println!("{}", name);
}

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

本文将介绍:

  • 什么是Borrow

  • 为什么需要借用

  • 引用&T

  • 借用与Move的区别

  • 多个不可变引用

  • 函数参数中的借用

  • &String&str

  • Vec和切片借用

  • 结构体借用

  • 解引用*

  • 借用作用域

  • Borrow Checker

  • 常见借用错误

  • Borrow最佳实践

什么是Borrow

Borrow表示:

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

例如:

arduino 复制代码
fn main() {
    let text = String::from("Rust");
    let reference = &text;
    println!("{}", reference);
    println!("{}", text);
}

这里:

arduino 复制代码
&text

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

变量:

复制代码
reference

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

因此:

arduino 复制代码
text

仍然可以继续使用。

什么是引用

Rust中使用:

r 复制代码
&T

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

例如:

rust 复制代码
&i32
&String
&Vec<i32>
&User

如果:

ini 复制代码
let number = 100;

那么:

ini 复制代码
let reference = &number;

reference的类型就是:

rust 复制代码
&i32

示例:

ini 复制代码
fn main() {
    let number = 100;
    let reference: &i32 = &number;
    println!("{}", reference);
}

引用可以理解为:

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

为什么需要借用

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

arduino 复制代码
fn get_length(text: String) -> usize {
    text.len()
}

调用:

ini 复制代码
fn main() {
    let text = String::from("Hello Rust");
    let length = get_length(text);
    println!("长度:{}", length);
}

函数调用之后,text已经被移动,不能继续使用。

get_length()实际上只需要读取字符串长度,并不需要取得所有权。

更合理的写法:

arduino 复制代码
fn get_length(text: &String) -> usize {
    text.len()
}

fn main() {
    let text = String::from("Hello Rust");
    let length = get_length(&text);
    println!("长度:{}", length);
    println!("内容:{}", text);
}

这里:

scss 复制代码
get_length(&text)

只是把text借给函数使用。

Borrow与Move的区别

Move:

ini 复制代码
let first = String::from("Rust");
let second = first;

结果:

sql 复制代码
first失效
second取得所有权

Borrow:

ini 复制代码
let first = String::from("Rust");
let second = &first;

结果:

sql 复制代码
first仍然拥有数据
second只是借用数据

可以简单记住:

Move是"把东西给你"。

Borrow是"借给你看看"。

函数参数中的Borrow

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

例如:

arduino 复制代码
fn print_user(name: &String) {
    println!("用户名:{}", name);
}

调用:

scss 复制代码
fn main() {
    let name = String::from("Alice");
    print_user(&name);
    print_user(&name);
    println!("{}", name);
}

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

&String与&str

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

python 复制代码
&str

而不是:

arduino 复制代码
&String

例如:

ini 复制代码
fn print_name(name: &str) {
    println!("用户名:{}", name);
}

fn main() {
    let first = String::from("Tom");
    let second = "Alice";
    print_name(&first);
    print_name(second);
}

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

arduino 复制代码
String

的借用,也能接受字符串字面量。

所以只读字符串参数通常推荐:

javascript 复制代码
fn function(text: &str)

多个不可变引用

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

例如:

ini 复制代码
fn main() {
    let text = String::from("Rust");
    let first = &text;
    let second = &text;
    let third = &text;
    println!("{}", first);
    println!("{}", second);
    println!("{}", third);
}

这是安全的。

因为多个不可变引用只能读取数据,不会修改数据,因此不会产生数据冲突。

可以简单表示为:

sql 复制代码
      ┌─ first
text ─┼─ second
      └─ third

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

不可变引用不能修改数据

下面的代码会报错:

ini 复制代码
fn main() {
    let text = String::from("Rust");
    let reference = &text;
    reference.push_str(" Language");
}

原因是:

复制代码
reference

类型为:

arduino 复制代码
&String

它只是不可变借用。

不可变借用的规则是:

可以读取,不能修改。

如果需要通过引用修改数据,需要使用:

r 复制代码
&mut T

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

借用Vec

Vec也可以通过引用借用:

rust 复制代码
fn print_numbers(numbers: &Vec<i32>) {
    println!("{:?}", numbers);
}

fn main() {
    let numbers = vec![10, 20, 30];
    print_numbers(&numbers);
    println!("{:?}", numbers);
}

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

rust 复制代码
fn print_numbers(numbers: &[i32]) {
    for number in numbers {
        println!("{}", number);
    }
}

这样既可以传入Vec:

ini 复制代码
let numbers = vec![1, 2, 3];
print_numbers(&numbers);

也可以传入数组:

ini 复制代码
let numbers = [1, 2, 3];
print_numbers(&numbers);

借用结构体

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

rust 复制代码
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn print_user(user: &User) {
    println!("姓名:{}", user.name);
    println!("年龄:{}", user.age);
}

fn main() {
    let user = User {
        name: String::from("Tom"),
        age: 20,
    };
    print_user(&user);
    println!("{:?}", user);
}

print_user()不会取得User的所有权,因此调用后user仍然有效。

在业务项目中,大型结构体通常不会为了读取而频繁Move或Clone,而是通过:

sql 复制代码
&User

借用。

引用不会拥有数据

看下面代码:

ini 复制代码
fn main() {
    let text = String::from("Rust");
    let reference = &text;
    println!("{}", reference);
}

reference离开作用域时,不会释放字符串数据。

真正负责释放数据的是:

arduino 复制代码
text

因为它才是所有者。

可以理解为:

arduino 复制代码
text
  |
  └──拥有──> "Rust"
      ↑
      |
reference只是指向

解引用*

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

markdown 复制代码
*

称为解引用。

示例:

ini 复制代码
fn main() {
    let number = 100;
    let reference = &number;
    println!("{}", *reference);
}

这里:

yaml 复制代码
*reference

得到引用指向的i32值。

例如:

ini 复制代码
fn main() {
    let number = 10;
    let reference = &number;
    assert_eq!(number, *reference);
}

number是:

rust 复制代码
i32

reference是:

rust 复制代码
&i32

*reference再次得到:

rust 复制代码
i32

为什么println!经常不需要*

可能会发现:

arduino 复制代码
println!("{}", reference);

也能正常输出。

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

yaml 复制代码
*reference

类似地:

复制代码
user.name

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

借用的作用域

引用不能比它指向的数据活得更久。

错误示例:

rust 复制代码
fn create_reference() -> &String {
    let text = String::from("Rust");
    &text
}

这是不允许的。

因为函数结束时:

arduino 复制代码
text

会被释放。

如果允许返回:

arduino 复制代码
&text

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

这种引用叫:

复制代码
悬空引用 Dangling Reference

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

悬空引用

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

复制代码
创建数据
↓
取得指针
↓
释放数据
↓
继续访问指针

这可能造成严重内存错误。

Rust通过借用检查确保:

引用永远不能比被引用的数据活得更久。

因此Rust从编译阶段避免大量悬空指针问题。

借用作用域可以提前结束

看下面代码:

arduino 复制代码
fn main() {
    let text = String::from("Rust");
    let reference = &text;
    println!("{}", reference);
    println!("{}", text);
}

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

复制代码
reference

最后一次使用的位置。

当:

arduino 复制代码
println!("{}", reference);

结束后,如果后面不再使用reference,这次借用也可以认为已经结束,而不一定要等整个代码块结束。

这让Rust的借用规则更加灵活。

Borrow Checker是什么

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

复制代码
Borrow Checker

借用检查器主要保证:

  • 引用指向的数据仍然有效

  • 不会出现悬空引用

  • 不会发生不安全的同时读写

  • 可变访问不会产生数据竞争

    例如下面的代码无法通过编译:

ini 复制代码
fn main() {
    let reference;
    {
        let text = String::from("Rust");
        reference = &text;
    }
    println!("{}", reference);
}

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

借用与所有权的关系

假设:

ini 复制代码
let text = String::from("Rust");
let reference = &text;

关系是:

arduino 复制代码
text
拥有String
|
└── reference借用text

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

所以:

scss 复制代码
drop(reference);

不会释放String

最终仍然由text负责释放内存。

需要注意,对于Copy类型的引用,显式drop(reference)通常没有实际意义,因为引用本身可以直接复制。

借用与Clone的选择

假设函数只需要打印字符串。

方案一:

arduino 复制代码
fn print_text(text: String) {
    println!("{}", text);
}

fn main() {
    let text = String::from("Rust");
    print_text(text.clone());
}

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

更好的方式:

arduino 复制代码
fn print_text(text: &str) {
    println!("{}", text);
}

fn main() {
    let text = String::from("Rust");
    print_text(&text);
}

原则:

只读取数据时优先Borrow。

真正需要独立副本时再Clone。

实战:计算订单总价

rust 复制代码
#[derive(Debug)]
struct Order {
    product: String,
    price: f64,
    count: u32,
}

fn calculate_total(order: &Order) -> f64 {
    order.price * order.count as f64
}

fn main() {
    let order = Order {
        product: String::from("Rust Book"),
        price: 99.0,
        count: 3,
    };
    let total = calculate_total(&order);
    println!("订单:{:?}", order);
    println!("总价:{:.2}", total);
}

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

css 复制代码
&Order

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

实战:查找最大值

rust 复制代码
fn find_max(numbers: &[i32]) -> Option<&i32> {
    numbers.iter().max()
}

fn main() {
    let numbers = vec![10, 88, 35, 99, 20];
    match find_max(&numbers) {
        Some(max) => println!("最大值:{}", max),
        None => println!("没有数据"),
    }
    println!("{:?}", numbers);
}

函数接收:

css 复制代码
&[i32]

并返回:

rust 复制代码
Option<&i32>

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

常见错误

函数只读却取得所有权

不推荐:

php 复制代码
fn print_user(user: User)

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

php 复制代码
fn print_user(user: &User)

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

不推荐:

scss 复制代码
process(data.clone());

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

返回局部变量的引用

错误:

rust 复制代码
fn get_name() -> &String {
    let name = String::from("Tom");
    &name
}

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

试图通过不可变引用修改

错误:

ini 复制代码
let text = String::from("Rust");
let reference = &text;
reference.push('!');

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

把引用误认为新的所有者

ini 复制代码
let second = &first;

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

Borrow最佳实践

只读函数参数优先引用

字符串:

php 复制代码
fn print(text: &str)

集合:

php 复制代码
fn sum(values: &[i32])

结构体:

sql 复制代码
fn show(user: &User)

不需要独立数据时不要Clone

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

业务实体负责拥有数据

例如:

arduino 复制代码
struct User {
    name: String,
}

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

sql 复制代码
&User

或:

python 复制代码
&str

访问。

API通过类型表达意图

php 复制代码
fn save(user: User)

通常表达"函数接管User"。

sql 复制代码
fn show(user: &User)

表达"函数只临时访问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常见报错

  • 可变借用实战与最佳实践

相关推荐
城管不管1 小时前
2026.8.6成都分部一面当场OC
面试·职场和发展
程序员爱钓鱼10 小时前
Rust Copy详解:隐式复制与轻量数据类型
前端·后端·rust
仙人球部落 揞殺10 小时前
细说ASP.NET的各种异步操作
后端·asp.net
kaixin_learn_qt_ing11 小时前
了解Rust/Tauri
rust
卷无止境12 小时前
当独立开发者也能造出3A画质的游戏:Godot引擎深度解析
后端·python·godot
doiito12 小时前
【Agent Harness】Gliding Horse 最新升级:把“智能闭环”真正跑通的一次深度优化
rust·架构设计·系统设计·ai agent
万物皆智能12 小时前
AI合规面试:AI合规常见面试题与答题思路
人工智能·面试·职场和发展
独行侠影a13 小时前
SpringBoot 分布式锁实战:Redisson 解决订单超卖并发问题
spring boot·分布式·后端