slice切片
在前面我们说到,借用是“对数据的引用”,而切片(slice)是一种更精细粒度的引用:
切片语法
&var[start..end]- var:切片的原变量名
- start:切片开始的索引,包含此索引值
- end:切片结束的索引,不包含此索引值
它不是借整个值,而是借用值里连续的一部分元素。
- 切片可以理解为“借用的子集”。
- 如果
&T借的是整个房子,那&T[start..end]借的只是其中几间房。
Rust 的切片主要有两种:
- 数组切片(
&[T]) - 字符串切片(
&str)
它们都不拥有数据,只是指向已有数据的视图。
1.数组切片
任何数组或 Vec<T> 都可以通过 [..] 语法创建切片:
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]。
2.切片的索引方式
切片语法使用 范围表达式(Range),也就是形如 start..end 的语法糖。这实际上是一个范围类型,表示一个左闭右开区间 [start, end)。
常见形式如下:
| 写法 | 含义 |
|---|---|
&arr[..] | 借整个数组 |
&arr[..end] | 从开头到索引 end |
&arr[start..] | 从索引 start 到末尾 |
&arr[start..end] | 从 start 开始到 end 结束,左闭右开[start, end) |
&mut arr[start..end] | 可变切片,可修改原数组 |
例如:
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]通过 可变切片,你可以修改原数组的一部分数据。
切片会继承借用规则:可变切片独占访问,不可与其他借用共存。
3.切片与借用规则
切片其实就是一个特殊的借用。
因此,它严格遵循我们之前讲过的全部借用规则:
- 不能同时存在可变切片与不可变借用
- 可以存在多个不可变切片
- 可变切片在使用期间独占数据
- 切片的生命周期必须短于被切的值
来看一个例子,直观地感受“独占借用”:
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);有人可能有问题,能不能对一个数组的不同部分进行多次可变借用?
可以,但是需要通过特殊方法。
比如这段代码:
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。
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..]。这样就可以通过两个可变切片同时修改一个数组,并且保证不会相互冲突了。
4.字符串切片
字符串比较特殊,它在内部分配 UTF-8 字节序列。我们可以借用字符串的一部分字节作为字符串切片 &str:
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 保证字符正确性的一个设计点。
5.切片的底层含义
在底层实现上,切片是一个胖指针,它同时保存了:
- 一个指向起始位置的指针
- 一个长度信息
这样 Rust 既能实现安全的边界检查,又无需在运行时拷贝任何内容。
let arr = [10, 20, 30];
let s = &arr[..];
println!("切片长度: {}", s.len()); // 3当访问越界时,Rust 会在运行时触发 panic,防止非法内存访问。
6.再谈类型系统与安全性
在Rust借用体系中,有一个很特别的存在,那就是可变与不可变。
如果你有其它语言的学习经验,其实借用就是其它语言中的引用
在C++、Java等等语言中,凡是引用,都没有去区分可变引用与不可变引用。这个引用能否修改值,完全取决于值本身能不能被修改。
而rust不同,在借用层面,从类型上就把&T和&mut T区分开,进一步在编译期引入借用检查机制,来保证“共享不可变,可变不共享”的核心理念。
你有没有发现,这和之前的never类型有点类似?
这里把一个运行时的共享理念,借助于类型系统&T和&mut T进行区分,随后让编译器在编译期使用类型检查对类型做分析,从而保证一定满足“共享不可变,可变不共享”,从而实现安全性。
这是我们第二次谈到这样一条逻辑链:把运行时的逻辑以类型系统为媒介,体现到编译期,从而让编译器可以介入分析,保证安全的同时不影响效率。
画个表格对比:
| 场景 | 运行时逻辑 | 类型系统媒介 | 编译期分析 | 结果 |
|---|---|---|---|---|
| 循环 | 一个永不退出的循环,只有一条函数返回路径; 表达式返回 panic,还没等到变量接收表达式返回值,程序就结束了; | Never 类型 ! | Never 可以随意转换为任何类型 | 存在 Never 的地方,可以写出很自然的逻辑处理 |
| 借用 | 共享不可变,保证访问数据时,数据不会被篡改; 可变不共享,保证不会有多个变量同时修改数据; | &T 和 &mut T | 可以存在多个&T; 存在&mut T时不能有任何其它借用; | 保证了“共享不可变,可变不共享”的核心机制 |
后面这张表格会越来越丰富,我们可以看到这个逻辑链条是如何让Rust一步一步成为一个安全高效而优雅的语言的,而且这个过程是零运行时成本的。
Index和IndexMut
Range
