当前位置: 首页 > news >正文

Rust学习笔记(三)|所有权机制 Ownership

本篇文章包含的内容

  • 1 重新从堆和栈开始考虑
  • 2 所有权规则
  • 3 变量和数据(值)的交互方式
    • 3.1 移动 Move
    • 3.2 克隆 Clone
    • 3.3 复制 Copy
  • 4 函数与所有权
    • 4.1 参数传递时的所有权转移
    • 4.2 函数返回时的所有权转移
  • 5 引用和借用
  • 6 切片

前面两篇仅仅介绍了一些Rust的语法以及一些程序书写特点。如果是其他语言,其实已经可以说完成了六成以上的学习,可以开始着手项目,以实践驱动学习了。但所有权和生命周期才是Rust的魅力所在,真正的难点现在才刚刚开始(噔噔咚)。


1 重新从堆和栈开始考虑

所有权是Rust最独特的特性之一,使得它与Java、C#等语言相比不需要GC(Garbage Collector,垃圾收集器)就可以保证内存安全,同时也不需要像C/C++一样手动释放内存。为了理解所有权,我们必须了解Rust的内存分配机制,这是在之前学习的语言中基本不会注意的点。

无论哪种语言编写的程序,都必须考虑他们运行时对计算机内存的操作方式。Rust并不相信程序员,但是也摒弃了GC算法这种低效的方式,取而代之的是引入所有权的概念,使程序中的内存操作错误在编译时就基本解决,并且这种做法不会造成任何的运行时开销。

在程序运行时,堆(Heap)和栈(Stack)都是程序可用的内存,它们的本质区别是内存组织的方式不同。栈内存先入后出,永远有一个指针指向栈顶,内存的存储是连续的,所有存储在栈中的数据必须有已知的或者固定的大小;而堆内存相对比较混乱,程序使用的内存是碎片化的,一般在运行时申请的动态内存都属于堆内存,操作系统在申请Heap时,需要申请一个足够大的空间,并返回一个额外的指针变量记录变量的存储位置(并且需要做好记录和管理方便下次分配),这导致程序运行时的指针可能存在大范围的跳转。总之,栈内存效率更高,堆内存以牺牲效率为代价换取了更多的灵活性。

所有权解决了以下问题:

  • 跟踪代码的哪些部分正在使用Heap的哪些数据;
  • 最小化Heap上的重复数据量;
  • 及时清理Heap上未使用的数据以避免空间不足。

2 所有权规则

Rust中所有权有以下三条规则(它很重要,先记下来再慢慢理解):

  1. 每个值都有一个变量,这个变量就是这个值的所有者;
  2. 每个值同时只能有一个所有者;
  3. 当所有者超出作用域(Scope)时,该值将被删除。

下面是一个关于作用域(Scope)的简单例子。作用域的概念在其他编程语言中也有,这里需要理解的是,s是变量,“hello”就是这个变量的值(一个字符串字面值)。

// s 无效
fn main() {// s 无效let s = "hello";	// s 可用// s 继续有效
}	// s 的作用域从这里结束

通过第一部分的解释,这里就比较好理解变量s的存储方式了。它的值在编译时就已经全部确定,并且不会随之变化(如果需要变化则需要引入String类型),所以这个变量和它的值在编译时就会被全部写入可执行文件中。

与之相比,String类型在堆上分配,这使得它可以存储在编译时未知数量的文本。下面的例子中,s超出作用域时会自动调用一个特殊的名为drop的函数来释放内存。所以String类型是一个实现了Drop trait(trait,接口)的类型。

fn main() {let mut s = String::from("Hello");s.push_str(", world!");println!("{}", s);		
}	// s 会自动调用一个drop函数

看到这里你可能依然一头雾水(这家伙在说什么呢.jpg),这些概念和C/C++以及其他语言难道做不到吗?超出作用域释放内存难道不是理所当然的吗?既然如此我还为什么要学Rust?Rust究竟好在哪?所谓的内存安全就这?

别急,这个Drop方法看似人畜无害,但是它会导致一个非常严重的bug。

3 变量和数据(值)的交互方式

3.1 移动 Move

首先看下面这个例子,创建了两个简单的整数变量,由于它们的大小是确定的,所以两个变量都将被压入栈中,值发生了复制。像整数这样完全存放在栈上的数据实现了Copy trait。

let x = 5;
let y = x;		// value copied here

