rust所有权进阶部分

double-w double-w     2022-12-01     422

关键词:

 

  为了演示所有权功能,我们需要一些复杂的数据类型,之前介绍的类型都是存储在栈上的并且当离开作用域就被移除栈,不过我们需要一个存储在堆上的数据来探索Rust是如何知道该在何时清理数据的。

这里使用String作为例子,它的一些例子可能也适用于标准库的或者你自定义的一些复杂数据类型,Rust中有两种字符串类型,第一种是直接使用 let s ="hello",这一种很明显不能满足要求,因为他们是不可变的,但是往往有时候我们想创建一个可变的变量。比如用户要输入时; let s = String::from("hello"), 这样就可以修改这个变量了。

fn main() 
    let mut s = String::from("hello");
    s.push_str(" wolrd");

    println!("", s);

 

内存与分配

   就字符串字面值来说,我们在编译时就知道其内容,所以文本被直接硬编码进最终的可执行文件终。这使得字符串字面值快速且高效。不过这些特性都值得益于字符串字面值的不可变性,不幸的是,我们不能为了每一个在编译时大小未知的文本而将一块内存放进二进制文件中

,并且他的大小还可能随着程序运行变化。

对于String类型,为了支持可变,可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存存放内容,这意味着:

  1、必须在运行时向操作系统请求内存;(调用String::from时,它的实现(implemetation)请求其所需的内存)

  2、需要一个当我们处理完String时将内存返回给操作系统的方法,(在其他有GC的语言中,会自动清除不再使用的内存,在没有GC的语言中,就需要程序员自己allocate配对一个free)

  Rust中采取了一个不同的策略:内存在拥有它的变量离开区域时就被自动释放。如上面的例子,当main函数的s变量在离开了作用域后,会触发一个drop方法清除内存。

 

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

  1、基础数据/不可变数据存放在栈中,通过赋值拷贝创建另一个变量

fn main() 
    let x = 5;
    let y = x;

    println!("x:", x); // 不可变数据存放在栈上,进行拷贝吗,此时栈上有两个5

    let s1 = String::from("hello");  
    // 首先在栈上创建一组数据:指向字符串的内容指针+长度+容量
    // 然后在堆上存放字符数据
    let s2 = s1;
    // 当s2指向s1时,会在栈上拷贝份s1给s2,但是这意味着栈上有两个数据指向同一个内存
    // 如果要清除就会发生“二次释放(double free)”的错误,为了解决这个问题,Rust在
    // 将s1赋值给s2后,s1就失效了。
    // println!("s1:",s1);

    在其他语言中,可能叫“浅拷贝”,在Rust中同时使第一个变量无效了,这个操作称为移动。

 

变量和数据交互方式(二):克隆

  如果我们确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone的通用函数。

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

  效果是在栈上创建两个变量,指向两个堆上的数据。

 

只在栈上的数据:拷贝

fn main() 
    let x = 5;
    let y = x;

    println!("x:,y:", x, y); // 不可变数据存放在栈上,进行拷贝吗,此时栈上有两个5

  可以看到上段代码并没有使用clone,不过x依旧没有失效被移动到y上,原因是像整型这种的在编译时就已知大小的类型被整个存储在栈上,所以拷贝其实实际的值是快速的,这意味着没有理由再创建变量y后x无效,且Rust有一个叫做Copy trait的特殊注解,可以用在类型类似整型这样的存储在栈上的变量,如果一个类型拥有copy trait的类型,一个旧的变量在讲其变量赋值后依旧可用。满足这种功能的类型有:

1、所有的整型
2、布尔类型
3、所有浮点型
4、字符类型
5、元组,当且仅当包含的类型也是满足Copy类型的

  

所有权与函数

  将值传递给函数在语义上与给变量赋值相似,向函数传递值可能会移动或者复制,就像赋值语句一样。

fn main() 
    let s = String::from("hello"); // s 进入作用域
    takes_ownership(s); // s 的值移动到函数里 ...
                        // ... 所以到这里不再有效
    let x = 5; // x 进入作用域
    makes_copy(x); // x 应该移动函数里,
                   // 但 i32 是 Copy 的,所以在后面可继续使用 x
 // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
  // 所以不会有特殊操作
