【Rust】004-Rust 所有权

打印 上一主题 下一主题

主题 533|帖子 533|积分 1599

【Rust】004-Rust 所有权


  
   rust 语言很严谨!符合我严谨认真的性格!
  一、准备知识

1、堆和栈

堆和栈都可以在程序运行时给程序提供内存空间,但是他们是有区别的。

想象一下,栈就像一叠盘子。先来的盘子在底部,新盘子则放在顶部。取用时,总是先取最上面的盘子,就像在餐馆里洗完的盘子先用先拿。这就是先辈后出的原则。但是,盘子大小必须是确定的,否则无法存放在这个“栈”里。

而堆则像一个大杂货堆栈。你可以随时放入各种大小外形物品。为了方便找到它们,你必要贴上标签,这些标签就像指针,告诉你物品位置。固然这样存放物品灵活多变,但要找到合适的空间并读取它们就不如“栈”中的盘子方便了。

性能竞走中,栈就像速度迅猛的跑车,堆则更像一辆载重卡车。栈的读写速度快,由于一切都井井有条,就像把盘子放在最上面那样简单。而堆要先找到合适的存放空间,读取时还要通过标签指针)找到物品,就像在杂货堆栈里寻找一个小小的零件一样,可能必要多花点时间

2、String 类型

Rust 编程天下中,字符串有点像我们现实生活中的名片(印好之后,不再改变)和便签(可以继续往上面写东西)。有两种类型让我们来认识它们:String和&str
&str

存储逻辑
在 Rust 中,字符串字面量(如 "hello")的现实值通常存储在程序的只读数据段中。这个数据段是编译时确定的,并且在程序运行时是不可变的。这意味着字符串字面量在程序的生命周期内是常驻内存的,不会被修改或释放。
当你在代码中使用字符串字面量时,例如:
  1. let s: &str = "hello";
复制代码
这个 &str 类型的变量 s 是一个胖指针,包罗了指向 "hello" 的内存地址和字符串的长度。这些信息自己存储在栈上,而 "hello" 的现实字符串数据则在程序的只读数据段中。

想象&str是一种名片,它被称为字符串字面值,就像是工厂里生产出来的名片,印好了,就再也不能改了。你可以这样给自己制造一张名片
  1. fn main() {
  2.     // 使用字符串字面量创建静态字符串
  3.     let hello = "hello world";
  4. }
复制代码
这张名片是固定的,不可变的,由于它是硬编码到程序中的,就像是直接印在墙上的字。

String

存储逻辑

  • 堆上存储数据

    • String 的现实字符串数据存储在堆上。这意味着 String 可以在程序运行时动态调解其大小,而不受栈大小的限定

  • 栈上存储元数据

    • String 类型在栈上存储了一些元数据,包括一个指向堆上数据的指针、字符串的长度以及当前的容量(即分配的堆内存大小)。这些元数据使得 String 可以管理堆上的内存。

  • 容量与长度

    • 长度:表示当前存储在 String 中的字符数。
    • 容量:表示已经分配的内存空间,允许在不重新分配的环境下扩展字符串。String 可能会预先分配比现实必要更多的内存,以优化性能,淘汰频繁的内存分配和复制操作。

  • 增长计谋

    • 当 String 的内容增加到超出其当前容量时,String 会主动分配更大的内存块,并将现有数据复制到新分配的内存中。这通常是通过倍增计谋来实现的,以平衡内存使用和性能。

  • 所有权与内存管理

    • String 拥有其堆上数据的所有权,这意味着当 String 被抛弃时,它会主动释放其占用的堆内存。这是 Rust 所有权模型和借用检查器的一个紧张特性,确保了内存的安全管理。

  • 可变性

    • String 是可变的,可以通过方法如 push 或 push_str 来追加数据,或者通过 truncate 来淘汰长度。

但生活中不是所有的信息都是确定的,偶然候我们必要一些便签。这就是String的脚色了,好比你想要记录下用户的一些临时想法或命令。String的数据存储在上,就像前面提到的杂货堆栈,可以随时变更。你还记得堆和栈的故事吗?
创建一个String就像是抓一张空白的便签纸,你可以从字符串字面值开始书写:
  1. fn main() {
  2.    
  3.     // 创建一个可变字符串
  4.     let mut hi = String::new();
  5.     // 写入文字
  6.     hi.push_str("hello");
  7.     hi.push_str(" world");
  8. }