但是下面这个例子不同,s1在内存中的索引信息存储在栈中,s1所对应的内容需要被存放在堆中(出于值的长度可变的需要)。栈中包含一个指向字符串存储位置的指针,一个字符串实际长度,一个从操作系统中获得的内存的总字节数。

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

在这里插入图片描述
如果接下来接着执行这一语句,那么栈中s1的信息会被复制一份,但是堆中字符串的值不会复制(有点像浅拷贝),s1的所有权将会直接被递交给s2,同时s1会直接失效,这时我们说值的所有权发生了移动(Move)。这样做的目的是避免两个字符串离开作用域时调用两次drop函数,从而导致严重的Double Free错误。

let s2 = s1;			// value moved here
println!("{}", s1);		// 编译直接报错

请添加图片描述

3.2 克隆 Clone

对于上面的s1s2的例子,如果想同时拷贝栈和堆中的信息,可以使用clone()方法。这样的操作明显是比较浪费资源的。

在这里插入图片描述

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

3.3 复制 Copy

总之,如果一个变量存在Copy trait,那么旧变量在“移动”后依然可用;如果一个类型或者该类型的一部分实现了Drop triait(例如定义的元组的一部分是String的情况),那么Rust就不允许它再实现Copy trait了,编译时就会进行检查,在移动后旧变量就不再可用,除非使用了clone()方法。

4 函数与所有权

Rust中的变量总是遵循下面的规则:

  • 把一个变量赋值给其他变量就会发生移动(除非变量存在Copy trait);
  • 当变量超出其作用域后,存储在Heap上的数据就会被销毁(Drop trait),除非它的所有权已经被转移。

4.1 参数传递时的所有权转移

在Rust中,如果函数参数的类型是一个实现了Drop trait的类型(例如String类型),把值传递给函数中往往伴随着所有权的转移,也就是说旧变量对值的所有权会发生丢失,这里发生的事情和把变量赋值给另一个变量是类似的。看下面这个例子:

fn main() {let s1 = String::from("hello");take_ownership(s1);// println!("{}", s1);      // 编译报错let x = 1;makes_copy(x);println!("the x is {}", x);
}fn take_ownership(some_string: String) {println!("{}", some_string);
}fn makes_copy(some_integer: i32) {println!("{}", some_integer);
}

对于String这种类型的变量,直接将其作为函数参数时,传入参数时helloString的所有权会从s1转换到函数内部的some_string,程序运行到take_ownership函数之外时会自动调用Drop trait,字符串的值的内存会被释放。但是对于实现了Copy trait的类型,例如i32,参数传递时会发生copy,而不是move,这样在函数调用后x变量依然是可用的。

4.2 函数返回时的所有权转移

这个比较好理解,看下面一个例子:

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

对于gives_ownership函数,在函数内部创建了一个新的String,函数返回时不会将其销毁,而是把它的所有权交给主函数的s1;而takes_and_gives_back函数获取到s2到的所有权,s2之后会失效,返回时将String的所有权交还给主函数的s3

5 引用和借用

但有些时候,我们只想获得变量的值,而不想它的所有权发生转移(甚至丢失),这时候就可以使用引用(Reference)。

fn main() {let s1 = String::from("hello");let lenth = calculate_length(&s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &String) -> usize {s.len()
}

在上面的例子中,calculate_length函数使用了String的引用作为参数,函数计算返回字符串长度后s1仍然是可用的。引用相当于一个指针,它可以获取到变量对应的值,但是不拥有它,所以当其离开作用域时也无法销毁它。像这样,把引用作为函数参数这个行为称为借用(Borrow)
在这里插入图片描述

在Rust中,引用和变量类似,也分为可变的引用和不可变的引用,创建的引用默认同样是不可变的。下面是一个使用可变引用的例子。

fn main() {let mut s1 = String::from("hello");let lenth = calculate_length(&mut s1);println!("The length of '{}' is {}.", s1, lenth);
}fn calculate_length(s: &mut String) -> usize {s.push_str(", world!");s.len()
}

需要注意引用的特殊限制:在特定的作用域内,一个变量只能同时拥有一个可变的引用;并且不能同时存在可变的引用和不可变的引用。一个变量可以拥有多个不可变的引用。Rust从编译层面解决了数据竞争的问题。

let mut s = String::from("hello");let s1 = &mut s;
let s2 = &mut s;	// 非法
let mut s = String::from("hello");
{let s1 = &mut s;
}
let s2 = &mut s;	// 合法

这样的做法还带来了另一个好处,即永远不会存在“悬空引用”(Dangling Reference,一个引用或者指针指向一块内存,但是这一块内存可能已经被释放或者被其他人使用了)或者“野指针”。

总之,引用一定满足下面的规则

  • 引用一定有效;
  • 引用一定满足下列条件之一,不可能同时满足:
    • 存在一个可变引用;
    • 存在任意数量的不可变引用。

6 切片

切片(Slice)是指一段数据的引用。这里的一段数据可以是String类型,也可以是数组。字符串切片的写法如下所示,类型名在程序中是&str

let s = String::from("hello world");let hello = &s[0..5];	// 左闭右开,此时相当于 &s[..5]
let world = &s[6..11]	// 此时相当于 &s[6..]let whole = &s[..]		// 整个字符串的切片

在这里插入图片描述

需要注意,字符串切片的索引必须发生在有效的UTF-8字符边界内(就是不能把字符切“坏”了),否则程序就会报错退出。

为什么要使用切片?看下面这个例子:获取字符串中的各个单词,如果字符串中没有空格,则返回整个字符串。

fn main() {let s = String::from("hello");let word_index = first_word(&s);println!("{}", word_index);
}fn first_word(s: &String) -> usize {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return i;}}s.len()
}

