Rust所有权

文章目录

  • 什么是所有权
    • Stack vs Heap
    • 所有权规则
    • 变量作用域
    • String类型
    • 内存与分配
    • 所有权与函数
  • 引用与借用
    • 可变引用
    • 悬垂引用
    • 引用的规则
  • 切片
    • 字符串切片
    • 其他类型的切片

什么是所有权

什么是所有权

所有程序在运行时都必须管理其使用计算机内存的方式:

  • 一些语言中具有垃圾回收机制,在程序运行时有规律地寻找不再使用的内存,比如C#和Java。
  • 在另一些语言中,程序员必须自行分配和释放内存,比如C/C++。

而Rust则是通过所有权系统管理内存:

  • 所有权是Rust最独特的特性,它让Rust无需GC就可以保证内存安全,这也是Rust的核心特性。
  • 通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,如果违反了所有权规定,则程序不能通过编译。
  • 在程序运行时,所有权系统不会减慢程序的运行速度,因为所有权规则的检查是在编译时进行的。

Stack vs Heap

在很多语言中,程序员不需要经常考虑到堆和栈,但在Rust这样的系统编程语言中,一个值存储在heap上还是stack上,会很大程度上影响语言的行为,所以这里先对堆和栈进行简单介绍。

分配内存

栈(stack):

  • stack按值的接收顺序来存储,按相反的顺序将它们移除,即后进先出(LIFO)。
  • 所有存储在stack上的数据必须拥有已知的固定大小,添加数据叫做入栈,移除数据叫做出栈。
  • 将数据存放在stack时不需要寻找用来存储数据的空间,因为那个位置永远在stack的顶端。

堆(heap):

  • 在编译时大小未知的数据或运行时大小可能发生变化的数据,必须存放在heap上。
  • 当把数据放入heap时,需要先在heap上分配对应大小的空间,即操作系统在heap中找到一块足够大的空间把它标记为在用,并将该空间的地址返回,后续访问heap上的数据时,需要通过指针来定位。

访问数据

  • 访问heap中的数据比访问stack中的数据慢,因为需要通过指针才能找到heap中的数据,属于间接访问。
  • 对于现代的处理器来说,由于缓存的缘故,如果指令在内存中的跳转的次数越少,那么速度就越快。
  • stack中数据存放的距离比较近,而heap中数据存放的距离比较远,因此访问heap中的数据比访问stack中的数据慢。

所有权存在的原因

所有权存在的原因,就是为了管理存放在heap上的数据:

  • 跟踪代码的哪些部分正在使用heap上的哪些数据。
  • 最小化heap上的重复数据量。
  • 清理heap上未使用的数据,以避免内存泄露。

所有权规则

所有权规则

所有权的规则如下:

  • Rust中的每一个值都有一个对应的变量作为它的所有者。
  • 在同一时间内,每个值有且只有一个所有者。
  • 当所有者离开自己的作用域时,它持有的值就会被释放掉。

变量作用域

变量作用域

  • 作用域(scope)指的是程序中一个项,在程序中的有效范围。

在下面的代码中,变量s从第三行声明开始变得可用,在第五行代码块结束时离开作用域变得不可用。如下:

fn main() {//s不可用let s = "hello"; //s可用//可以对s进行相关操作
} //s作用域到此结束,s不再可用

String类型

String类型

为了后续讲解Rust的所有权,我们需要借助一个管理的数据存储在heap上的类型,这里选择String类型。

  • Rust中基础的标量类型的数据是存储在stack上的,而String类型比这些类型更加复杂,它管理的数据是存储在heap上。
  • String类型管理的数据存储在heap上,因此String类型能够存储在编译时未知大小的文本,即String类型是可变的。
  • Rust中有两种字符串类型,一种是字符串字面值,它是不可变的,另一种就是String类型,其管理的字符串是可变的。

String类型由三部分组成:

  • ptr:指向存放字符串内容的指针。
  • len:表示字符串的长度。
  • capacity:表示字符串的容量。