fn takes_ownership(some_string: String) 
    // some_string 进入作用域
    println!("", some_string);
 // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) 
    // some_integer 进入作用域
    println!("", some_integer);
 // 这里,some_integer 移出作用域。不会有特殊操作

  

返回值与作用域

  返回值也可以转移所有权。

fn main() 
    let s1 = gives_ownership(); // gives_ownership 将返回值移交s1

    let s2 =String::from("hello"); // s2进入作用域
    let s3 = take_and_gives_back(s2); // s2被移动到函数中,它将返回s3
  // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,所以什么也不会发生,s1被移出作用域并丢弃

fn gives_ownership() -> String
    let some_string = String::from("hello");
    some_string


fn take_and_gives_back(a_string: String) -> String
    a_string


转移返回值的所有权

  变量的所有权总是遵循相同的模式:将值赋值给另一个变量时移动它,当持有堆中数据值的变量离开作用域时,其值将通过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(); // len() 返回字符串的长度 
    (s, length) 

  

返回参数的所有权

  使用引用(reference),上面的例子中,为了main函数中还能使用s1,我们必须将s1的所有权返回,显得麻烦,这里我们可以使用引用参数传进函数。

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()

  这些 & 符号就是 引用,它们允许你使用值但不获取其所有权,我们将获取引用作为函数参数称为 借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来。当你使用完毕,必须还回去。

尝试修改借用的值,剧透这是不行的

fn main()  
    let s = String::from("hello"); 
    change(&s); 


fn change(some_string: &String)  
    some_string.push_str(", world"); 

// 正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值

  

可变引用

   我们通过一个小调整就能修复上面不可以修改引用值的问题。

fn main() 
    let mut s = String::from("hello");
    change(&mut s);


fn change(some_string: &mut String) 
    some_string.push_str(", world");


// 必须将 s 改为 mut 。然后必须创建一个可变引用 &mut s 和接受一个可变引用 some_string: &mut String 。

  不过可变引用有一个很大的约束,在特定作用域中的特定数据有且只有一个可变引用

fn main() 
    let mut s = String::from("hello"); 
    let r1 = &mut s; 
    let r2 = &mut s; 
    println!(", ", r1, r2);


//错误
// let r1 = &mut s; | ------ first mutable borrow occurs here 
// let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here

  这个限制允许可变性,不过是以一种限制的方式允许,这个限制的好处是Rust可以在编译时就避免数据竞争(data race)。

数据竞争的造成原因:
    1、两个或更多指针同时访问同一个数据
    2、至少有一个指针被用来写数据
    3、没有同步数据访问的机制

一如既往,可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同 时 拥有:

let mut s = String::from("hello"); 
 
    let r1 = &mut s; 
 // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 

let r2 = &mut s;

类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:
let mut s = String::from("hello");
let r1 = &s; // 没问题 
let r2 = &s; // 没问题 
let r3 = &mut s; // 大问题 
println!(", , and ", r1, r2, r3);

  

悬挂引用

  也就是如果一个引用的指针本来是返回给的,但是还没返回就被释放了,后面接受者得到一个野指针。所谓悬垂指针是其指向的内存可能已经被分配给其他持有者了。

fn main() 
    let reference_to_nothing = dangle();


fn dangle() -> &String 
    let s = String::from("hello");
    &s  // // 这里 s 离开作用域并被丢弃。其内存被释放。



// 错误:expected lifetime parameter

// 解决方法:
fn no_dangle() -> String  
    let s = String::from("hello"); 
    s 
 // 所有权被移动出去,所以没有值被释放
接下来,我们来看看另一种不同类型的引用:slice。另一个没有所有权的数据类型是 slice。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
 
这里有一个编程小习题:编写一个函数,该函数接收一个字符串,并返回在该字符串中找到的第一个单词。如果函数在该字符串中并未找到空格,则整个字符串就是一个单词,所以应
该返回整个字符串。
  分析:first_word 函数有一个参数 &String 。因为我们不需要所有权,所以这没有问题。不过应该返回什么呢?我们并没有一个真正获取 部分 字符串的办法。不过,我们可以返回单词结尾
的索引。但是这种方法有个问题,因为存在问题。
fn main() 
    let mut s = String::from("hello world");
    let world = first_word(&s);

    s.clone() // 如果这里将s清除了
    // world还是5,但是s已经变了


