【Rust】所有权

这篇具有很好参考价值的文章主要介绍了【Rust】所有权。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

所有权

所有权是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从操作系统欧冠总共获得内存的字节数。

【Rust】所有权

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

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

【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);

【Rust】所有权

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

【Rust】所有权

借用

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

可变引用

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

【Rust】所有权

字符串切片的范围索引必须发生在有效的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 的工作方式一样,通过存储第一个集合元素的引用和一个集合总长度。文章来源地址https://www.toymoban.com/news/detail-509026.html

到了这里,关于【Rust】所有权的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Rust-所有权(ownership)

    Rust入门学习系列-Rust 的核心功能(之一)是 所有权(ownership)。引入这个概念是为了更好的管理计算机的内存。下面篇幅让我们来研究下这个功能有什么神奇之处。 常见的编程语言中计算机内存管理方式: Java:Java使用Java虚拟机(JVM)来管理计算机内存。JVM有一个垃圾回收

    2024年02月19日
    浏览(15)
  • Rust核心功能之一(所有权)

    Rust核心功能之一(所有权)

    目录 1、什么是所有权? 1.1 所有权规则  1.2 变量作用域 1.3 String 类型 1.4 内存与分配 变量与数据交互的方式(一):移动 变量与数据交互的方式(二):克隆 只在栈上的数据:拷贝 1.5 所有权与函数 1.6 返回值与作用域 所有权(系统)是 Rust 最为与众不同的特性,对语言的

    2024年02月04日
    浏览(17)
  • rust学习——栈、堆、所有权

    rust学习——栈、堆、所有权

    栈和堆是编程语言最核心的数据结构,但是在很多语言中,你并不需要深入了解栈与堆。 但对于 Rust 这样的系统编程语言,值是位于栈上还是堆上非常重要, 因为这会影响程序的行为和性能。 栈和堆的核心目标就是为程序在运行时提供可供使用的内存空间。 栈 栈按照顺序存

    2024年02月07日
    浏览(13)
  • Rust-所有权和移动语义

    Rust-所有权和移动语义

    拿C语言的代码来打个比方。我们可能会在堆上创建一个对象,然后使用一个指针来管理这个对象: 接下来,我们可能需要使用这个对象: 然而,这段代码之后,谁能猜得到,指针p指向的对象究竟发生了什么?它是否被修改过了?它还存在吗,是否已经被释放?是否有另外一个指

    2024年01月18日
    浏览(14)
  • 【Rust】Rust学习 第四章认识所有权

    【Rust】Rust学习 第四章认识所有权

    所有权(系统)是 Rust 最为与众不同的特性,它让 Rust 无需垃圾回收(garbage collector)即可保障内存安全。因此,理解 Rust 中所有权如何工作是十分重要的。 4.1 所有权 所有运行的程序都必须管理其使用计算机内存的方式。一些语言中具有垃圾回收机制,在程序运行时不断地

    2024年02月13日
    浏览(12)
  • 30天拿下Rust之所有权

    概述         在编程语言的世界中,Rust凭借其独特的所有权机制脱颖而出,为开发者提供了一种新颖而强大的工具来防止内存错误。这一特性不仅确保了代码的安全性,还极大地提升了程序的性能。在Rust中,所有权是一种编译时检查机制,用于追踪哪些内存或资源何时可

    2024年03月08日
    浏览(14)
  • Rust语法:所有权&引用&生命周期

    Rust语法:所有权&引用&生命周期

    垃圾回收管理内存 Python,Java这类语言在管理内存时引用了一种叫做垃圾回收的技术,这种技术会为每个变量设置一个引用计数器(reference counter),来统计每个对象的引用次数。 一旦某个对象的引用数为0,垃圾回收器就会择取一个时机将其所占用的空间回收。 以Python为例子

    2024年02月12日
    浏览(12)
  • Rust语言从入门到入坑——(5)Rust 所有权

    Rust语言从入门到入坑——(5)Rust 所有权

    主要介绍Rust所有权的知识,涉及到变量的作用域,内存释放机制,移动,克隆,引用等知识,很多知识是Rust语言特有机制。 所有权有以下三条规则: - Rust 中的每个值都有一个变量,称为其所有者。 - 一次只能有一个所有者。 - 当所有者不在程序运行范围时,该值将被删除

    2024年02月10日
    浏览(11)
  • Rust 基础入门 —— 2.3.所有权和借用

    Rust 的最主要光芒: 内存安全 。 实现方式: 所有权系统 。 因为我们这里实际讲述的内容是关于 内存安全的,所以我们最好先复习一下内存的知识。 然后我们,需要理解的就只有所有权概念,以及为了开发便利,进一步引出的引用借用概念。 内存作为存储程序运行时数据

    2024年02月12日
    浏览(15)
  • 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念

    【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念

    【跟小嘉学 Rust 编程】一、Rust 编程基础 【跟小嘉学 Rust 编程】二、Rust 包管理工具使用 【跟小嘉学 Rust 编程】三、Rust 的基本程序概念 【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念 本章节将讲解 Rust 独有的概念(所有权)。所有权是 Rust 最独特的特性,它使得 Rust 能够

    2024年02月10日
    浏览(16)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包