复制代码

二、所有权规则

1、所有权系统的三条规则


  • Rust 中每个值都有一个所有者;
  • 一个值同时只能有一个所有者;
  • 当所有者离开作用域范围,这个值将被抛弃。

2、代码示例

  1. fn main() {
  2.     // 规则 1:Rust中每个值都有一个所有者
  3.     let s1 = String::from("hello"); // s1 是值 "hello" 的所有者
  4.     {
  5.         // 规则 2:一个值同时只能有一个所有者
  6.         let s2 = s1; // 所有权从 s1 转移到 s2(s1 不再有效)
  7.         // println!("{}", s1); // 这会导致编译时错误,因为 s1 不再有效
  8.         println!("{}", s2); // 这是允许的,因为 s2 现在拥有该值
  9.     } // s2在此处超出范围
  10.     // 规则 3:当所有者离开作用域范围,这个值将被丢弃
  11.     // 由于 s2 超出范围,因此为值 "hello" 分配的内存在此处自动释放。
  12. }`  
复制代码

3、所有权转移

简单示例

   i32这样的简单类型,赋值的时候 Rust 会主动举行拷贝(Copy)。
  1. let x = 5;
  2. let y = x;
复制代码
这段代码中,起首将 5 绑定到 x,接着再将 x 的值拷贝给 y。这两行实验完, x 和 y 都是 5,且都可以正常使用。稍稍改变一下这个例子。
   而对于 String 这样的分配到堆上的复杂类型,发生的却是所有权的转移,而不是拷贝。
  1. let s1 = String::from("hello");
  2. let s2 = s1;
复制代码
  简单类型:主动拷贝(简单类型的现实数据内容存储在栈上的);
  复杂类型:所有权转移(拷贝的是内存地址,现实数据内容在堆上,但会导致一个数据被两个变量同时拥有,离开作用域会出现双重释放的环境,进而导致安全题目,因此Rust计划了所欲全转移机制!)。
  
复杂类型的拷贝

   这种拷贝不存在所有权的转移,他们是相互独立的!
  1. let s1 = String::from("hello");
  2. let s2 = s1.clone();
  3. println!("s1 = {}, s2 = {}", s1, s2); // s1 = hello, s2 = hello
复制代码

4、函数的传值与返回

将值传给函数跟上面讲的赋值类似,都会进行转移或者拷贝的过程。**函数返回一个值的时候,也会经历所有权转移的过程。**我们用下面的例子来说明:
  1. fn takes_ownership(s: String) {
  2.     println!("Received string: {}", s);
  3. } // s 离开作用域,被丢弃
  4. fn gives_ownership() -> String {
  5.     String::from("hello")
  6. } // 返回了String的所有权
  7. fn main() {
  8.     // s 拿到了"hello"的所有权
  9.     let s = String::from("hello");
  10.     // 所有权转移给了 takes_ownership 函数的参数:s
  11.     takes_ownership(s); // s转移到了函数内,不再可用
  12.     // s 不再可用
  13.     // 此处还可以声明一个 s ,是因为上面的 s 已经被回收了!
  14.     let s = gives_ownership(); // s 获得了返回值的所有权
  15. }
复制代码

三、引用与借用

1、借用

只使用变量,而不拿走所有权,叫“借用”!

2、不可变引用(只读)

  1. fn main() {
  2.     // s1 拿到"hello"的所有权
  3.     let s1 = String::from("hello");
  4.     // 使用 &s1 而不是 s1,借出去,只是借出去,并不允许值被改变
  5.     // 使用 len 接收返回值
  6.     let len = calculate_length(&s1);
  7.     // s1 仍然具有"hello"的所有权
  8.     // len 是借用出去后所得到 len() 的返回值
  9.     println!("The length of '{}' is {}.", s1, len);
  10. }
  11. // 使用 &String 表示借用,是 String 类型的引用
  12. fn calculate_length(s: &String) -> usize {
  13.     // 从借来的 s 取得 len() 的值,并返回
  14.     s.len()
  15. }
复制代码

3、可变引用(可读可写)

  1. fn main() {
  2.     // s 拿到 "hello" 的所有权,s 本身是可修改的
  3.     let mut s = String::from("hello");
  4.     // 将 s 借出去,并允许被修改
  5.     change(&mut s);
  6.     // s 的值被修改了
  7.     println!("The updated string is: {}", s);
  8. }
  9. // 这里使用 &mut String 来接收,表示要求可被修改
  10. fn change(s: &mut String) {
  11.     // 修改借来的数据
  12.     s.push_str(", world!");
  13. }
复制代码

4、紧张规则

   对于一个变量,同时只能存在一个可变引用或者多个不可变引用。
  1. fn main() {
  2.     let mut s = String::from("hello");
  3.     // 多个不可变引用是允许的
  4.     let r1 = &s;
  5.     let r2 = &s;
  6.     println!("r1: {}, r2: {}", r1, r2);
  7.     // 在这里,多个不可变引用是允许的,因此打印不会引发错误。
  8.     // 一个可变引用
  9.     let r3 = &mut s;
  10.     println!("r3: {}", r3);
  11.     // 在这里,只有一个可变引用,因此打印不会引发错误。
  12.     // 不允许同时存在可变引用和不可变引用
  13.     // let r4 = &s; // 这会导致编译时错误
  14.     // 如果这里只打印 r4 是不会报错的,因为 r3 在上面已经释放,但此处打印了 r3 ,r3 就不会在此前被释放了!
  15.     // println!("r3: {}, r4: {}", r3, r4);
  16.     // 如果取消注释上面两行,同时存在可变引用和不可变引用将导致编译时错误。
  17. }
复制代码

5、NLL

在老版本的 Rust 编译器中(1.31之前),确实上述的r1,r2和r3是会报错的。但是这样实在会带来很多麻烦,导致代码很难写。于是 Rust 编译器做了一项优化:引用的作用域结束的位置不再是花括号的位置,而是末了一次使用的位置。因此,在现在 Rust 的版本中,上面的例子并不会报错。

6、悬垂引用

悬垂引用指的是指针指向的是内存中一个已经被释放的地址**,这在其他的一些有指针语言中是很常见的错误。而 Rust 则可以在编译阶段就包管不会产生悬垂引用。也就是说,如果有一个引用指向某个数据编译器能包管在引用离开作用域之前,被指向的数据不会被释放。
错误代码

  1. fn main() {
  2.     let reference_to_nothing = dangle();
  3. }
  4. fn dangle() -> &String {
  5.     let s = String::from("hello");
  6.     // 这里会报错,因为 s 已经被释放了!返回的地址是一个无效的地址!
  7.     // this function's return type contains a borrowed value, but there is no value for it to be borrowed from
  8.         // 该函数返回了一个借用的值,但是没有可以借用的来源
  9.     // 引用必须是有效的
  10.     &s
  11. }
复制代码

四、切片

1、概念

切片可以让我们引用集合中的一段连续空间。切片也是一种引用,因此没有所有权

2、字符串切片

基本写法

  1. fn main() {
  2.     let s = String::from("hello world");
  3.     let hello = &s[0..5];
  4.     let world = &s[6..11];
  5.     println!("{}", s);
  6.     println!("{}", hello);
  7.     println!("{}", world);
  8. }
复制代码

简化写法

  1. let s = String::from("hello");
  2. let len = s.len();
  3. // 以0开始时,0可以省略
  4. let slice = &s[0..2];
  5. let slice = &s[..2];
  6. // 以最后一位结束时,len可以省略
  7. let slice = &s[3..len];
  8. let slice = &s[3..];
  9. // 同时满足上述两条,那么两头都可以省略
  10. let slice = &s[0..len];
  11. let slice = &s[..];
复制代码

3、其他切片

   String 自己就是数组
  1. #[derive(PartialEq, PartialOrd, Eq, Ord)]
  2. #[stable(feature = "rust1", since = "1.0.0")]
  3. #[cfg_attr(not(test), lang = "String")]
  4. pub struct String {
  5.     vec: Vec<u8>,
  6. }
复制代码
除了 String,数组类型也有切片。例如:
  1. let a = [1, 2, 3, 4, 5];
  2. let slice = &a[1..3];
  3. assert_eq!(slice, &[2, 3]);
复制代码

五、总结

本节内容较多,主要包罗了三部门的知识:所有权,借用和切片。所有权这套系统是 Rust 内存安全的紧张保障。有了这套系统,我们既可以享受不必要手动释放内存的便利,又可以对内存使用有足够的控制,包管内存安全。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

千千梦丶琪

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表