Rust:借用 & 切片
借用
在C++中,传递参数时我们经常需要在值传递、指针传递、引用传递之间选择。Rust的借用系统在编译期就解决了很多C++运行时才能发现的内存安全问题。
&T 不可变借用
不可变借用允许你读取数据,但不能通过借用修改数据。
语法:
            
            
              rust
              
              
            
          
          let 借用: &类型 = &变量;示例:
            
            
              rust
              
              
            
          
          let s: String = String::from("hello");
let r: &String = &s;
println!("原始字符串: {}", s);
println!("借用内容: {}", r);不可变借用不会获取所有权,原始变量仍然有效。通过&操作符创建借用,借用只是指向数据的指针,相当于一个别名,你可以同时通过r和c访问这个字符串。
不可变借用不能修改数据
尝试通过不可变借用修改数据会导致编译错误:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r = &s;
r.push_str(", world"); // 编译错误!这个限制确保了数据的不变性。即使原始变量是mut的,通过不可变借用也无法修改数据,这是Rust类型系统的核心安全保证。
多个不可变借用可以同时存在
可以同时创建多个不可变借用:
            
            
              rust
              
              
            
          
          let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{}, {}, {}", r1, r2, r3);多个不可变借用是安全的,因为它们都只是读取数据,不会造成数据竞争。读取操作本身是线程安全的。
&mut T 可变借用
可变借用允许你通过借用修改数据。
语法:
            
            
              rust
              
              
            
          
          let 借用: &mut 类型 = &mut 变量;示例:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r: &mut String = &mut s;
r.push_str(", world");
println!("修改后: {}", r);可变借用在其生命周期内独占访问权。注意原始变量必须声明为mut才能创建可变借用。
可变借用存在时原始变量不可访问
当可变借用存在时,不能同时使用原始变量:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r = &mut s;
println!("{}", s); // 编译错误!
r.push_str(", world");以上代码中,r 获取了字符串的可变借用,同时用户使用了原始变量 s,编译器不允许这种行为,可变借用使用期间,独占所有权。
这个限制防止了数据竞争。如果允许同时通过原始变量和可变借用访问数据,可能导致数据不一致。
借用规则
Rust的借用规则是其内存安全的核心,这些规则在编译期强制执行,防止数据竞争和内存安全问题。
规则1:同一时间只能有一个可变借用
正确的使用方式:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(", world");
println!("{}", r1);违反规则的错误示例:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // 编译错误
println!("{}, {}", r1, r2);这个规则防止了数据竞争。如果允许多个可变借用同时存在,可能导致一个借用修改数据时,另一个借用读取到不一致的状态。
规则2:可变借用与不可变借用不能同时存在
正确的使用方式:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);违反规则的错误示例:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 编译错误!
println!("{}, {}, {}", r1, r2, r3);这个规则防止不可变借用的数据被意外修改。如果允许可变借用和不可变借用同时存在,不可变借用持有者期望数据不变,但可变借用可能会修改数据,导致不可变借用看到意外的数据变化。
规则3:借用的生命周期不能超过其借用的数据
正确的生命周期管理:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
{
    let r1 = &s;
    println!("{}", r1);
} // r1 在这里超出作用域
let r2 = &mut s;
r2.push_str(", world");
println!("{}", r2);违反生命周期规则的错误示例:
            
            
              rust
              
              
            
          
          let r;
{
    let x = 5;
    r = &x; // 编译错误:x 的生命周期不够长
}
println!("{}", r);借用必须始终指向有效的内存。这个规则防止了悬垂借用,确保借用永远不会指向已经被释放的内存。
Non-Lexical Lifetimes (NLL) 优化
Rust 2018引入了更智能的借用检查,能够分析借用的实际使用范围:
            
            
              rust
              
              
            
          
          let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这里不再被使用,生命周期结束
