【百例RUST - 006】一文理解所有权和切片
前言:
虽然之前的版本中,介绍了 所有权和切片的内容,
但是描述可能并不算特别完整,重新介绍一下 所有权和切片的内容。帮助我们理解。
前面的文章地址 点我直达
一、深浅拷贝模型
1、模型图
在很多的编程语言中,都存在这样的 浅拷贝和深拷贝 模型图,对于 RUST所有权的介绍,需要从这里开始。

2、两者区别
生活案例
场景:你租用了一套精装修的公寓(这是原始对象)房间里面所有的家具、电器都在里面。
事件:你的好朋友也要住进来。
1、浅拷贝(只配了一把 "大门钥匙")
A.操作: 你带朋友去配了一把大门钥匙
B.发生什么: 你们俩现在一人一把钥匙, 但是都是同一个房间
C.修改后果: 某天你朋友觉得客厅的沙发(内存中的数据)太丑,私自换成了绿色,当你下班回家推开门一看,发现沙发也变成了绿色的。
D.内存逻辑: 内存里面没有增加新的"房间", 只是多了一个指向现有地址的指针。
E.总结: 共享空间, 一人闯祸, 全家遭罪
2、深拷贝(直接 "克隆一间房")
A.操作: 你是超级富豪, 你不仅仅给朋友配了钥匙, 还直接在隔壁楼买了一套一模一样的精装修公寓,连里面的沙发,电视品牌和摆放位置都完全照抄。
B.发生什么: 内存里现在有两个完全独立的房间,虽然刚开始他们长得一模一样。
C.修改后果: 你朋友在他那间房,把沙发换成绿色的,甚至把墙拆了,你回家推开自己的门,发现你家还是原来的样子,舒适的旧沙发还在。
D.内存逻辑: 这种操作会在内存中重新申请一块空间,并且把原对象内部的所有东西(包括嵌套的小对象)全部复制一份新的放进去。
E.总结: 独立门户,互不干扰,你的就是你的
核心对比
| 维度 | 浅拷贝(Shallow Copy) | 深拷贝(Deep Copy) |
|---|---|---|
| 操作本质 | 复制了进入权限(地址、指针) | 复制了物理实体(整个对象) |
| 空间变化 | 没开新房,只是多发了把钥匙 | 重新盖了一栋楼,占用了新地皮 |
| 修改影响 | 连坐制:修改 PN1,PN2 也会跟着变化 | 自治制:改了 PM1,PM2纹丝不动 |
| 适用场景 | 仅仅想给对象起个 "别名" 时 | 需要完全隔离数据,防止原始数据被破坏时 |
通俗结论
- 浅拷贝 就是 "影分身",真身挨一拳,影子也跟着疼
- 深拷贝 就是 "克隆人",克隆人感冒了,真身依然活蹦乱跳
3、理解两个案例
案例01
正常的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1); // 正常输出了 s1 = hello
}
错误的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1);
// 追加字符串
// 下面运行报错了!
// 原因: s1是不可变变量, 报错
s1.push_str("world");
println!("s1 = {}", s1);
}
修正的场景
rust
fn main() {
let mut s1 = String::from("hello");
println!("s1 = {}", s1); // 正常输出了 s1 = hello
// 追加字符串
// 需要在前面定义为可变的变量, let mut s1 = String::from("hello");
s1.push_str("world");
println!("s1 = {}", s1); // 正常输出了 s1 = helloworld
}
结论
如果想要改变之前定义的数据,那么需要加上 mut 关键字。
案例02
正常的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1); // 正常输出了 s1 = hello
// 将 s1的地址赋值给了 s2 ---> 浅拷贝模型
let s2 = s1;
println!("s2 = {}", s2); // 正常输出了 s2 = hello
}
错误的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1);
// 将 s1的地址赋值给了 s2 ---> 浅拷贝模型
let s2 = s1;
// 这里报错了
// 原因: 只是拷贝了地址信息。(地址对应数据值未做复制)
println!("s1 = {}", s1);
println!("s2 = {}", s2);
}
修正的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1); // 正常输出了 hello
// 将 s1 克隆给了 s2 ---> 深拷贝模型
let s2 = s1.clone();
println!("s1 = {}", s1); // 正常输出了 hello
println!("s2 = {}", s2); // 正常输出了 hello
}
结论
如果一个不可变的变量,将地址值赋值给了另外一个变量,后续想要继续使用先前的变量,则会出现错误(原因:地址值的浅拷贝)。
如果想要解决这个错误,应该采用深拷贝,克隆的方式,将所有值进行克隆操作,才能继续使用先前的变量。
二、所有权介绍
第01节 描述说明
一句话解释
Rust 的所有权(Ownership)就是一套 "内存管理规则"
什么规则呢?
任何数据在同一时刻只能有一个负责人,负责人不在了,数据就地销毁。
三条硬性规定:
1、第一条:
规则: 每个内存空间, 只能拥有一个 "主人"
例如: 一本书(值)在同一时间只能被一个人(变量)拥有
2、第二条:
规则: 独占性(所有权转移) 当你把一个变量赋给另一个变量时,所有权就转交了。
例如: 原先的那个人就不再拥有这本书,也看不了书了。这在 Rust 里叫 Move(移动)。
原因: 为什么这么做? 防止两个人同时觉得自己拥有这本书,最后在还书(释放内存)时发生争抢。
3、第三条:
规则: 出局即销毁 当所有者(变量)离开它所在的代码块(花括号 { })时,它所拥有的值会被立刻自动销毁。
例如: 这就好比你离开了图书馆,必须把手里的书放回书架,不能带走。
为什么 rust 会弄得这么麻烦呢?
因为其他语言出现过弊端, 对比一下其他语言:
1、C/C++: 像搬家,你自己决定什么时候扔家具。忘了扔,屋子就满了(内存泄漏);扔早了,还要进去住就会出事(野指针)。
2、Java/Python: 请了个保洁(垃圾回收 GC),定期帮你查哪些家具不要了。虽然省心,但保洁干活时会占用你的私人时间(程序卡顿)。
针对于上述弊端, Rust的如何处理呢?
Rust 的方案: 谁搬进来的家具谁负责。当你离开房间时,家具自动消失。既不需要你手动扔,也不需要保洁。
说到这里,我不难想到,为什么 rust 存在性能高?
或许正是因为 rust 拥有所有权的概念,不存在 内存泄露 和 程序卡顿的问题。
可能是因为这个原因,导致了 RUST 所有权比较复杂,学习难度相对而言比其他的语言更高。
此时此刻,我想回头,看看前面的案例。为什么这里会出现错误呢?
错误的场景
rust
fn main() {
let s1 = String::from("hello");
println!("s1 = {}", s1);
// 将 s1的地址赋值给了 s2 ---> 浅拷贝模型(在 RUST 当中,就是出现了 MOVE 移动, 将 "hello"内存的所有权从s1交给了s2)
let s2 = s1;
// 这里报错了
// 原因: 只是拷贝了地址信息。---> 这里还想要继续通过 s1 去使用 "hello" 的内存所有权, 就出现错误!因为 "hello"所有权属于s2了
println!("s1 = {}", s1);
println!("s2 = {}", s2);
}
看看上面的几条铁律,不难发现一件事情,当我们出现了移动 MOVE 的时候,将 s1 的所有权交给了 s2 以后。(铁律第二条)
在交出了所有权之后,在内存中, s1 失去了所有权,那么 s1 会被销毁掉!
此时此刻 s1 还想继续使用就会出错,因为他已经失去了(对之前定义在内存区域的 "hello" )所有权。
现在,对于 rust 所有权来说:
讲的应该是 内存空间 什么时候,被回收的问题。
1、某块内存空间开辟出来之后,只会属于一个变量持有。
2、变量正常情况下,会在大括号的末尾被直接回收。(其他语言不同,如 C/C++ 析构函数 Java 垃圾回收等)
3、如果出现了 MOVE 所有权的转交,那么之前持有所有权的变量,直接被回收,内存空间,由新的持有所有权的变量持有。
新的变量如果出现了销毁,内存立即回收。
新的变量如果出现了MOVE所有权转交,那么 内存空间,则交给转交之后的变量。
生活实例:
1、张三 买了一套别墅,房产证上面写 张三的名字,这套别墅,只会被 张三持有。
2、正常情况下,如果 张三 寿终正寝之后,张三被清理,不留任何东西在世界上,别墅立即被清理掉,夷为平地。
3、如果张三将这套别墅 MOVE所有权给 李四,房产证上面写 李四的名字,李四拥有这套别墅。张三会被立即清理掉,抹杀不留任何东西在世界上。
4、正常情况下,如果 李四 寿终正寝之后,李四被清理,不留任何东西在世界上,别墅立即被清理掉,夷为平地。
5、如果李四将这套别墅 MOVE所有权给 王五,房产证上面写 王五的名字,王五拥有这套别墅。李四会被立即清理掉,抹杀不留任何东西在世界上。
针对于整个生态平衡系统(内存)来说:谁有所有权,谁存在于世间,没有所有权,则彻底抹杀。空间管理更为严苛。
第02节 栈内存Copy
针对于栈内存,对于上述的MOVE 方式有所不同,不会提前结束
案例
rust
fn main() {
let num1 = 30;
println!("num1 = {}", num1); // num1 = 30
let num2 = num1;
println!("num1 = {}", num1); // num1 = 30
println!("num2 = {}", num2); // num2 = 30
}
存在于栈上面的数据,内存中具有 copy trait 的特征,会等待大括号完毕的时候,才会被销毁掉。
哪些数据存在这种现象呢?
1、所有的整型
2、浮点型
3、布尔值
4、字符类型 char
5、元组
第03节 函数中的所有权
正常的场景
rust
fn main() {
let s = String::from("hello");
method_ownership(s);
let num = 30;
method_copy(num);
}
fn method_ownership(some: String){
println!("{}", some); // 正常输出 hello
}
fn method_copy(number: i32){
println!("{}", number); // 正常输出 30
}
错误的场景
rust
fn main() {
let s = String::from("hello");
method_ownership(s);
println!("{}", s); // 这里报错了!所有权已经转移 MOVE了
let num = 30;
method_copy(num);
println!("{}", num);
}
fn method_ownership(some: String){
println!("{}", some);
}
fn method_copy(number: i32){
println!("{}", number);
}
修正的场景
rust
fn main() {
let s = String::from("hello");
let s2 = method_ownership(s);
println!("{}", s2); // 正常输出 hello
let num = 30;
method_copy(num);
println!("{}", num); // 正常输出 30
}
fn method_ownership(some: String) -> String{
println!("{}", some); // 正常输出 hello
some
}
fn method_copy(number: i32){
println!("{}", number); // 正常输出 30
}
结论
在函数当中,变量作为方法的参数传入的时候,所有权发生了 MOVE 转移,如果后续继续使用该变量,会出现错误。
想要修复这个问题,可以将所有权重新传回来,交付给新的变量,使用新的变量。
第04节 引用
案例
正常的场景
rust
fn main() {
let s = String::from("hello");
let size = calcute_length(s);
println!("{}", size); // 正常输出 5
}
// 用于计算字符串的长度
fn calcute_length(some: String) -> usize{
some.len()
}
错误的场景
rust
fn main() {
let s = String::from("hello");
let size = calcute_length(s);
println!("{}", size);
println!("{}", s); // 报错了!所有权已经转移 MOVE了, 交给了 calcute_length 函数
}
// 用于计算字符串的长度
fn calcute_length(some: String) -> usize{
some.len()
}
我们是否要编写出前面所写的代码,将当前的字符串返回呢?
当然可以的,但是每次这种写法,会较为复杂,我只是作为一个简单的操作,每次都需要将原本的字符串返回,代码编写就会很复杂。
使用元组的方式,返回两个值
rust
fn main() {
let s = String::from("hello");
let tup = calcute_length(s);
println!("{}", tup.0); // 正常输出 5
println!("{}", tup.1); // 正常输出 hello
}
// 用于计算字符串的长度
fn calcute_length(some: String) -> (usize, String){
(some.len(), some)
}
这种方式,虽然能够解决问题,但是每次都需要返回多余的值,将简单的函数调用,书写的较为复杂。
我们可以采用引用的方式完成这个功能。 引用(&)
修正的场景
rust
fn main() {
let s = String::from("hello");
let size = calcute_length(&s);
println!("{}", size); // 正常输出了 5
println!("{}", s); // 正常输出了 hello
}
// 用于计算字符串的长度
fn calcute_length(some: &String) -> usize{
some.len()
}
说明
引用的基础用法,就是添加上 & 符号。
给我们创建一个指向值的引用,但是不会拥有它,因为不会拥有这个值,所以,当引用离开了作用域之后,也不会被丢弃。
生活实例:
1、张三 买了一套别墅,房产证上面写 张三的名字,这套别墅,只会被 张三持有。
2、张三现在没有住,准备将别墅出租,但是张三因为长期在外地,自己不方便带 其他租客去看房。
3、张三就配置了一把临时的钥匙,将这个钥匙,交给了 中介(小明)由小明带着租客去看房。
4、此时此刻,中介小明拥有打开别墅的权利,能够看到房子里面的装修家具等,但是 小明和准备租房的看客,没有改造别墅的权利。