String类型的这三部分数据存储在stack上,而String管理的字符串则存储在heap上。如下:

在这里插入图片描述

创建String字符串

创建String字符串可以使用from函数,该函数可以基于字符串字面值来创建String字符串。如下:

fn main() {let mut s = String::from("Hello");s.push_str(" String");println!("{}", s); //Hello String
}

说明一下:

  • 代码中的::,表示from是String类型的命名空间下的函数。
  • String类型的push_str方法,可以将指定的字符串插入到String字符串的后面。

内存与分配

内存与分配

  • 对于字符串字面值来说,在编译时就知道它的内容了,其文本内容会直接被硬编码到最终的可执行文件中,因此访问字符串字面值快速且高效。
  • 而String类型为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容,其必须在运行时向内存分配器请求内存,当我们处理完String时再将内存返回给分配器。
注:在Rust中,当某个值离开作用域时,会自动调用drop函数释放内存。

变量与数据交互的方式:移动(Move)

在Rust中,多个变量可以采取不同的方式与同一数据进行交互。如下:

fn main() {let x = 10;let y = x;println!("x = {}", x); //x = 10println!("y = {}", y); //y = 10
}

说明一下:

  • 代码中先将整数字面值10绑定到了变量x,接着生成了变量x的拷贝,并将其绑定到变量y。
  • 因为整数是已知固定大小的简单值,因此x和y都被放入到了栈中,在赋值后两个变量都有效。

如果将代码中的整数换成String,那么运行程序将会产生报错。如下:

fn main() {let x = String::from("hello");let y = x;println!("x = {}", x); //errorprintln!("y = {}", y);
}

报错的原因就是我们借用了已经被移动的值x。如下:

在这里插入图片描述

现在我们来分析一下代码,刚开始声明变量x的时候,整体布局如下:

在这里插入图片描述

当把变量x赋值给变量y时,String的数据被拷贝了一份,但拷贝的仅仅是stack上的String元数据,而并没有拷贝指针所指向的heap上的数据。如下:

在这里插入图片描述

当变量离开作用域时,Rust会自动调用drop函数释放内存,为了避免这种情况下heap上的数据被二次释放,因此Rust会让赋值后的变量x失效,此时当x离开作用域时就不会释放内存。如下:

在这里插入图片描述

这就是为什么在赋值后访问变量x就会产生报错的原因,因为此时变量x已经失效了。

说明一下:

  • stack上的拷贝可以视为浅拷贝,heap上的拷贝可以视为深拷贝。
  • 由于深拷贝的成本比较高,因此Rust不会自动进行数据的深拷贝。

变量与数据交互的方式:克隆(Clone)

如果确实需要对String的heap上的数据进行拷贝,那么可以使用String的clone方法。如下:

fn main() {let x = String::from("hello");let y = x.clone(); //深拷贝println!("x = {}", x); //x = helloprintln!("y = {}", y); //y = hello
}

拷贝后变量x和变量y都是有效的,因为String的clone方法会将stack和heap上的数据都进行拷贝。如下:

在这里插入图片描述

stack上的数据:拷贝(Copy)

  • Copy trait可以用在类似整型这样存储在栈上的类型,如果一个类型实现了Copy trait,那么旧的变量在赋值给其他变量后仍然可用。
  • 任何简单标量的组合类型都可以实现Copy trait,任何不需要分配内存或某种形式资源的类型也都可以实现Copy trait。
  • 所有整数类型、浮点类型、布尔类型、字符类型都实现了Copy,此外,如果元组中所有字段都实现了Copy,那么这个元组也是可Copy的,比如(i32, i32)是可Copy的,而(i32, String)是不可Copy的。

说明一下:

  • 如果一个类型或该类型的一部分实现了Drop trait,那么Rust不允许它再实现Copy trait。
  • 如果一个类型要实现Copy trait,那么该类型也必须实现Clone trait。
  • String赋值后变量会失效,就是因为String没有实现Copy trait,在赋值时会发生移动。