fn  first_word(s:&String) -> usize 
    let bytes  =s.as_bytes(); // 将String转化为字符数组
    
    for (i,&item) in bytes.iter().enumerate()   // 使用 iter 方法在字节数组上创建一个迭代器
        // 而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。 enumerate 返 回的元组中,
        // 第一个元素是索引,第二个元素是集合中元素的引用
        if item == b‘ ‘ 
            return i;
        
    
    s.len()

  

字符串slice
  它是String中一部分值的引用,它看起来像这样:
fn main() 
    let mut s = String::from("hello world");
    let world = first_word(&s);

    println!("ret : ", world);


fn first_word(s: &String) -> &str 
    // 字符串slice的类型声明写作&str
    let bytes = s.as_bytes(); // 将String转化为字符数组

    for (i, &item) in bytes.iter().enumerate() 
        // 使用 iter 方法在字节数组上创建一个迭代器
        // 而 enumerate 包装了 iter 的结果,将这些元素作为元组的一部分来返回。 enumerate 返 回的元组中,
        // 第一个元素是索引,第二个元素是集合中元素的引用
        if item == b‘ ‘ 
            return &s[..i];
        
    

    &s[..]

  更有经验的开发者,可能会这样写函数的签名: fn  first_world(s &str) -> str, 这样如果有一个字符串slice,可以直接传递它,如果有一个String,则可以传递整个String的slice给这个函数,更加舒服。

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[..]


fn main() 
    let my_string = String::from("hello world");
    // first_word 中传入 `String` 的 slice
    let word = first_word(&my_string[..]);
    let my_string_literal = "hello world"; // first_word 中传入字符串字面值的 slice
    let word = first_word(&my_string_literal[..]); // 因为字符串字面值 **就是** 字符串 slice,
                                                   // 这样写也可以,即不使用 slice 语法!
    let word = first_word(my_string_literal);

  

其他的slice

代码如下:

fn main() 
    let a = [1, 2, 4, 5];
    let slice = &a[1..3]; // 这里的slice是&[i32],它跟字符串slice的工作方式一样,通过存储第一个元素的引用和集合长度,后期讲vector再说

    for i in slice 
        println!("item:", i); // 2,4
    

  

  所有权、借用和 slice 这些概念让 Rust 程序在编译时确保内存安全。Rust 语言提供了跟其他系统编程语言相同的方式来控制你使用的内存,但拥有数据所有者在离开作用域后自动清除
其数据的功能意味着你无须额外编写和调试相关的控制代码。所有权系统影响了 Rust 中很多其他部分的工作方式,所以我们还会继续讲到这些概念。

rust语言教程-iflet表达式与枚举进阶(代码片段)

Rust语言教程(9)-iflet表达式与枚举进阶枚举复习前面我们介绍了Rust中的枚举类型,以及通过枚举实现的Option,Result等类型。温故而知新,我们再来复习一下枚举的定义和使用。首先,Rust的enum可以像C语言中的enum一样ÿ... 查看详情

rust语言教程-iflet表达式与枚举进阶(代码片段)

Rust语言教程(9)-iflet表达式与枚举进阶枚举复习前面我们介绍了Rust中的枚举类型,以及通过枚举实现的Option,Result等类型。温故而知新,我们再来复习一下枚举的定义和使用。首先,Rust的enum可以像C语言中的enum一样ÿ... 查看详情

rust网络编程框架-tokio进阶(代码片段)

我们在上文《小朋友也能听懂的Rust网络编程框架知识-Tokio基础篇》对于Tokio的基础知识进行了一下初步的介绍,本文就对于Tokio的用法及原理进行进一步的介绍与说明。目前市面上绝大多数编程语言所编写的程序,执行程... 查看详情

rust和go该如何选择

为了内存安全引入所有权概念,为了圆所有权这个坑,引入生命周期,各种BOX。艰难的圆着所有权的坑,因此在rust群里经常谈论的大部分是语法问题,这是其他所有语言都不常见的。虽然一次编译完就可以安... 查看详情

rust和go该如何选择

为了内存安全引入所有权概念,为了圆所有权这个坑,引入生命周期,各种BOX。艰难的圆着所有权的坑,因此在rust群里经常谈论的大部分是语法问题,这是其他所有语言都不常见的。虽然一次编译完就可以安... 查看详情

