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

系列文章目录

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

文章目录

  • 系列文章目录
  • 前言
  • 一、所有权(Ownership)
    • 1.1.、所有权(Ownership)
    • 1.2、栈(Stack)和堆(Heap)
    • 1.3、所有权规则(Ownership Rules)
    • 1.4、变量作用域(Variable Scope)
    • 1.5、字符串类型(String Type)
      • 1.5.1、字符串切片引用(&str 类型)
      • 1.5.2、String类型
        • 1.5.2.1、 String 字符串介绍
        • 1.5.2.2、 创建 String 字符串
        • 1.5.2.2、 追加字符串
        • 1.5.2.3、 插入字符串
        • 1.5.2.4、字符串替换
        • 1.5.2.4、字符串删除
        • 1.5.2.5、字符串连接
        • 1.5.2.6、使用 format!连接字符串
        • 1.5.2.7、转义的方式 `\`
        • 1.5.2.7、字符串行连接
        • 1.5.2.7、原始字符串
        • 1.5.2.8、字符串带双引号问题
        • 1.5.2.9、字符数组
    • 1.6、内存(Memory)和分配(Allocation)
    • 1.7、变量与数据交互的方式
      • 1.7.1、移动(Move)
      • 1.7.3、拷贝(copy)
      • 1.7.2、克隆(clone)
    • 1.8、涉及函数的所有权机制
    • 1.9、函数返回值的所有权机制
  • 二、引用(Reference)和租借(Borrowing)
    • 2.1、引用(Reference)
    • 2.3、租借(Borrowing)
      • 2.3.1、租借
      • 2.3.2、Mutable References
    • 2.4、垂悬引用(Dangling Reference)
  • 三、切片(Slice Type)
    • 3.1、字符串切片(String Slice)
    • 3.2、数组切片
  • 总结


前言

本章节将讲解 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 declaredlet 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 literalscan 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 Rustlet 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 scopetakes_ownership(s);             // s's value moves into the function...// ... and so is no longer valid hereprintln!("s {}", s);let x = 5;                      // x comes into scopemakes_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 scopeprintln!("{}", 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 scopeprintln!("{}", 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 s1let s2 = String::from("hello");     // s2 comes into scopelet 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 itlet some_string = String::from("yours"); // some_string comes into scopesome_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// scopea_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()
}


因为 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]);

总结

以上就是今天要讲的内容

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

本文链接:https://my.lmcjl.com/post/12479.html

展开阅读全文

4 评论

留下您的评论.