Skip to content

slice切片

在前面我们说到,借用是“对数据的引用”,而切片(slice)是一种更精细粒度的引用

切片语法

rust
&var[start..end]
  • var:切片的原变量名
  • start:切片开始的索引,包含此索引值
  • end:切片结束的索引,不包含此索引值

它不是借整个值,而是借用值里连续的一部分元素。

  • 切片可以理解为“借用的子集”。
  • 如果 &T 借的是整个房子,那 &T[start..end] 借的只是其中几间房。

Rust 的切片主要有两种:

  • 数组切片(&[T]
  • 字符串切片(&str

它们都不拥有数据,只是指向已有数据的视图

1.数组切片

任何数组或 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]

2.切片的索引方式

切片语法使用 范围表达式(Range),也就是形如 start..end 的语法糖。这实际上是一个范围类型,表示一个左闭右开区间 [start, end)

常见形式如下:

写法含义
&arr[..]借整个数组
&arr[..end]从开头到索引 end
&arr[start..]从索引 start 到末尾
&arr[start..end]从 start 开始到 end 结束,左闭右开[start, end)
&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]

通过 可变切片,你可以修改原数组的一部分数据。

切片会继承借用规则:可变切片独占访问,不可与其他借用共存

3.切片与借用规则

切片其实就是一个特殊的借用。

因此,它严格遵循我们之前讲过的全部借用规则:

  • 不能同时存在可变切片与不可变借用
  • 可以存在多个不可变切片
  • 可变切片在使用期间独占数据
  • 切片的生命周期必须短于被切的值

来看一个例子,直观地感受“独占借用”:

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..]。这样就可以通过两个可变切片同时修改一个数组,并且保证不会相互冲突了。

4.字符串切片

字符串比较特殊,它在内部分配 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] 并不是复制前五个字符,而只是借用那一段。因此,helloworld 都只是指向原始字符串 s 的不同部分。

特别注意:字符串切片的边界必须落在合法的 UTF-8 字符边界上,否则会在运行时报错。这是 Rust 保证字符正确性的一个设计点。

5.切片的底层含义

在底层实现上,切片是一个胖指针,它同时保存了:

  1. 一个指向起始位置的指针
  2. 一个长度信息

这样 Rust 既能实现安全的边界检查,又无需在运行时拷贝任何内容。

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

MIT Licensed