当前位置: 首页 > news >正文

Rust:借用 切片

Rust:借用 & 切片

    • 借用
      • &T 不可变借用
      • &mut T 可变借用
    • 借用规则
      • Non-Lexical Lifetimes (NLL) 优化
    • 解借用
      • * 操作符
      • 多级借用
      • 自动解借用
    • 切片
      • 数组切片
      • 切片的索引方式
      • 切片与借用规则
      • 字符串切片
      • 切片的底层含义
    • 再谈类型系统与安全性


借用

在C++中,传递参数时我们经常需要在值传递、指针传递、引用传递之间选择。Rust的借用系统在编译期就解决了很多C++运行时才能发现的内存安全问题。

&T 不可变借用

不可变借用允许你读取数据,但不能通过借用修改数据。

语法:

let 借用: &类型 = &变量;

示例:

let s: String = String::from("hello");
let r: &String = &s;println!("原始字符串: {}", s);
println!("借用内容: {}", r);

不可变借用不会获取所有权,原始变量仍然有效。通过&操作符创建借用,借用只是指向数据的指针,相当于一个别名,你可以同时通过rc访问这个字符串。

不可变借用不能修改数据

尝试通过不可变借用修改数据会导致编译错误:

let mut s = String::from("hello");
let r = &s;r.push_str(", world"); // 编译错误!

这个限制确保了数据的不变性。即使原始变量是mut的,通过不可变借用也无法修改数据,这是Rust类型系统的核心安全保证。

多个不可变借用可以同时存在

可以同时创建多个不可变借用:

let s = String::from("hello");let r1 = &s;
let r2 = &s;
let r3 = &s;println!("{}, {}, {}", r1, r2, r3);

多个不可变借用是安全的,因为它们都只是读取数据,不会造成数据竞争。读取操作本身是线程安全的。


&mut T 可变借用

可变借用允许你通过借用修改数据。

语法:

let 借用: &mut 类型 = &mut 变量;

示例:

let mut s = String::from("hello");
let r: &mut String = &mut s;r.push_str(", world");
println!("修改后: {}", r);

可变借用在其生命周期内独占访问权。注意原始变量必须声明为mut才能创建可变借用。

可变借用存在时原始变量不可访问

当可变借用存在时,不能同时使用原始变量:

let mut s = String::from("hello");
let r = &mut s;println!("{}", s); // 编译错误!
r.push_str(", world");

以上代码中,r 获取了字符串的可变借用,同时用户使用了原始变量 s,编译器不允许这种行为,可变借用使用期间,独占所有权

这个限制防止了数据竞争。如果允许同时通过原始变量和可变借用访问数据,可能导致数据不一致。


借用规则

Rust的借用规则是其内存安全的核心,这些规则在编译期强制执行,防止数据竞争和内存安全问题。

规则1:同一时间只能有一个可变借用

正确的使用方式:

let mut s = String::from("hello");
let r1 = &mut s;r1.push_str(", world");
println!("{}", r1);

违反规则的错误示例:

let mut s = String::from("hello");let r1 = &mut s;
let r2 = &mut s; // 编译错误println!("{}, {}", r1, r2);

这个规则防止了数据竞争。如果允许多个可变借用同时存在,可能导致一个借用修改数据时,另一个借用读取到不一致的状态。

规则2:可变借用与不可变借用不能同时存在

正确的使用方式:

let mut s = String::from("hello");let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);

违反规则的错误示例:

let mut s = String::from("hello");let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 编译错误!println!("{}, {}, {}", r1, r2, r3);

这个规则防止不可变借用的数据被意外修改。如果允许可变借用和不可变借用同时存在,不可变借用持有者期望数据不变,但可变借用可能会修改数据,导致不可变借用看到意外的数据变化。

规则3:借用的生命周期不能超过其借用的数据

正确的生命周期管理:

let mut s = String::from("hello");{let r1 = &s;println!("{}", r1);
} // r1 在这里超出作用域let r2 = &mut s;
r2.push_str(", world");
println!("{}", r2);

违反生命周期规则的错误示例:

let r;
{let x = 5;r = &x; // 编译错误:x 的生命周期不够长
}
println!("{}", r);

借用必须始终指向有效的内存。这个规则防止了悬垂借用,确保借用永远不会指向已经被释放的内存。


Non-Lexical Lifetimes (NLL) 优化

Rust 2018引入了更智能的借用检查,能够分析借用的实际使用范围:

let mut s = String::from("hello");let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这里不再被使用,生命周期结束let r3 = &mut s; // 现在可以创建可变借用
println!("{}", r3);

编译器能够分析出r1r2println!之后不再使用,所以允许后续创建可变借用。这比简单的词法作用域更智能,提高了代码的灵活性。


解借用

* 操作符

使用*操作符可以访问借用指向的值:

let x = 5;
let y = &x;println!("x = {}", x);
println!("*y = {}", *y);

解借用操作符*获取借用指向的实际值,这是最基础的解借用方式。