所有权与函数

所有权与函数

将值传递给函数和给变量赋值的原理类似:

  • 对于没有实现Copy trait类型的变量来说,将值传递给函数时会发生移动,调用函数后变量失效。
  • 对于实现了Copy trait类型的变量来说,将值传递给函数时会发生拷贝,调用函数后变量仍然有效。

例如,下面代码中变量s传入函数时将发生移动,后续不再有效,而变量x传入函数时将发生拷贝,后续仍然有效。如下:

fn main() {let s = String::from("hello world");take_ownership(s); //发生移动//println!("s = {}", s); //errorlet x = 10;makes_copy(x); //发生拷贝println!("x = {}", x); //x = 10;
}fn take_ownership(some_string: String) {println!("{}", some_string); //hello world
}fn makes_copy(some_number: i32) {println!("{}", some_number); //10
}

返回值与作用域

函数在返回值的过程中同样会发生所有权的转移。如下:

fn main() {let s1 = gives_ownership();let s2 = String::from("hello");let s3 = takes_and_gives_back(s2);
}fn gives_ownership() -> String {let some_string = String::from("hello");some_string
}fn takes_and_gives_back(a_string: String) -> String {a_string
}

代码说明:

  • gives_ownership函数的作用是,创建了一个String,并将其所有权返回。
  • takes_and_gives_back函数的所用是,取得了一个String的所有权,然后再将其所有权返回。

引用与借用

引用与借用

  • 对于String类型来说,&String就是String类型的引用,我们将创建一个引用的行为称为借用。
  • 一个类型的引用不会取得该类型变量的所有权,因此当引用离开作用域时不会释放对应的空间。

例如,下面代码中的calculate_length函数的参数类型是&String,该函数返回传入String的长度但不获取其所有权,函数调用后传入的String变量仍然有效。如下:

fn main() {let s1 = String::from("hello world");let len = calculate_length(&s1);println!("'{}'的长度是{}", s1, len); //'hello world'的长度是11
}fn calculate_length(s: &String) -> usize {s.len()
}

实际calculate_length函数的参数s就是一个指针,它指向了传入的实参s1。如下:

在这里插入图片描述

说明一下:

  • &引用相反的操作是解引用,解引用运算符是*

可变引用

可变引用

引用和变量一样默认也是不可变的,要让引用变得可变,同样需要使用mut关键字。如下:

fn main() {let mut s1 = String::from("hello world");let len = calculate_length(&mut s1);println!("'{}'的长度是{}", s1, len); //'hello world!!!'的长度是14
}fn calculate_length(s: &mut String) -> usize {s.push_str("!!!"); //修改了引用的变量s.len()
}

但可变引用有一个重要的限制就是,在特定作用域内,一个变量只能有一个可变引用,否则会产生报错。如下:

fn main() {let mut s = String::from("hello world");let s1 = &mut s;let s2 = &mut s; //errorprintln!("s1 = {}, s2 = {}", s1, s2);
}

Rust这样做可以在编译时就防止数据竞争,但可以通过创建新的作用域来允许非同时的创建多个可变引用,因为只要保证同一个作用域下一个变量只有一个可变引用即可。如下:

fn main() {let mut s = String::from("hello world");{let s1 = &mut s;}let s2 = &mut s;
}

可变引用的其他限制

Rust中不允许一个变量同时拥有可变引用和不可变引用,否则会产生报错。如下:

fn main() {let mut s = String::from("hello world");let r1 = &s;let r2 = &s;let s1 = &mut s; //errorprintln!("{} {} {}", r1, r2, s1);
}

原因: 不可变引用的要求其引用的值不能发生改变,而可变引用却可以改变其引用的值,因此一个变量同时拥有可变引用和不可变引用,就是的不可变引用的作用失效了,但一个变量同时拥有多个不可变引用是可以的。

悬垂引用

悬垂引用(Dangling References)