第05节 借用
"借用" borrow 借用可以理解为 "可变的引用"
案例1
在上述案例中,我们说当前的引用,如果 函数内部出现调整的情况下,是不正确的。
错误的场景
rust
fn main() {
let s = String::from("hello");
let size = calcute_length(&s);
println!("{}", size);
println!("{}", s);
}
// 用于计算字符串的长度
fn calcute_length(some: &String) -> usize{
some.push_str("world"); // 这里修改了引用的内容!出错啦
some.len()
}
修正的场景
在三处位置,添加了 mut 变成了借用。
rust
fn main() {
let mut s = String::from("hello");
let size = calcute_length(&mut s);
println!("{}", size); // 正常输出了 5
println!("{}", s); // 正常输出了 helloworld
}
// 用于计算字符串的长度
fn calcute_length(some: &mut String) -> usize{
some.push_str("world");
some.len()
}
说明1
借用的基础用法,就是添加上 &mut 符号。
生活实例:
1、张三 买了一套别墅,房产证上面写 张三的名字,这套别墅,现在会被 张三持有。
2、张三将别墅抵押给了李四 20年,房产证上面写 张三的名字。
3、此时此刻,这套别墅,虽然房产证上表述的是 张三持有,但是李四可以对别墅进行修改。
案例2
正常的场景
rust
fn main() {
let mut s = String::from("hello");
let size = calcute_length(&mut s);
println!("{}", size); // 正常输出了 5
println!("{}", s); // 正常输出了 helloworld
let new_s = &mut s;
println!("{}", new_s); // 正常输出了 helloworld
}
// 用于计算字符串的长度
fn calcute_length(some: &mut String) -> usize{
some.push_str("world");
some.len()
}
错误的场景
rust
fn main() {
let mut s = String::from("hello");
let size = calcute_length(&mut s);
println!("{}", size);
println!("{}", s);
let new_s = &mut s;
println!("{}", s); // 这里报错了! 因为s已经被借用给了 new_s
println!("{}", new_s);
}
// 用于计算字符串的长度
fn calcute_length(some: &mut String) -> usize{
some.push_str("world");
some.len()
}
修正的场景
rust
fn main() {
let mut s = String::from("hello");
let size = calcute_length(&mut s);
println!("{}", size); // 正常输出了 5
println!("{}", s); // 正常输出了 helloworld
let new_s = &mut s;
println!("{}", new_s); // 正常输出了 helloworld
println!("{}", s); // 正常输出了 helloworld
}
// 用于计算字符串的长度
fn calcute_length(some: &mut String) -> usize{
some.push_str("world");
some.len()
}
说明2
借用的基础用法,就是添加上 &mut 符号。
对于 Rust 的 "借用的终点"
借用的终点在哪里?
在 Rust 中,一个引用(借用)的生命周期 不再是从它定义开始到大括号结束,而是从它定义开始,到它最后一次被使用的地方结束。
在借用未结束前,使用引用将会报错!
上述报错的原因说明
1、报错的代码是因为你试图在借用期间使用原变量;
2、不报错的代码是因为借用在原变量被使用前就已经提前结束了。
结论
Rust 的 "借用规则" (可变借用)
在 &mut借用期间,其他的引用都不可以读写
生活实例:
1、张三 买了一套别墅,房产证上面写 张三的名字,这套别墅,现在会被 张三持有。
2、张三 将别墅抵押(可变借用)给了 田七 20年,房产证上面写 张三的名字。
3、此时此刻,这套别墅,虽然房产证上表述的是 张三持有,但是 田七 可以对别墅进行修改。
4、需要注意的事情是 20年期间内,田七 可以对别墅进行修改。张三在此期间,无法操作别墅。
5、当20年期限已过,则 田七 无法操作别墅内容,张三可以操作别墅了。
第06节 悬垂引用
悬垂引用(Dangling References) 是指针的一种安全隐患,在很多底层语言(如 C/C++)中非常常见。
简单来说,悬垂引用是指:一个引用(指针)指向了内存中的某个地址,但那个地址原本存放的数据已经被释放或销毁了。
想象一下:你有一把钥匙能打开某个房间,但那个房间已经被拆迁了,如果你还试着用钥匙去开门,就会发生严重的错误。
错误的场景
rust
fn main() {
let ref_s = dangle();
}
// 出现 悬垂引用的情况
fn dangle() -> &String {
let s = String::from("hello");
&s
}
代码解释:
1、我们的 s 变量,他的作用于是在 dangle 的函数内部 {} 内部
2、但是将 s 变量的引用返回给了 外面的 ref_s
3、变量 s 在离开作用域之后, 立即被销毁掉了。
4、但是此时此刻的引用 ref_s 还存在, 指向的是一个被销毁的地方
这里就会导致严重的错误!!!
错误: consider using the `'static` lifetime, but this is uncommon unless ...
图示