借用和值是不同的类型,不能直接比较

let x = 5;
let y = &x;assert_eq!(5, y); // 编译错误:不能比较i32和&i32
assert_eq!(5, *y);   // 正确:解借用后比较

必须通过解借用将借用转换为值才能进行操作。


多级借用

可以创建指向借用的借用:

let x = 5;
let r1 = &x;      // r1: &i32
let r2 = &r1;     // r2: &&i32  
let r3 = &r2;     // r3: &&&i32println!("x = {}", x);
println!("*r1 = {}", *r1);      // 5
println!("**r2 = {}", **r2);    // 5  
println!("***r3 = {}", ***r3);  // 5

每增加一层借用,就需要增加一个*来解借用


自动解借用

  • 方法调用

在使用.调用方法时,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()

方法调用会自动解借用到找到对应方法为止

  • 字段访问

访问结构体字段时也会自动解借用:

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).yprintln!("二级借用: ({}, {})", r2.x, r2.y);        // 等价于 (**r2).x, (**r2).y
}

如果把变量放在函数 () 内作为参数传入,那么此时必须手动解借用

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):需要手动解借用确保参数类型匹配

自动解借用是编译器自动完成的,编译器查找方法时的步骤:

let s = String::from("hello");
let r = &s;
let rr = &r;let len = rr.len();
println!("字符串长度: {}", len);

代码中,定义了一个二级借用 rr,并调用了 rr.len() 方法。此时编译器会进行尝试:

  1. rr.len() 不存在,rr 这个二级借用没有 len 方法
  2. *rr.len() 不存在,*rr 这个一级借用没有 len 方法
  3. **rr.len() 存在,**rr 这个Stringlen 方法

编译器会按照固定顺序尝试,一层一层解借用,直到找到匹配的方法,如果没有匹配的方法,就会报错


非常好,这篇文章的结构已经很清晰:从借用规则、生命周期到自动解借用,层层递进。
直接在这套逻辑下补充“切片(slice)”内容——最自然的落脚点,就是放在借用之后,因为切片本质上就是“借用的一部分连续元素”。下面是一个与你现有行文风格、语气、深度完全一致的补充章节。


切片

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

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

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

Rust 的切片主要有两种:

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

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


数组切片

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


切片的索引方式

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

常见形式如下:

写法含义
&arr[..]借整个数组
&arr[..3]从开头到索引 2
&arr[2..]从索引 2 到末尾
&arr[1..4]从 1 开始到 3 结束
&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]

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

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


切片与借用规则

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

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

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

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

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


字符串切片

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

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


切片的底层含义

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

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

这样 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一步一步成为一个安全高效而优雅的语言的,而且这个过程是零运行时成本的。


http://www.dtcms.com/a/548982.html

相关文章:

  • 【Blender工具】
  • Spring Al学习6:嵌入模型 API
  • 坪山区住房和建设局网站wordpress能放视频
  • 网站承建商有哪些注册了一个域名怎么做网站
  • 我公司是帮企业做网站的_现在要帮客户们的网站备案微信公众营销平台开发
  • MPC模型预测控制:原理、设计与MATLAB实现
  • JavaEE初阶,网络编程篇
  • 基于中值滤波和高斯平滑的三维点云数据滤波matlab仿真
  • Java设计模式应用--装饰器模式
  • 【MATLAB例程】基于梯度检测自适应的互补滤波 vs 标准互补滤波,附MATLAB代码下载链接,可直接运行,方便学习和修改成自己想要的程序
  • 在检验铸铁平台精度使用三研法检验有哪些好处
  • 用Blender制作室内效果图宜居之地
  • blender4.5 使用外部IDE(pycharm)编辑脚本(bpy)实践指南
  • 计算机的一点基础知识
  • 广州网站建设 乐云seo国外优秀论文网站
  • CSS 图像拼合技术
  • 【C++】模板进阶 | 继承
  • 排名优化网站建设长沙网站建设优化
  • 厦门网站优化服务pyhton做网站
  • 论文阅读笔记——数据增强
  • 如何裁剪YOLOv8m的大目标检测头并验证其结构
  • 扩展阅读:目标检测(Object Detection)标注
  • MR30分布式IO:破局锂电池制造产线,引领高效生产新变革
  • AI赋能科研创新:ChatGPT-4o与DeepSeek-R1在学术研究中的深度应用指南
  • 《数据库系统》SQL语言之分组查询与分组过滤(理论理解分析+实例练习)
  • 家乡介绍网页设计海口seo网络推广
  • 【ROS2】动作服务器:rclcpp_action::Client 详解
  • 红松APP首秀北京老博会,“有温度的科技”赋能退休兴趣生活
  • 【ZEGO即构开发者日报】Soul AI Lab开源播客语音合成模型;腾讯混元推出国内首个交互式AI播客;ChatGPT Go向用户免费开放一年......
  • 数据库基础-数据库的三级模式