【Rust】所有权

news2024/12/26 22:26:08

文章目录

  • 所有权
  • stack与heap
  • 所有权存在的原因
  • 所有权规则
  • 变量作用域
  • String类型
  • 内存和分配
  • 变量与数据交互的方式
    • 1.Move
    • 2.Clone
    • 3.Copy
  • 所有权与函数
  • 返回值与作用域
  • 引用
  • 借用
  • 可变引用
  • 悬空引用Dangling References
  • 引用的规则
  • 切片
  • 字符串切片
  • 将字符串切片作为参数传递
  • 其他类型的切片

所有权

所有权是Rust最独特的特性,它让Rust无需GC(Garbage Collection)就可保证内存安全。Rust的核心特性就是所有权,所有程序在运行时都必须管理它们使用计算机内存的方式。有些语言有垃圾回收机制,在程序运行时会不断地寻找不再使用的内存。在其他语言中,程序员必须显式地分配和释放内存。

Rust采用了第三种方式,内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则。当程序运行时,所有权特性不会减慢程序的运行速度。

stack与heap

在像Rust这样的系统级编程语言里,一个值在stack上还是在heap上对语言的行为和你为什么要做某些决定是有更大的影响的。在你的代码运行的时候,stack和heap都是你可用的内存,但是它们的结构很不相同。

  • stack按值的接收顺序来存储,按相反的顺序将它们移除(后进先出,LIFO),添加数据叫压入栈,移除数据叫做弹出栈。把值压到stack上不叫分配。因为指针是固定大小的,可以把指针存放在stack上。
  • 所有存储在stack上的数据必须拥有已知的固定的大小。编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在heap上。
  • Heap 内存组织性差一些,一当你把数据放入heap时,你会请求一定数量的空间,操作系统在heap里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址。这个过程叫做在heap上进行分配,有时仅仅称为“分配”。
  • 入栈比在堆上分配内存要快,因为(入栈时)分配器无需为存储新数据去搜索内存空间;其位置总是在栈顶。相比之下,在堆上分配内存则需要更多的工作,这是因为分配器必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
  • 访问heap 中的数据要比访问stack 中的数据慢,因为需要通过指针才能找到heap中的数据。对于现代的处理器来说,由于缓存的缘故,如果指令在内存中跳转的次数越少,那么速度就越快。
  • 如果数据存放的距离比较近,那么处理器的处理速度就会更快一些(stack 上)。如果数据之间的距离比较远,那么处理速度就会慢一些(heap 上)。在heap上分配大量的空间也是需要时间的。
  • 当你的代码调用函数时,值被传入到函数(也包括指向 heap 的指针)。函数本地的变量被压到stack 上。当函数结束后,这些值会从stack 上弹出。

所有权存在的原因

所有权解决的问题:跟踪代码的哪些部分正在使用heap 的哪些数据;最小化 heap 上的重复数据量;清理heap上未使用的数据以避免空间不足。一旦懂了所有权,就不需要经常去想stack或heap了,但是知道管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作。

所有权规则

  • 每个值都有一个变量,这个变量是该值的所有者。
  • 每个值同时只能有一个所有者。
  • 当所有者超出作用域(scope)时,该值将被删除。

变量作用域

Scope就是程序中一个项目的有效范围。

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

String类型

String比那些基础标量数据类型更加复杂。字符串字面值:程序里手写的那些字符串值,它们是不可变的。Rust还有第二种字符串类型:String。在heap上分配,能够存储在编译时未知数量的文本。

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

为什么String类型的值可以修改,而字符串字面值却不能修改,因为它们处理内存的方式不同。

内存和分配

字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件里,速度快、高效。是因为其不可变性。

String类型为了支持可变性,需要在heap上分配内存来保存编译时未知的文本内容:操作系统必须在运行时来请求内存,这步通过调用String::from来实现。当用完 String之后,需要使用某种方式将内存返回给操作系统。这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存。没有GC,就需要我们去识别内存何时不再使用,并调用代码将它返回。―如果忘了,那就浪费内存;如果提前做了,变量就会非法;如果做了两次,也是 Bug。必须一次分配对应一次释放。

Rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统。Rust会在变量超出作用域时调用一个特殊的函数drop释放其内存。

变量与数据交互的方式

1.Move

多个变量可以与同一个数据使用一种独特的方式来交互。

let x = 5;
let y = x;

整数是已知固定大小的简单的值,这两个5被压到了stack中。

let s1 = String::from("hello");
let s2 = s1;

一个String由3部分组成:一个指向存放字符串内容的指针,一个长度,一个容量。这些存放在stack中,存放字符串内容的部分在heap上,长度len,就是存放字符串内容所需的字节数。容量capacity是指String从操作系统欧冠总共获得内存的字节数。

在这里插入图片描述