let r3 = &mut s; // 现在可以创建可变借用
println!("{}", r3);编译器能够分析出r1和r2在println!之后不再使用,所以允许后续创建可变借用。这比简单的词法作用域更智能,提高了代码的灵活性。
解借用
* 操作符
使用*操作符可以访问借用指向的值:
            
            
              rust
              
              
            
          
          let x = 5;
let y = &x;
println!("x = {}", x);
println!("*y = {}", *y);解借用操作符*获取借用指向的实际值,这是最基础的解借用方式。
借用和值是不同的类型,不能直接比较
            
            
              rust
              
              
            
          
          let x = 5;
let y = &x;
assert_eq!(5, y); // 编译错误:不能比较i32和&i32
assert_eq!(5, *y);   // 正确:解借用后比较必须通过解借用将借用转换为值才能进行操作。
多级借用
可以创建指向借用的借用:
            
            
              rust
              
              
            
          
          let x = 5;
let r1 = &x;      // r1: &i32
let r2 = &r1;     // r2: &&i32  
let r3 = &r2;     // r3: &&&i32
println!("x = {}", x);
println!("*r1 = {}", *r1);      // 5
println!("**r2 = {}", **r2);    // 5  
println!("***r3 = {}", ***r3);  // 5每增加一层借用,就需要增加一个*来解借用。
自动解借用
- 方法调用
在使用.调用方法时,Rust会自动解借用:
            
            
              rust
              
              
            
          
          let s = String::from("hello");
let r1 = &s;
let r2 = &r1;
let r3 = &r2;
// 以下调用都等价
println!("直接调用: {}", s.len());
println!("一级借用: {}", r1.len());      // 等价于 (*r1).len()
println!("二级借用: {}", r2.len());      // 等价于 (**r2).len()  
println!("三级借用: {}", r3.len());      // 等价于 (***r3).len()方法调用会自动解借用到找到对应方法为止。
- 字段访问
访问结构体字段时也会自动解借用:
            
            
              rust
              
              
            
          
          struct Point {
    x: i32,
    y: i32,
}
fn main() {
    let point = Point { x: 10, y: 20 };
    let r1 = &point;
    let r2 = &r1;
    
    // 以下访问都等价
    println!("直接访问: ({}, {})", point.x, point.y);
    println!("一级借用: ({}, {})", r1.x, r1.y);        // 等价于 (*r1).x, (*r1).y
    println!("二级借用: ({}, {})", r2.x, r2.y);        // 等价于 (**r2).x, (**r2).y
}如果把变量放在函数 () 内作为参数传入,那么此时必须手动解借用。
            
            
              rust
              
              
            
          
          let x = 5;
let r1 = &x;
let r2 = &r1;
// 方法调用:自动解借用
let abs1 = x.abs();      // 直接调用
let abs2 = r1.abs();     // 自动解借用
let abs3 = r2.abs();     // 自动多重解借用
// 函数调用:需要手动解借用  
let abs4 = i32::abs(x);     // 传值
let abs5 = i32::abs(*r1);   // 手动解借用
let abs6 = i32::abs(**r2);  // 手动多重解借用
println!("方法调用结果: {}, {}, {}", abs1, abs2, abs3);
println!("函数调用结果: {}, {}, {}", abs4, abs5, abs6);以上代码中,用两种不同的方式调用了相同的函数abs,一个需要手动解借用,另一个自动解借用。
关键区别:
- obj.function():编译器自动处理所有层级的解借用
- function(*obj):需要手动解借用确保参数类型匹配
自动解借用是编译器自动完成的,编译器查找方法时的步骤:
            
            
              rust
              
              
            
          
          let s = String::from("hello");