悬垂引用指的是,一个指针引用了内存中的某个地址,但这块内存可能已经释放了。如下:

fn main() {let r = dangle();
}fn dangle() -> &String {let s = String::from("hello world");&s //悬垂引用
}

在Rust中编译器确保引用永远不会变成悬垂状态,因为编译器会确保数据不会在其引用之前离开作用域,因此上述代码会编译报错。如下:

在这里插入图片描述

说明一下: 报错内容说缺少一个生命周期说明符,生命周期相关的内容会在后续博客中讲解。

引用的规则

引用的规则

引用的规则如下:

  • 在任何时刻,一个变量只能有一个可变引用,或任意数量的不可变引用。
  • 引用必须一直有效。

切片

字符串切片

字符串切片

  • 除了引用之外,Rust还有另一种不持有所有权的数据类型,叫做切片(slice)。
  • 字符串切片就是指向字符串中一部分值的引用,切片形式为:&字符串变量名[开始索引..结束索引]
  • 字符串切片中的开始索引指的是切片起始位置的索引值,结束索引指的是切片终止位置的下一个索引值。
  • 如果切片的起始位置是0,那么开始索引可以省略,如果切片的终止位置是字符串的长度,那么结束索引可以省略。
  • 字符串切片的类型是&str,字符串切片是不可变的。

例如,下面分别创建了字符串hello worldhello的切片和world的切片。如下:

fn main() {let s = String::from("hello world");let hello = &s[0..5];let world = &s[6..11];println!("hello = {}", hello); //hello = helloprintln!("world = {}", world); //world = world
}

切片中包含一个指针和一个长度,比如上述的world切片,其指针指向字符串索引为6的位置,其长度就是5。如下:

在这里插入图片描述

切片在Rust中是非常有用的,比如获取字符串中的第一个单词,那么借助字符串切片可以编写出如下代码:

fn main() {let s = String::from("hello world");let word = first_word(&s);//s.clear(); //error: s已经存在一个不可变引用println!("word = {}", word); //word = hello
}fn first_word(s: &String) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

说明一下:

  • 如果不使用字符串切片,也可以通过返回字符串中第一个空格的索引来间接表示字符串中第一个单词的位置,但此时这个索引是独立于这个字符串存在的,当字符串中的内容被清除后这个索引就没有意义了。
  • 而切片是不可变的,因此如果一个字符串存在一个切片,那么在这个切片没有离开作用域之前,这个字符串中的内容是无法被修改的,因为Rust不允许一个变量同时拥有可变引用和不可变引用,否则会产生报错。
  • as_bytes方法的作用是将String转化为字节数组,以方便遍历String的每一个字节来与空格进行比较。
  • iter方法的作用是在字节数组上创建一个迭代器,它将会返回字节数组中的每一个元素,而enumerate方法的作用是对iter的结果进行包装,将这些元素作为元组的一部分来返回。enumerate返回的元组中,第一个元素是索引,第二个元素是集合中元素的引用。
  • 在for循环中,通过模式对enumerate返回的元组进行解构,由于元组中第二个元素是集合中元素的引用,因此item需要使用&

注意:

  • 字符串切片的范围索引必须发生在有效的UTF-8字符边界内。
  • 如果尝试从一个多字节的字符中创建字符串切片,程序会报错并退出。

字符串字面值就是切片

字符串字面值的类型实际上就是字符串切片&str,这就是为什么字符串字面值不可变的原因,因为字符串切片&str就是不可变的。如下:

fn main() {let s = "hello world"; //s的类型是&str
}

将字符串切片作为参数

如果要将字符串切片作为函数的参数,那么最好将函数的参数类型定义为&str,而不是&String,这样就能同时接收&String和&str的参数了,能够使我们的API更加通用且不会损失任何功能。如下:

fn main() {let my_string = String::from("hello world");let word = first_word(&my_string); //接收&Stringlet my_string_literal = "hello world";let word = first_word(my_string_literal); //接收&str
}fn first_word(s: &str) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