「rust进阶笔记」rust之derive特性总结(代码片段)

前言编译器可以通过#[derive]为一些trait提供基础的实现。如果需要更复杂的逻辑,这些trait也可以被手动实现。这些可导入的实现:比较:Eq、PartialEq、Ord、PartialOrdClone:从&T的一个拷贝创建TCopy:把一个类型的move转换为copyHash:... 查看详情

rust语言教程-iflet表达式与枚举进阶(代码片段)

Rust语言教程(9)-iflet表达式与枚举进阶枚举复习前面我们介绍了Rust中的枚举类型,以及通过枚举实现的Option,Result等类型。温故而知新,我们再来复习一下枚举的定义和使用。首先,Rust的enum可以像C语言中的enum一样ÿ... 查看详情

rust和go该如何选择

为了内存安全引入所有权概念,为了圆所有权这个坑,引入生命周期,各种BOX。艰难的圆着所有权的坑,因此在rust群里经常谈论的大部分是语法问题,这是其他所有语言都不常见的。虽然一次编译完就可以安... 查看详情

Rust 所有权问题

】Rust所有权问题【英文标题】:Rustownershipissues【发布时间】:2021-01-2916:10:51【问题描述】:我对Rust很陌生,我主要是C#、javascript和python开发人员,所以我喜欢以OOP方式处理事情,但是我仍然无法理解rust的所有权。尤其是在OOP... 查看详情

新手眼中的rust所有权规则(代码片段)

新手眼中的Rust所有权规则如果你有关注本人博客,那么很明显,从今年年初开始,我便开始学习Rust。此文与之前风格略有不同,旨在总结阅读Rust书籍时遇到的要点。到目前为止,它包含了我对Rust所有权规则的所有理解。Rust的... 查看详情

为啥 Rust 需要所有权注释而不是推断它? [复制]

】为啥Rust需要所有权注释而不是推断它?[复制]【英文标题】:WhydoesRustrequireownershipannotationsinsteadofinferringit?[duplicate]为什么Rust需要所有权注释而不是推断它?[复制]【发布时间】:2020-10-1722:31:43【问题描述】:Rust为什么不能完... 查看详情

rust所有权

所有权规则Rust中的每一个值都有一个被称为其所有者(owner)的变量。值在任一时刻有且只有一个所有者。当所有者(变量)离开作用域,这个值将被丢弃。引用和Copy特性赋值过程:包括变量赋值,函数传参,函数返回如果类... 查看详情

rust网络编程框架-tokio进阶(代码片段)

我们在上文《小朋友也能听懂的Rust网络编程框架知识-Tokio基础篇》对于Tokio的基础知识进行了一下初步的介绍,本文就对于Tokio的用法及原理进行进一步的介绍与说明。目前市面上绝大多数编程语言所编写的程序,执行程... 查看详情

网络进阶部分(代码片段)

查看进程进程ps支持三种选项:UNIX选项如-A-eBSD选项如aGNU选项如--helpa选项包括所有终端中的进程?x选项包括不链接终端的进程?u选项显示进程所有者的信息?f选项显示进程树,相当于--forest?k|--sort属性对属性排序,属性前加-表示倒序?... 查看详情

rust所有权语义模型

首发于知乎专栏本文试图从语义角度来解释Rust所有权的概念,以便帮助降低Rust的学习曲线。编程语言的内存管理,大概可以分为自动和手动两种。自动管理就是用GC(垃圾回收)来自动管理内存,像Java、Ruby、Golang、Elixir等语言... 查看详情

rust内存管理(代码片段)

...不外乎两种方式:使用者在代码中显示调用函数,回收这部分内存;或者引入自动的垃圾 查看详情

rust从入门到精通04-数据类型

...标量scalar每个类型有一个单独的值。1.1整型表示没有小数部分的数 查看详情

Rust 如何知道哪些类型拥有资源?

...指向某个堆分配的内存时,我假设Rust已经“硬编码”了所有权知识,因此当通过调用某个函数来转移所有权时,资源会移动并且函数中的参数是新的所有者。但是,例如,矢量如何发生这种情况?它们也“拥有”自己的资源,... 查看详情