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

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

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念


前言

本章节将讲解 Rust 独有的概念(所有权)。所有权是 Rust 最独特的特性,它使得 Rust 能够在不需要垃圾收集器的情况下保证内存安全。因此理解所有权是如何工作很重要,本章节将讲解所有权相关的特性:借用、切片以及 Rust 如何在内存中布局数据。

主要教材参考 《The Rust Programming Language》


一、所有权(Ownership)

所有权是 Rust 最独特的特性,它使得 Rust 能够在不需要垃圾收集器的情况下保证内存安全。

1.1.、所有权(Ownership)

在 Java 等编程语言中存在垃圾回收机制,在程序运行时定期查找不再使用的内存,在C/C++中,程序员必须显式地分配和释放内存。

在 Rust 之中使用了第三种方法:内存通过一个所有权系统进行管理,该系统拥有一组编译器检查的规则,如果违反了任何规则,程序将无法编译。所有权的任何特性都不会再程序运行时减慢它的速度。

1.2、栈(Stack)和堆(Heap)

许多语言不需要经常考虑堆和栈,但是在Rust 这样的系统编程语言中,值存在栈还是堆上都会影响语言的行为,以及你要做出什么样的处理。

堆栈都是都可以在运行时使用的内粗部分,但是它们的结构方式不同。栈按照获取值的顺序存储值,并按照相反的顺序删除值。这被称为后进先出。添加数据称为压栈(push),删除数据称为出栈(pop)。

存储在栈上的所有数据必须具有已知的固定大小。在编译时大小未知或大小可能改变的数据必须存储在堆中。

堆的组织较差:当你数据放在堆上,您请求一定数量的空间。内存分配器在堆中找到一个足够大的空间,将其标记为正在使用,并返回一个指针,该指针是该位置的地址,这个过程叫做在堆上分配,由于指向堆堆指针是已知的固定大小,因此可以将指针存储在栈上。

1.3、所有权规则(Ownership Rules)

所有权有如下三条规则

  • Rust 中的每个值都有一个变量,称为其所有者;
  • 一次只能有一个所有者;
  • 当所有者不再程序运行范围时,该值将会被删除;

这三条规则时所有权概念的基础;

1.4、变量作用域(Variable Scope)

通过理解下面的代码实例,可以理解变量作用范围。

fn main() {
    let s1 = "hello";
    {                        // s2 is not valid here, it’s not yet declared
        let s2 = "hello";    // s2 is valid from this point forward
        // do stuff with s2
    }                        // this scope is now over, and s is no longer valid
}

1.5、字符串类型(String Type)

1.5.1、字符串切片引用(&str 类型)

使用字符串字面初始化的字符串类型是 &str 类型的字符串。此种类型是已知长度,存储在可执行程序的只读内存段中(rodata)。通过 &str 可以引用过 rodata 中的字符串。

let s:&str = "hello";

如果想直接使用 str 类型 是不可以的,只能通过 Box<str> 来使用。

1.5.2、String类型

1.5.2.1、 String 字符串介绍

Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片引用。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。

Rust 中的字符串是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的。

字符串字面量值被硬编码到程序中,它们是不可变的。但是实际上并不是所有的字符串值都是已知的。如果我们想要获取用户输入并存储他,Rust 提供了第二种字符串类型。这种类型管理在堆上分配的数据。

fn main() {
    let s1:&str = "hello";
    let s2:String = s1.to_string();
    let s3:&String = &s2;
    let s4:&str = &s2[0..3];
    let s5:String = String::from(s1);
}

需要注意,rust 要求索引必须是 usize 类型。如果起始索引是0,可以简称 &s[..3] ,同样可以终止索引 是 String 的最后一个字节,那么可以简写为 &s[1..],如果要引用整个 String 可以简写为 &s[..]

字符串切片引用索引必须在字符之间的边界未知,但是由于 rust 的字符串是 utf-8 编码,因此必须小心。

1.5.2.2、 创建 String 字符串
let s = "Hello".to_string();
let s = String::from("world");
let s: String = "also this".into();
1.5.2.2、 追加字符串
fn main() {
    let mut s = String::from("Hello ");
    s.push('r');
    println!("追加字符 push() -> {}", s);

    s.push_str("ust!");
    println!("追加字符串 push_str() -> {}", s);
}
1.5.2.3、 插入字符串
fn main() {
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);
}

