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 = 10
    println!("y = {}", y); //y = 10
}

说明一下:

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

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

fn main() {
    let x = String::from("hello");
    let y = x;

    println!("x = {}", x); //error
    println!("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 = hello
    println!("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); //error

    let 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; //error

    println!("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; //error

    println!("{} {} {}", 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 = hello
    println!("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); //接收&String

    let 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]类型
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go 包操作之如何拉取私有的Go Module

Go 包操作之如何拉取私有的Go Module 在前面,我们已经了解了GO 项目依赖包管理与Go Module常规操作,Go Module 构建模式已经成为了 Go 语言的依赖管理与构建的标准。 在平时使用Go Module 时候,可能会遇到以下问题: 在某 modul…

如何使用 PostgreSQL 进行数据迁移和整合?

​ PostgreSQL 是一个强大的开源关系型数据库管理系统,它提供了丰富的功能和灵活性,使其成为许多企业和开发者的首选数据库之一。在开发过程中,经常会遇到需要将数据从一个数据库迁移到另一个数据库,或者整合多个数据源的情况。…

webGL编程指南 第四章 旋转+平移.TanslatedRotatdTriangle

我会持续更新关于wegl的编程指南中的代码。 当前的代码不会使用书中的缩写,每一步都是会展开写。希望能给后来学习的一些帮助 git代码地址 :git 本篇文章将把旋转和平位移结合起来,因为矩阵的不存在交换法则 文章中设计的矩阵地址在这里​…

Go 实现插入排序算法及优化

插入排序 插入排序是一种简单的排序算法,以数组为例,我们可以把数组看成是多个数组组成。插入排序的基本思想是往前面已排好序的数组中插入一个元素,组成一个新的数组,此数组依然有序。光看文字可能不理解,让我们看看…

【vue3】状态过渡-GSAP插件实现

效果图&#xff1a; 实现代码 安装库&#xff1a;npm install --save-dev gsap 引入&#xff1a;import gsap from gsap <template><div><el-input v-model"num.currNum" type"number" step"20" style"width: 120px;"…

算法训练 第四周

一、二分查找 本题给我们提供了一个有n个元素的升序整形数组nums和一个目标值target&#xff0c;要求我们找到target在nums数组中的位置&#xff0c;并返回下标&#xff0c;如果不存在目标值则返回-1。nums中的所有元素不重复&#xff0c;n将在[1&#xff0c;10000]之间&#x…

基于C/C++的UG二次开发流程

文章目录 基于C/C的UG二次开发流程1 环境搭建1.1 新建工程1.2 项目属性设置1.3 添加入口函数并生成dll文件1.4 执行程序1.5 ufsta入口1.5.1 创建程序部署目录结构1.5.2 创建菜单文件1.5.3 设置系统环境变量1.5.4 制作对话框1.5.5 创建代码1.5.6 部署和执行 基于C/C的UG二次开发…

基于MIMO+16QAM系统的VBLAST译码算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ........................................................................ for SNR_dBSNRS…

火山引擎 LAS Spark 升级:揭秘 Bucket 优化技术

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 文章介绍了 Bucket 优化技术及其在实际业务中的应用&#xff0c;包括 Spark Bucket 的基本原理&#xff0c;重点阐述了火山引擎湖仓一体分析服务 LAS&#xff08;下…

vue3 elementPlus 表格实现行列拖拽及列检索功能

1、安装vuedraggable npm i -S vuedraggablenext 2、完整代码 <template> <div classcontainer><div class"dragbox"><el-table row-key"id" :data"tableData" :border"true"><el-table-columnv-for"…

8.2 矢量图层点要素单一符号使用一

文章目录 前言单一符号&#xff08;Single symbol&#xff09;渲染简单标记(Simple Marker)QGis代码实现 SVG标记&#xff08;SVG marker&#xff09;QGis代码实现 总结 前言 上一篇教程对矢量图层符号化做了一个整体介绍&#xff0c;并以点图层为例介绍了可以使用的渲染器&am…

【SwiftUI模块】0060、SwiftUI基于Firebase搭建一个类似InstagramApp 3/7部分-搭建TabBar

SwiftUI模块系列 - 已更新60篇 SwiftUI项目 - 已更新5个项目 往期Demo源码下载 技术:SwiftUI、SwiftUI4.0、Instagram、Firebase 运行环境: SwiftUI4.0 Xcode14 MacOS12.6 iPhone Simulator iPhone 14 Pro Max SwiftUI基于Firebase搭建一个类似InstagramApp 3/7部分-搭建Tab…

ubuntu安装golang

看版本&#xff1a;https://go.dev/dl/ 下载&#xff1a; wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz卸载已有的go&#xff0c;可以apt remove go&#xff0c;也可以which go之后删除那个go文件&#xff0c;然后&#xff1a; rm -rf /usr/local/go && tar…

在 Python 中使用 Pillow 进行图像处理【3/4】

第三部分 一、腐蚀和膨胀 您可以查看名为 的图像文件dot_and_hole.jpg&#xff0c;您可以从本教程链接的存储库中下载该文件&#xff1a; 该二值图像的左侧显示黑色背景上的白点&#xff0c;而右侧显示纯白色部分中的黑洞。 侵蚀是从图像边界去除白色像素的过程。您可以通过使用…

如何创建前端绘图和图表?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

【excel技巧】excel单元格内如何换行?

Excel表格&#xff0c;在制作完成之后&#xff0c;在输入数据的时候&#xff0c;总是会遇到内容长度太长导致无法全部显示或者破坏表格整体格式。几天分享4个单元格换行的方法给大家。 方法一&#xff1a; 首先我们先介绍一个&#xff0c;通过调整列宽的方式来达到显示全部内…

使用Python的Flask框架开发验证码登录功能

目录 一、安装和配置Flask 二、生成验证码 三、处理用户输入和验证验证码 四、实现安全的用户认证 五、创建HTML模板 总结 验证码登录功能是现代Web应用程序中常见的安全特性之一&#xff0c;它有助于防止自动化机器人或恶意用户进行非法登录。在本文中&#xff0c;我们将…

hadoop伪分布式安装部署

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

华为云 CodeArts Snap 智能编程助手 PyCharm 插件安装与使用指南

1 插件安装下载 1.1 搜索插件 打开 PyCharm&#xff0c;选择 File&#xff0c;点击 Settings。 选择 Plugins&#xff0c;点击 Marketplace&#xff0c;并在搜索框中输入 Huawei Cloud CodeArts Snap。 1.2 安装插件 如上图所示&#xff0c;点击 Install 按钮安装 Huawei Cl…

Azure - 机器学习企业级服务概述与介绍

目录 一、什么是 Azure 机器学习&#xff1f;大规模生成业务关键型机器学习模型 二、Azure 机器学习适合哪些人群&#xff1f;三、Azure 机器学习的价值点加快价值实现速度协作并简化 MLOps信心十足地开发负责任地设计 四、端到端机器学习生命周期的支持准备数据生成和训练模型…