【百例RUST - 006】一文理解所有权和切片

【百例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);
    }
}
相关推荐
Westward-sun.2 小时前
PyQt5入门实战:从零实现一个表达式输入式计算器(附完整代码)
开发语言·qt
喂_balabala2 小时前
Kotlin-属性委托
android·开发语言·kotlin
dashizhi20152 小时前
如何禁止外来设备连接内网wifi、禁止外来电脑接入单位局域网?
开发语言·网络·php
不想写代码的星星2 小时前
类型萃取:重生之我在幼儿园修炼类型学
开发语言·c++
csbysj20202 小时前
C++ 接口(抽象类)
开发语言
香香甜甜的辣椒炒肉2 小时前
Spring JDBC 万能模板
java·后端·spring
常利兵2 小时前
从0到1:Spring Boot 中WebSocket实战揭秘,开启实时通信新时代
spring boot·后端·websocket
m0_694845572 小时前
VoxCPM部署教程:构建AI语音交互系统
服务器·人工智能·后端·自动化
Rust研习社2 小时前
Rust 是如何判断对象是否相等的?一起来聊一聊 PartialEq 与 Eq
后端·rust·编程语言