1.5.2.4、字符串替换

1、replace 方法使用 两种类型的 字符串;

fn main() {
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace); // 调试使用宏
    let s = "12345";
    let new_s = s.replace("3", "t");
    dbg!(new_s); 
}

2、replacen 方法使用 两种类型的 字符串;

fn main() {
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}

3、replace_range 只使用 String 类型

fn main() {
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}
1.5.2.4、字符串删除

1、pop

fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

2、remove

fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}

3、truncate

fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

4、clear

fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear(); // 相当于string_clear.truncate(0)
    dbg!(string_clear);
}

1.5.2.5、字符串连接

字符串连接 使用 + 或 += 操作符,要求右边的参数必须是字符串的切片引。使用 + 相当于使用 std::string 标准库中的 add 方法

fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // // &string_rust会自动解引用为&str,这是因为deref coercing特性。这个特性能够允许把传进来的&String,在API执行之前转成&str。
    let result = string_append + &string_rust;
    let mut result = result + "!";
    result += "!!!";

    println!("连接字符串 + -> {}", result);
}

1.5.2.6、使用 format!连接字符串

这种方式适用于 String 和 &str,和C/C++提供的sprintf函数类似

fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}
1.5.2.7、转义的方式 \
fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // \u 可以输出一个 unicode 字符
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );
}

1.5.2.7、字符串行连接
fn main() {
    let long_string = "String literals
                    can span multiple lines.
                    The linebreak and indentation here \	
                    can be escaped too!";
    println!("{}", long_string);
}

1.5.2.7、原始字符串

使用 r 开头的字符串不会被转义

fn main() {
    println!("{}", "hello \x52\x75\x73\x74");           // 输出hello Rust
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";    // 原始字符串
    println!("{}", raw_str);        // 输出Escapes don't work here: \x3F \u{211D}
}

1.5.2.8、字符串带双引号问题

rust 提供来 r# 方式来避免引号嵌套的问题。

fn main() {
    // 如果字符串包含双引号,可以在开头和结尾加 #
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // 如果还是有歧义,可以继续增加#,没有限制
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

1.5.2.9、字符数组

由于 rust 的字符串是 utf-8 编码的,而String 类型不允许以字符为单位进行索引。所以 String 提供了 chars() 遍历字符和 bytes() 方法遍历字节。

但是需要从 String 中获取子串是比较困难的,标准库中没有提供相关的方法。

1.6、内存(Memory)和分配(Allocation)

字符出字面量快速高效,是因为硬编码到最终可执行文件之中。这些字符出是不可变的。但是我们不能为每个在编译时大小未知且运行程序时可能改变的文本放入二进制文件的内存块中。

String 类型为了支持可变、可增长的文本片段,我们需要在堆上分配一定数量的内存来保存内容,这就意味着内存必须在运行时从内存分配器中请求,我们需要一种方法,在使用完 String 后将这些内存返回给分配器。

第一部分:当调用String::from时,它的实现请求它所需的内存。
第二部分:在带有垃圾回收器(GC)的语言, GC 会清理不在使用的内存,我们不需要考虑他。在没有GC的语言中我们有责任识别内存不再使用,并且调用代码显式释放它。

Rust 采用不同的方式:一旦拥有的内存的变量超出作用域,内存就会自动返回(Rust 会为我们调用一个特殊的函数叫做 drop)。
在c++中 这种项目生命周期结束时释放资源的模式有时候被称为 资源获取即初始化(RALL)。

1.7、变量与数据交互的方式

1.7.1、移动(Move)

1、赋值

将一个变量赋值给另一个变量会将所有权转移。

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

2、参数传递或函数返回

赋值并不是唯一涉及移动的操作,值在作为参数传递或从函数返回时也会被移动。

3、赋给结构体或 enum

1.7.3、拷贝(copy)

在编译是已知大小的整数类型完全存在栈中,因此可以快速直接复制实际值。

    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);

在 Rust 中有一个特殊的注解 叫做 Copy trait ,我们可以把它放在存储在栈上的类型,就像整数一样,如果一个类型实现了 Copy 特性,那么它的变量就不会移动。