三、切片
切片 slice
案例01
字符串的切片
rust
fn main() {
let s = String::from("hello world");
// 从0到5 包括0,不包括5
let value1 = &s[0..5];
println!("value1 = {}", value1); // value1 = hello
// 从0开始, 包括0包括4
let value2 = &s[0..=4];
println!("value2 = {}", value2); // value2 = hello
// 从0开始, 包括0包括4
let value3 = &s[..=4];
println!("value3 = {}", value3); // value3 = hello
// 从6到11 包括6,不包括11
let value4 = &s[6..11];
println!("value4 = {}", value4); // value4 = world
// 从6到10 包括6,包括10
let value5 = &s[6..=10];
println!("value5 = {}", value5); // value5 = world
// 从6到结束 包括6
let value6 = &s[6..];
println!("value6 = {}", value6); // value6 = world
// 整个字符串
let value7 = &s[..];
println!("value7 = {}", value7); // value7 = hello world
}
案例02
超过范围,会怎么样
rust
fn main() {
let s = String::from("hello world");
// 从10到15 包括10,不包括15
let value = &s[10..15];
println!("value = {}", value);
// 报错 byte index 15 is out of bounds of `hello world`
}
案例03
不正常的截取,会怎样
rust
fn main() {
let s = String::from("你好");
// 从3到5 包括3,不包括5
let value = &s[3..5];
println!("value = {}", value);
// 报错 byte index 5 is not a char boundary; it is inside '好' (bytes 3..6) of `你好`
}
案例04
其他类型的切片,如数组
rust
fn main() {
let arr = [1,2,3,4,5];
// 从索引0到索引2 包括索引0,不包括索引2
let value1 = &arr[0..2];
print_array(&value1); // 正常输出 1,2
// 从索引0开始, 包括索引0包括索引2
let value2 = &arr[0..=2];
print_array(&value2); // 正常输出 1,2,3
// 从索引0开始, 包括索引0包括索引2
let value3 = &arr[..=2];
print_array(&value3); // 正常输出 1,2,3
// 整个数组
let value4 = &arr[..];
print_array(&value4); // 正常输出 1,2,3,4,5
}
// 定义函数, 输出数组的内容
fn print_array(array: &[i32]){
for element in array {
println!("{}", element);
}
}