说明一下:

  • &String等价于整个String的切片,因此可以用&str接收,而字符串字面值的类型本来就是&str。

其他类型的切片

其他类型的切片

与字符串切片类似,其他类型也可以有切片,比如对于下面代码中的数组来说,其切片类型就是&[i32]。如下:

fn main() {let a = [1, 2, 3, 4, 5];let slice = &a[1..3]; //&[i32]类型
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/117357.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Mr.Alright---MTK安卓13 抬手亮屏功能的逻辑

该功能在系统设置-显示-拿起设备时唤醒 alps\vendor\mediatek\proprietary\packages\apps\MtkSettings\src\com\android\settings\display\LiftToWakePreferenceController.javapublic boolean isAvailable() {SensorManager sensors (SensorManager) mContext.getSystemServ…

分库分表-ShardingSphere 4.x(2)

❤️作者简介:2022新星计划第三季云原生与云计算赛道Top5🏅、华为云享专家🏅、云原生领域潜力新星🏅 💛博客首页:C站个人主页🌞 💗作者目的:如有错误请指正,将…

小知识(6) el-table表格选中行和回显行(vue3)

el-table表格选中行和回显行 官方文档说明 https://element-plus.org/zh-CN/component/table.html#table-%E6%96%B9%E6%B3%95 环境&#xff1a;vue3element-plus 选中行selection <el-table ref"baseTableRef" row-key"id" border :selection"tr…

SpringBoot整合XXL-JOB详解

❤️作者简介&#xff1a;2022新星计划第三季云原生与云计算赛道Top5&#x1f3c5;、华为云享专家&#x1f3c5;、云原生领域潜力新星&#x1f3c5; &#x1f49b;博客首页&#xff1a;C站个人主页&#x1f31e; &#x1f497;作者目的&#xff1a;如有错误请指正&#xff0c;将…

12.4 组播鼠标批量执行

组播模式相比单播模式可以提高网络的效率和带宽利用率&#xff0c;因为组播数据包只需要发送一次&#xff0c;就可以被多个接收者接收&#xff0c;而不需要每个接收者都单独发送一份数据包。这在需要同时向多个接收者发送相同数据的场景下特别有用&#xff0c;如视频会议、在线…

矢量图形编辑软件 illustrator 2023 mac 中文软件特点

illustrator 2023 mac是一款矢量图形编辑软件&#xff0c;用于创建和编辑排版、图标、标志、插图和其他类型的矢量图形。 illustrator 2023 mac软件特点 矢量图形&#xff1a;illustrator创建的图形是矢量图形&#xff0c;可以无限放大而不失真&#xff0c;这与像素图形编辑软…

Ubuntu - sudo apt update 报错源问题解决方案

sudo apt update 报错…lease’ does not have a Release file. 反正就是觉得是网络的问题 尝试添加国内清华源、阿里源 不行 尝试DNS 为8.8.8.8&#xff0c;114.114.114.114 还是不行 解决方案&#xff1a;设置里面让 Ubuntu 找到适合自己的源 1、Settings -> About…

分布式限流:Redis

目录 1:如何实现分布式限流 2:限流的几种类别 2.1:固定窗口限流 2.2:滑动窗口限流 2.3:漏桶限流 2.4:令牌桶限流 3:实现分布式限流:Redis 3.1:引入Redisson的依赖包 3.2:初始化Redisson 3.3:创建Redisson的限流类 1:如何实现分布式限流 1:把统计用户的使用频率等这些…

Spark简单回顾

星光下的赶路人star的个人主页 大鹏一日同风起&#xff0c;扶摇直上九万里 文章目录 1、Spark1.1 Spark入门1.1.1 Spark部署模式1.1.2 常用端口 1.2 SparkCore1.2.1 RDD不可变和五大属性1.2.2 RDD的弹性1.2.3 cache和Checkpoint的区别1.2.4 算子 1.3 SparkSQL1.4 内核1.4.1提交…

lv8 嵌入式开发-网络编程开发 20 域名解析与http服务实现原理

目录 1 域名解析 2 如何实现万维网服务器&#xff1f; 2.1 HTTP 的操作过程 2.2 万维网服务器实现 1 域名解析 域名解析gethostbyname函数 主机结构在 <netdb.h> 中定义如下&#xff1a; struct hostent {char *h_name; /* 官方域名 */char **h_alias…

hadoop伪分布式安装部署

首先jdk安装完毕 jdk安装文档参考&#xff1a; Linux 环境下安装JDK1.8并配置环境变量_linux安装jdk1.8并配置环境变量_Xi-Yuan的博客-CSDN博客 准备好hadoop的安装包 我的下载地址如下&#xff1a; We Transfer Gratuit. Envoi scuris de gros fichiers. 将hadoop包上传到随…

Redis数据类型——hash类型的概念及操作

1.hash类型介绍 可以理解为redis中的一个小型redis 2.v 3.hash数据类型的扩展操作 4.hash类型数据操作的注意事项

WPF中的绑定知识详解(含案例源码分享)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

C/C++新冠疫情死亡率 2020年9月电子学会青少年软件编程(C/C++)等级考试一级真题答案解析

目录 C/C新冠疫情死亡率 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 C/C新冠疫情死亡率 2020年9月 C/C编程等级考试一级编程题 一、题目要求 1、编程实现 2020年全世界爆发了新冠疫情&#xff0c;请…

基于定容积法标准容器容积标定中的电动针阀自动化解决方案

摘要&#xff1a;在目前的六氟化硫气体精密计量中普遍采用重量法和定容法两种技术&#xff0c;本文分析了重量法中存在的问题以及定容法的优势&#xff0c;同时也指出定容法在实际应用中还存在自动化水平较低的问题。为了提高定容法精密计量过程中的自动化水平&#xff0c;本文…

睿趣科技:抖音小店申请流程

随着移动互联网的发展&#xff0c;越来越多的人开始尝试通过开设网店来创业。抖音作为国内最受欢迎的短视频平台之一&#xff0c;也推出了自己的电商功能——抖音小店。那么&#xff0c;如何申请抖音小店呢?下面就为大家详细介绍一下抖音小店的申请流程。 首先&#xff0c;打开…

docker环境,ubuntu18.04安装VTK8.2和PCL1.9.1

下载源码和依赖库 首先下载源码VTK8.2: Download | VTK 下载PCL1.9.1链接&#xff1a;Releases PointCloudLibrary/pcl GitHub 下载好了以后&#xff0c;先安装PCL依赖 sudo apt-get update sudo apt-get install git build-essential linux-libc-dev sudo apt-get instal…

算法通关村第十一关白银挑战——位运算符的高频算法题

大家好&#xff0c;我是怒码少年小码。 今天讲讲几个位运算的经典算法。 位移的妙用 1. 位1的个数 LeetCode 191&#xff1a;编写一个函数&#xff0c;输入是一个无符号整数&#xff08;以二进制串的形式&#xff09;&#xff0c;返回其二进制表达式中数字位数为 ‘1’ 的个…

Spring Boot集成RESTful API

在Spring Boot中集成一个RESTful API是我们在实际开发中较为常见的一种开发任务&#xff0c;以下通过一个小的案例来展示在Spring Boot中创建RESTful API来编写一个单元测试。 本节使用到的注解&#xff1a; Controller&#xff1a;修饰class&#xff0c;用来创建处理http请求的…

互联网Java工程师面试题·Spring篇·第四弹

目录 6、AOP 6.1、什么是 AOP&#xff1f; 6.2、什么是 Aspect&#xff1f; 6.3、什么是切点&#xff08;JoinPoint&#xff09; 6.4、什么是通知&#xff08;Advice&#xff09;&#xff1f; 6.5、有哪些类型的通知&#xff08;Advice&#xff09;&#xff1f; 6.6、指出…