如果类型或其任何部分实现了Drop trait,Rust将不允许我们用Copy注释类型。

实现了 Copy trait的类型

  • 所有的整型类型

  • bool 类型: true、false

  • 浮点类型;f32、f64

  • 字符类型:char

  • 元组(只包含了实现了 Copy trait),例如 (i32,i32) 就是 copy,而( i32,String) 是move;

1.7.2、克隆(clone)

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

    println!("s1 = {}, s2 = {}", s1, s2);

1.8、涉及函数的所有权机制

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    println!("s {}", s);
    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

1.9、函数返回值的所有权机制

fn main() {
  let s1 = gives_ownership();         // gives_ownership moves its return
                                      // value into s1

  let s2 = String::from("hello");     // s2 comes into scope

  let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                      // takes_and_gives_back, which also
                                      // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                           // return value into the function
                                           // that calls it

  let some_string = String::from("yours"); // some_string comes into scope

  some_string                              // some_string is returned and
                                           // moves out to the calling
                                           // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                    // scope

  a_string  // a_string is returned and moves out to the calling function
}

二、引用(Reference)和租借(Borrowing)

2.1、引用(Reference)

引用(Reference) 是 C++ 开发者较为熟悉的概念,如果你熟悉指针的概念,你可以把它当作一种指针。实质上,引用是变量的间接访问方式。

我们使用引用就可以避免所有权移动导致原先的变量不能使用的问题。

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 编程】四、理解 Rust 的所有权概念
因为 s 是 String的引用,它没有所有权,当函数结束的时候,并不会drop。

2.3、租借(Borrowing)

2.3.1、租借

引用不会获得值的所有权,引用只能租借(Borrow)值的所有权。引用本身也是一种类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所有值的所有权。

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:3
  |
8 |   some_string.push_str(", world");
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                        ~~~~~~~~~~~

For more information about this error, try `rustc --explain E0596`.
error: could not compile `hello` (bin "hello") due to previous error

从上述错误提示可以知道 reference 是租借引用,只能读不能进行写操作,我们可以使用 &mut 来进行 可变引用。

2.3.2、Mutable References

使用 Mutable References 可以修改引用的内容。

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

  change(&mut s);
}

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

一个变量只能有一个 Mutable References。

对同一个值有不可变引用的时候,不能有可变引用。

2.4、垂悬引用(Dangling Reference)

垂悬引用 好像也叫做 野指针。

如果在有指针概念的编程语言,它指的是那种没有实际指向一个真正能访问的数据和指针(注意,不一定是空指针,还有可能是已经释放的资源),它们就像失去悬挂物体的绳子,所以叫做垂悬引用。

垂悬引用 在 Rust 语言里面不允许出现,如果有,编译器会发现它

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

很显然,伴随着 dangle 函数的结束,其局部变量的值本身没有被当作返回值,被释放了。但它的引用却被返回,这个引用所指向的值已经不能确定的存在,故不允许其出现。

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                 +++++++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `hello` (bin "hello") due to previous error

三、切片(Slice Type)

切片(Slice) 是对数据值的部分引用。

3.1、字符串切片(String Slice)

最简单、最常用的数据切片类型是字符串切片(String Slice)。

3.2、数组切片

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);

总结

以上就是今天要讲的内容文章来源地址https://www.toymoban.com/news/detail-497106.html

  • 本文介绍 rust的所有权、切片、字符出类型、租借、可变引用,本章节的内容比较难以理解;

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

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

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

相关文章

  • Rust-所有权(ownership)

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

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

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

    2024年02月07日
    浏览(67)
  • 【rust】| 06——语言特性 | 所有权

    系列文章目录 【rust】| 00——开发环境搭建 【rust】| 01——编译并运行第一个rust程序 【rust】| 02——语法基础 | 变量(不可变?)和常量 【rust】| 03——语法基础 | 数据类型 【rust】| 04——语法基础 | 函数 【rust】| 05——语法基础 | 流程控制 【rust】| 06——语言特性 | 所有权  

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

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

    2024年01月18日
    浏览(34)
  • Rust核心功能之一(所有权)

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

    2024年02月04日
    浏览(35)
  • 【Rust】Rust学习 第四章认识所有权

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

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

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

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

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

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

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

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

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

    2024年02月12日
    浏览(30)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包