let r = &s;
let rr = &r;
let len = rr.len();
println!("字符串长度: {}", len);代码中,定义了一个二级借用 rr,并调用了 rr.len() 方法。此时编译器会进行尝试:
- rr.len()不存在,- rr这个二级借用没有- len方法
- *rr.len()不存在,- *rr这个一级借用没有- len方法
- **rr.len()存在,- **rr这个- String有- len方法
编译器会按照固定顺序尝试,一层一层解借用,直到找到匹配的方法,如果没有匹配的方法,就会报错。
非常好,这篇文章的结构已经很清晰:从借用规则、生命周期到自动解借用,层层递进。
直接在这套逻辑下补充"切片(slice)"内容------最自然的落脚点,就是放在借用之后,因为切片本质上就是"借用的一部分连续元素"。下面是一个与你现有行文风格、语气、深度完全一致的补充章节。
切片
在前面我们说到,借用是"对数据的引用",而切片(slice)是一种更精细粒度的引用:
它不是借整个值,而是借用值里连续的一部分元素。
切片可以理解为"借用的子集"。
如果
&T借的是整个房子,那&T[start..end]借的只是其中几间房。
Rust 的切片主要有两种:
- 数组切片(&[T])
- 字符串切片(&str)
它们都不拥有数据,只是指向已有数据的视图。
数组切片
任何数组或 Vec<T> 都可以通过 [..] 语法创建切片:
            
            
              rust
              
              
            
          
          let arr = [10, 20, 30, 40, 50];
let part = &arr[1..4];      // 取索引 1, 2, 3
println!("切片内容: {:?}", part); // [20, 30, 40]这里的 &arr[1..4] 会创建类型为 &[i32] 的切片引用,
它借用数组 arr 的一部分,而不是复制数据。
切片始终是左闭右开区间,即 [start, end) 不包含。
上例中索引 4 不会被包含。
& 是借用,[..] 是范围,两者叠加起来,&arr[..] 就是数组的某个范围。
访问切片时,从0下标重新开始访问。
比如说刚才的切片&arr[1..4],它是从下标1开始借用的,这意味着part[0]实际上是arr[1]。
切片的索引方式
切片语法使用 范围表达式(Range),也就是形如 start..end 的语法糖。这实际上是一个范围类型,表示一个左闭右开区间 [start, end)。
常见形式如下:
| 写法 | 含义 | 
|---|---|
| &arr[..] | 借整个数组 | 
| &arr[..3] | 从开头到索引 2 | 
| &arr[2..] | 从索引 2 到末尾 | 
| &arr[1..4] | 从 1 开始到 3 结束 | 
| &mut arr[start..end] | 可变切片,可修改原数组 | 
例如:
            
            
              rust
              
              
            
          
          let mut arr = [1, 2, 3, 4, 5];
let slice = &mut arr[2..4];
slice[0] *= 10;
slice[1] *= 10;
println!("修改后数组: {:?}", arr); // [1, 2, 30, 40, 5]通过 可变切片,你可以修改原数组的一部分数据。
切片会继承借用规则:可变切片独占访问,不可与其他借用共存。
切片与借用规则
切片其实就是一个特殊的借用。
因此,它严格遵循我们之前讲过的全部借用规则:
- 不能同时存在可变切片与不可变借用
- 可以存在多个不可变切片
- 可变切片在使用期间独占数据
- 切片的生命周期必须短于被切的值
来看一个例子,直观地感受"独占借用":
            
            
              rust
              
              
            
          
          let mut arr = [1, 2, 3, 4, 5];
let s1 = &mut arr[1..3];
// let s2 = &arr[2..4]; // 错误:s1 还在作用中,不能再创建重叠借用
s1[0] = 20;
s1[1] = 30;
println!("修改部分切片: {:?}", s1);有人可能有问题,能不能对一个数组的不同部分进行多次可变借用?
可以,但是需要通过特殊方法。
比如这段代码:
            
            
              rust
              
              
            
          
          let mut arr = [1, 2, 3, 4, 5];