上面这个程序虽然能完成一部分功能(获取第一个空格的位置),但是这个程序存在一个重要的结构性缺陷:变量word_index和Strings之间没有任何联系,即使s被释放,或者被修改,word_index也无法感知。

使用字符串切片重写上面的例子:

fn main() {let s = String::from("hello world");let word = first_word(&s);      // 把s作为不可变的引用发生借用,之后s都不可变// s.clear();       // s不可变println!("{}", word);
}fn first_word(s: &String) -> &str {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

请添加图片描述

字符串子面值也是切片。利用这一特点,我们可以将函数的参数类型改为字符串切片&str,使得函数可以直接接收字符串子面值作为参数,这样函数就可以同时接收String和字符串切片两种类型的变量作为参数了

fn main() {let word = first_word("hello world");      println!("{}", word);
}fn first_word(s: &str) -> &str {let bytes = s.as_bytes();   // 将String转换为字符数组for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[..i];}}&s[..]
}

其他数组类型也存在切片,例如使用下面的方法创建一个i32类型的切片,程序中用&[i32]表示该类型。

let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice = &a[1..3];		// slice类型是&[i32]

  原创笔记,码字不易,欢迎点赞,收藏~ 如有谬误敬请在评论区不吝告知,感激不尽!博主将持续更新有关嵌入式开发、FPGA方面的学习笔记。


http://www.dtcms.com/a/332961.html

相关文章:

  • fpga高速接口汇总整理
  • vue3 app.use()的作用
  • 功能组状态变更能否跨越功能组边界
  • 【递归、搜索与回溯算法】DFS解决FloodFill算法
  • Python subprocess.Popen 打开第三方程序
  • 鸿蒙ArkUI动画实战:TransitionEffect实现元素显隐过渡
  • 启动electron桌面项目控制台输出中文时乱码解决
  • 基于.net、C#、asp.net、vs的保护大自然网站的设计与实现
  • 深度解读 Browser-Use:让 AI 驱动浏览器自动化成为可能
  • 【微服务】.NET8对接ElasticSearch
  • Webapi发布后IIS超时(.net8.0)
  • 后台管理系统-2-vue3之路由配置和Main组件的初步搭建布局
  • 记一次impala的kerberos的配置信息
  • 什么是主网切换
  • DAY41打卡
  • 附045.Kubernetes_v1.33.2高可用部署架构二
  • Web攻防-大模型应用LLM安全提示词注入不安全输出代码注入直接间接数据投毒
  • 稳定且高效:GSPO如何革新大型语言模型的强化学习训练?
  • vue3相关基础
  • kubernetes(序)
  • 从前端框架到GIS开发系列课程(26)在mapbox中实现地球自转效果,并添加点击事件增强地图交互性
  • 超级云 APP 模式:重构移动互联网生态的新引擎
  • 开机自启脚本报错 which: no java in (/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin)
  • 区块链技术原理(12)-以太坊区块
  • 无人机光电探测模块技术分析
  • 39 C++ STL模板库8-容器1-array
  • 【Java】HashMap的详细介绍
  • uniApp App 端日志本地存储方案:实现可靠的日志记录功能
  • Python 高级语法与用法详解 —— 提升编程效率与代码质量
  • 【LeetCode Solutions】LeetCode 热题 100 题解(36 ~ 40)