当把s1赋给s2,String的数据被赋值了一份,在stack上复制了一份指针、长度、容量,并没有复制指针所指向的heap上的数据。当变量离开作用域时,Rust会自动调用drop 函数,并将变量使用的heap内存释放。当s1、s2离开作用域时,它们都会尝试释放相同的内存,这就是二次释放(double free)bug。

为了保证内存安全,Rust并没有尝试复制被分配的内存,而是选择让s1失效,当s1离开作用域的时候,Rust不需要释放任何东西。

在这里插入图片描述

如果你在其他语言中听说过术语浅拷贝(shallow copy)和深拷贝(deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为Rust同时使第一个变量无效了,这个操作被称为移动(move),而不是叫做浅拷贝。隐含的设计原则:Rust不会自动创建数据的深拷贝,就运行时性能而言,任何自动赋值的操作都是廉价的。

2.Clone

如果真想对heap上面的String数据进行深度拷贝,而不仅仅是stack上的数据,可以使用clone方法。

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

在这里插入图片描述

3.Copy

let x = 5;
let y = x;

这段代码似乎与我们刚刚学到的内容相矛盾:没有调用 clone,不过 x 依然有效且没有被移动到 y 中。

原因是像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量y后使x无效。换句话说,这里没有深浅拷贝的区别,所以这里调用 clone 并不会与通常的浅拷贝有什么不同。

Rust提供了Copy trait,可以用于像整数这样完全存放在stack上面的类型。如果一个类型实现了Copy这个trait,那么旧的变量在赋值后仍然可用;如果一个类型或者该类型的一部分实现了Drop trait,那么,Rust不允许让它再去实现Copy trait了。

一些拥有Copy trait的类型:任何简单标量的组合类型都可以是Copy的,任何需要分配内存或某种资源的都不是Copy的。

  • 所有的整数类型,例如u32
  • bool
  • char
  • 所有的浮点类型,例如f64
  • Tuple(元组),如果其所有的字段都是Copy的

所有权与函数

在语义上,将值传递给函数和把值赋给变量是类似的,将值传递给函数将发生移动或复制。

fn main() {
    let mut s = String::from("Hello,World");

    take_ownership(s);//s 被移动 不再有效

    let x = 5;

    makes_copy(x);//复制

    println!("x:{}",x);
}

fn take_ownership(some_string: String){
    println!("{}",some_string);
}

fn makes_copy(some_number: i32){
    println!("{}",some_number);
}

返回值与作用域

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

fn main() {
    let s1 = gives_ownship();gives_ownership 将返回值转移给s1

    let s2 = String::from("hello");

    let s3 = takes_and_give_back(s2);//s2 被移动到takes_and_gives_back 中,它也将返回值移给 s3
}

fn gives_ownship()->String{
    let some_string = String::from("hello");
    some_string
}

fn takes_and_give_back(a_string:String)->String{
    a_string
}

一个变量的所有权总是遵循同样的模式:把一个值赋给其它变量时就会发生移动。当一个包含heap数据的变量离开作用域时,它的值就会被drop函数清理,除非数据的所有权移动到另一个变量上了。

如何让函数使用某个值,但不获得其所有权?

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

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); 

    (s, length)
}

但是这传进来传出去很麻烦,Rust有一个特性,叫做引用(references)。

引用

参数的类型是&String而不是String,&符号就表示引用:允许你引用某些值而不取的其所有权。

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

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

在这里插入图片描述

借用

我们把引用作为函数参数这个行为叫做借用。不可以修改借用的变量。和变量一样,引用默认也是不可变的。

可变引用

fn main() {
    let mut s1 = String::from("Hello");

    let len = calculate_length(&mut s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &mut String) -> usize {
    s.push_str(",World");
    s.len()
} 

可变引用有一个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用。这样的好处是可在编译时防止数据竞争。以下三种行为会发生数据竞争,两个或多个指针同时访问一个数据,至少有一个指针用于写入数据,没有使用任何机制来同步对数据的访问。我们可以创建新的作用域,来允许非同时的创建多个可变引用。

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

另一个限制是不可以同时拥有一个可变引用和一个不变的引用。多个不可变的引用是可以的。

悬空引用Dangling References

悬空指针(Dangling Pointer):一个指针引用了内存中的某个地址,而这块内存可能己经释放并分配给其它人使用了。

在Rust里,编译器可保证引用永远都不是悬空引用:
如果你引用了某些数据,编译器将保证在引用离开作用域之前数据不会离开作用域。

引用的规则

在任何给定的时刻,只能满足下列条件之一:

  • 一个可变的引用
  • 任意数量不可变的引用引用必须一直有效

引用必须一直有效。

切片

Rust的另外一种不持有所有权的数据类型:切片(slice)。

编写一个函数,该函数接收一个用空格分隔单词的字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应该返回整个字符串。

fn main() {
    let mut s = String::from("Hello world");
    let wordIndex = first_word(&s);

    s.clear();
    println!("{}", wordIndex);
}

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    s.len()
}