let b1 = &mut arr[0..2];
let b2 = &mut arr[3..5];
b1[0] = 10;
b2[1] = 20;第一次借用[0..2],第二次借用[3..5],这是两个完全不重叠的区域,但是编译器拒绝了这段代码。
因为编译器分析不出来这是两个不重叠的区域,它只认为你对一个数组进行了两次可变借用,这违背了借用规则。也许未来编译器更加智能,可以通过编译期的范围重叠分析,至少截止1.90.0版本,依然是不支持的。
但是Rust专门提供了方法,将一个数组拆分为两个可变切片,它就是split_at_mut。
            
            
              rust
              
              
            
          
          let mut arr = [1, 2, 3, 4, 5];
let (b1, b2) = arr.split_at_mut(2);
b1[0] = 10;
b2[1] = 20;使用arr.split_at_mut(2),相当于以2为边界,拿到两个可变切片&mut arr[..2]和&mut arr[2..]。这样就可以通过两个可变切片同时修改一个数组,并且保证不会相互冲突了。
字符串切片
字符串比较特殊,它在内部分配 UTF-8 字节序列。我们可以借用字符串的一部分字节作为字符串切片 &str:
            
            
              rust
              
              
            
          
          let s = String::from("hello world");
let hello = &s[..5];
let world = &s[6..];
println!("切片1: {}", hello);
println!("切片2: {}", world);在这里,&s[..5] 并不是复制前五个字符,而只是借用那一段。因此,hello 和 world 都只是指向原始字符串 s 的不同部分。
特别注意:字符串切片的边界必须落在合法的 UTF-8 字符边界上,否则会在运行时报错。这是 Rust 保证字符正确性的一个设计点。
切片的底层含义
在底层实现上,切片是一个胖指针,它同时保存了:
- 一个指向起始位置的指针
- 一个长度信息
这样 Rust 既能实现安全的边界检查,又无需在运行时拷贝任何内容。
            
            
              rust
              
              
            
          
          let arr = [10, 20, 30];
let s = &arr[..];
println!("切片长度: {}", s.len()); // 3当访问越界时,Rust 会在运行时触发 panic,防止非法内存访问。
再谈类型系统与安全性
在Rust借用体系中,有一个很特别的存在,那就是可变与不可变。
如果你有其它语言的学习经验,其实借用就是其它语言中的引用,当然Rust社区也会把借用叫做引用,只是我个人喜欢借用这个叫法,更能体现它的机制。
在C++、Java等等语言中,凡是引用,都没有去区分可变引用与不可变引用。这个引用能否修改值,完全取决于值本身能不能被修改。
而rust不同,在借用层面,从类型上就把&T和&mut T区分开,进一步在编译期引入借用检查机制,来保证"共享不可变,可变不共享"的核心理念。
你有没有发现,这和之前的never类型有点类似?
这里把一个运行时的共享理念,借助于类型系统&T和&mut T进行区分,随后让编译器在编译期使用类型检查对类型做分析,从而保证一定满足"共享不可变,可变不共享",从而实现安全性。
这是我们第二次谈到这样一条逻辑链:把运行时的逻辑以类型系统为媒介,体现到编译期,从而让编译器可以介入分析,保证安全的同时不影响效率。
画个表格对比:
| 场景 | 运行时逻辑 | 类型系统媒介 | 编译期分析 | 结果 | 
|---|---|---|---|---|
| 循环 | 一个永不退出的循环,只有一条函数返回路径; 表达式返回 panic,还没等到变量接收表达式返回值,程序就结束了; | Never 类型 ! | Never 可以随意转换为任何类型 | 存在 Never 的地方,可以写出很自然的逻辑处理 | 
| 借用 | 共享不可变,保证访问数据时,数据不会被篡改; 可变不共享,保证不会有多个变量同时修改数据; | &T和&mut T | 可以存在多个 &T; 存在&mut T时不能有任何其它借用; | 保证了"共享不可变,可变不共享"的核心机制 | 
后面这张表格会越来越丰富,我们可以看到这个逻辑链条是如何让Rust一步一步成为一个安全高效而优雅的语言的,而且这个过程是零运行时成本的。