这个程序编译时没有任何错误,但是wordIndex与s状态完全没有联系。s被清空后wordIndex仍返回s传给函数时状态的值。Rust为这种情况提供了解决方案。字符串切片。

字符串切片

字符串切片是指向字符串中一部分内容的引用。形式:

[开始索引..结束索引]

开始索引是切片起始位置的索引值,结束索引是切片终止位置的所有值。

let s = String::from("Hello World");

let hello = &s[0..5];
let world = &s[6..11];

let hello2 = &s[..5];
let world2 = &s[6..];

在这里插入图片描述

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

重写firstworld:


fn main() {
    let mut s = String::from("Hello World");

    let word = first_word(&s);

    //s.clear(); // 错误!
    println!("the first word is: {}", word);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

字符串字面值是切片,字符串字面值被直接存储在二进制程序中。

将字符串切片作为参数传递

有经验的Rust开发者会采用&str作为参数类型,因为这样就可以同时接收String和&str类型的参数了:

fn first_word(s: &str) -> &str {

使用字符串切片直接调用该函数,使用String可以创建一个完整的String切片来调用该函数。

定义函数时使用字符串切片来代替字符串引用会使我们的API更加通用,且不会损失任何功能。

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

    let word = first_word(&s);

    let mut s2 = "hello world";

    let word2 = first_word(s2);
    //s.clear(); // 错误!
    println!("the first word of s is: {}", word);
    println!("the first word of s2 is: {}", word2);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

其他类型的切片

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

它跟字符串 slice 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。

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

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

相关文章

1.2 matlab信号分析编程基础

1.画正弦波 xlinspace(0,2*pi,100); ysin(x); plot(x,y);2.画方波 xlinspace(0,4*pi,100); ysquare(x); plot(x,y);3.矩阵赋值 4.标准函数 5.画图 title:标题 grid:开关网格线 叠加多条曲线 3.GUI画正弦波 Fs 44100; dt 1.0/Fs; T 1; N T/dt…

Acer宏碁笔记本电脑Aspire蜂鸟FUN S50-51原装Windows10系统镜像,恢复出厂系统

Acer宏碁笔记本电脑Aspire蜂鸟S50-51原厂Win10系统工厂模式恢复原装出厂OEM系统 系统自带所有驱动、办公软件、出厂主题壁纸LOGO、 Acer Care Center、Quick Access等预装程序 所需工具:32G或以上的U盘(非必需) 文件格式:多个I…

Java垃圾搜集算法和垃圾回收算法

垃圾回收主要针对的是JVM的堆内存,分为新生代和老年代。 按照以前的叫法,还有一个永久代,它在方法区里保存了class信息、静态变量、常量池等。 从jdk-1.8开始,方法区的实现发生了一些变化,取消了永久代的概念&#xff…

墨迹api实现天气预测

文章目录 需求背景解决效果接口地址index.vueweather.vue图标文件 视频效果 需求背景 使用墨迹天气api实现天气预报&#xff0c;空气质量预报功能 解决效果 接口地址 墨迹天气 index.vue <template><div class"dqhjjc-wrap"><div class"fir…

pytorch快速入门中文——03

神经网络 原文&#xff1a;https://pytorch.org/tutorials/beginner/blitz/neural_networks_tutorial.html#sphx-glr-beginner-blitz-neural-networks-tutorial-py 可以使用torch.nn包构建神经网络。 现在您已经了解了autograd&#xff0c;nn依赖于autograd来定义模型并对其进…

win10系统打开程序和功能的几种方式介绍

一&#xff0c;简介 在工作中常常会用到安装和卸载软件的功能&#xff0c;需要打开“程序和功能”&#xff0c;本文主要介绍如几种打开“程序和功能”的方法&#xff0c;供参考。 二&#xff0c;四种方法介绍 四种方法分别是&#xff1a; 从控制面板打开&#xff1b;通过运…

VsCode尝试在目标目录创建文件时发生一个错误

桌面右击vscode图标以管理员身份运行就可以了 结束语&#xff1a; 希望这篇文章能帮助到大家&#xff0c;如有不对之处&#xff0c;还请指正。愿我们一起成长。

按unicode值比较数组中的字符串元素numpy.compare_chararrays()方法

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 对比两个数组中对应位置 的元素的unicode值大小 numpy.compare_chararrays() [太阳]选择题 关于以下代码的输出结果是? import numpy as np a np.array(["a","B","…

联邦聚合(FedAvg、FedProx、SCAFFOLD)

目录 联邦聚合算法对比(FedAvg、FedProx、SCAFFOLD) 解决问题 FedAvg FedProx SCAFFOLD 实验结果 联邦聚合算法对比(FedAvg、FedProx、SCAFFOLD) 论文链接&#xff1a; FedAvg&#xff1a;Communication-Efficient Learning of Deep Networks from Decentralized Data …

在线预览文件

当我们前端小伙伴在码代码的时候&#xff0c;总会不约而同地遇到一个问题&#xff1a;上传文件。一旦文件成功上传&#xff0c;后端就会慷慨地给我们一个下载地址&#xff0c;这是怎么实现在线预览或者直接下载呢&#xff1f;fliencn 是后端给的地址这种是另外起一个标签页预览…

【Android Framework系列】第3章 Zygote进程相关

1 Zygote简介 Zygote是Android中最重要的一个进程&#xff0c;Zygote进程和Init进程、SystemServer进程是Android最重要的三大进程。Zygote是Android系统创建新进程的核心进程&#xff0c;负责启动Dalvik虚拟机&#xff0c;加载一些必要的系统资源和系统类&#xff0c;启动sys…

DevExpress WinForms日程/日历组件,可轻松创建信息管理解决方案!(一)

通过DevExpress WinForms完全可定制的Scheduler和Calendar组件&#xff0c;可以轻松地提供Outlook启发的调度/信息管理解决方案&#xff0c;Scheduler控件提供了多个日历视图选项(日、周、月、时间轴等)&#xff0c;并包括一个全面的内置和完全可定制的编辑表单集合。 PS&…

项目经理专用-项目周报模板-yyyymmdd-大型公司可用项目

作为一个项目经理或者子项目经理&#xff0c;肯定要面对各种文档&#xff0c;尤其是各种 *了狗的周报月报&#xff0c;有的甚至有日报&#xff0c;不厌其烦。 这个时候&#xff0c;一套规整的周报模板就可以省我们太多事&#xff0c;而且看起来也很专业。 今天分享一套自己用…

5.7.2 UDP协议格式(一)——UDP差错控制

5.7.2 UDP协议格式&#xff08;一&#xff09;——UDP差错控制 前面我们学习了UDP数据报格式&#xff08;5.7.1 UDP概述&#xff09;我们知道UDP只是在IP数据报服务基础上增加了端口的复用和分用功能&#xff0c;以及差错控制的功能&#xff0c;这里我们就一起来学习一下UDP的…

破局之作:首部开源 AIGC 软件工程应用电子书《构筑大语言模型应用:应用开发与架构设计》...

TL;DR 版&#xff1b; 在线&#xff1a;https://aigc.phodal.com 下载 1&#xff1a;https://github.com/phodal/aigc/releases 下载 2&#xff1a;https://pan.baidu.com/s/1wGc75vVHaZwvZyHeltyt8w?pwdphod 2023 年的上半年里&#xff0c;我&#xff08;phodal&#xff09;和…

数据结构KMP算法详解

目录 1. KMP算法是什么&#xff1f; 2. KMP算法的由来 2.1 需要要解决的问题 2.2 一开始想到的方法 2.3 KMP算法诞生了 3.KMP算法的详解 4.KMP算法的实现 5.KMP算法的改进 1. KMP算法是什么&#xff1f; KMP算法是一种改进的字符串匹配算法&#xff0c;即可以快速的从主…

通用分页【下】(将分页封装成标签)

目录 一、debug调试 1、什么是debug调试&#xff1f; 2、debug调试步骤 3、实践 二、分页的核心 三、优化 分页工具类 编写servlet jsp代码页面&#xff1a; 分页工具类PageBean完整代码 四、分页标签 jsp代码 编写标签 tld文件 助手类 改写servlet 解析&…

Let's Learn .NET - Web 开发实战

点击蓝字 关注我们 编辑&#xff1a;Alan Wang 排版&#xff1a;Rani Sun Lets Learn .NET 系列 “Lets Learn .NET” 是面向全球的 .NET 初学者学习系列&#xff0c;旨在通过不同语言&#xff0c;帮助不同地区的开发者掌握最新的 .NET 开发知识与技能。 7月2日&#xff0c;“…

【文件传输FTP】网络杂谈(4)之文件传输协议FTP详解

涉及知识点 什么是 FTP&#xff0c;FTP的概念&#xff0c;FTP客户端程序cuteFTP&#xff0c;FTP客户程序浏览器&#xff0c;FTP的客户程序。深入了解FTP技术。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多内容可去其主页关注下哈&#xff0c;不胜感激 文章…

Tips--解决SquareLine Studio create页面中只有desktop选项

解决SquareLine Studio create页面中只有desktop选项 前言问题解决方法方法1方法2 前言 LVGL是一个强大的GUI框架&#xff0c;很多电子爱好者使用很多。SquareLine Studio是针对LVGL的一款非常强大的图形化设计平台&#xff0c;大大缩减了UI设计的时间。 问题 但是很多小伙…