导图社区 Rust学习记录
这是一篇关于Rust学习记录的思维导图。Rust是一门系统编程语言 ,专注于安全 ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似 ,但是设计者想要在保证性能的同时提供更好的内存安全。
编辑于2021-05-28 15:36:43Rust学习记录
Rust语言
Rust 语言是一种高效、可靠的通用高级语言
语言特点
高性能
Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成
可靠性
Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误
生产力
Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具 —— 包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。
一、学习内容
Rust 基础语法
变量
首先必须说明,Rust 是强类型语言,但具有自动判断变量类型的能力。这很容易让人与弱类型语言产生混淆。
常量与不可变变量的区别
变量和常量还是有区别的。变量的值可以"重新绑定",但在"重新绑定"以前不能私自被改变,这样可以确保在每一次"绑定"之后的区域里编译器可以充分的推理程序逻辑
重影(Shadowing)
重影的概念与其他面向对象语言里的"重写"(Override)或"重载"(Overload)是不一样的。重影就是刚才讲述的所谓"重新绑定",之所以加引号就是为了在没有介绍这个概念的时候代替一下概念
Rust 数据类型
整数型(Integer)
isize 和 usize 两种整数类型是用来衡量数据大小的,它们的位长度取决于所运行的目标平台,如果是 32 位架构的处理器将使用 32 位位长度整型
浮点数型(Floating-Point)
Rust 与其它语言一样支持 32 位浮点数(f32)和 64 位浮点数(f64)。默认情况下,64.0 将表示 64 位浮点数,因为现代计算机处理器对两种浮点数计算的速度几乎相同,但 64 位浮点数精度更高
布尔型
布尔型用 bool 表示,值只能为 true 或 false
字符型
Rust的 char 类型大小为 4 个字节,代表 Unicode标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格在 Rust 中都是有效的 char 值
复合类型
元组用一对 ( ) 包括的一组数据,可以包含不同种类的数据: let tup: (i32, f64, u8) = (500, 6.4, 1); // tup.0 等于 500 // tup.1 等于 6.4 // tup.2 等于 1 let (x, y, z) = tup; // y 等于 6.4
注释
用于说明文档的注释
// 这是第一种注释方式 /* 这是第二种注释方式 */ /* * 多行注释 * 多行注释 * 多行注释 */
函数
函数参数
fn <函数名> ( <参数> ) <函数体> Rust 中定义函数如果需要具备参数必须声明参数名称和类型
函数体的语句和表达式
Rust 函数体由一系列可以以表达式(Expression)结尾的语句(Statement)组成。到目前为止,我们仅见到了没有以表达式结尾的函数,但已经将表达式用作语句的一部分。 let x = 5; let y = { let x = 3; x + 1 }; println!("x 的值为 : {}", x); println!("y 的值为 : {}", y); 函数体表达式并不能等同于函数体,它不能使用 return 关键字
函数返回值
在上一个嵌套的例子中已经显示了 Rust 函数声明返回值类型的方式:在参数声明之后用 -> 来声明函数返回值的类型(不是 : )。 在函数体中,随时都可以以 return 关键字结束函数运行并返回一个类型合适的值。这也是最接近大多数开发者经验的做法: 实例 fn add(a: i32, b: i32) -> i32 { return a + b; }
条件语句
在 Rust 语言中的条件语句使这种格式的: 实例 fn main() { let number = 3; if number < 5 { println!("条件为 true"); } else { println!("条件为 false"); } } 在上述程序中有条件 if 语句,这个语法在很多其它语言中很常见,但也有一些区别:首先,条件表达式 number < 5 不需要用小括号包括(注意,不需要不是不允许);但是 Rust 中的 if 不存在单语句不用加 {} 的规则,不允许使用一个语句代替一个块。尽管如此,Rust 还是支持传统 else-if 两个函数体表达式的类型必须一样!且必须有一个 else 及其后的表达式块。
Rust 中的 if 不存在单语句不用加 {} 的规则,不允许使用一个语句代替一个块
循环
while 循环
while 循环是最典型的条件语句循环: 实例 fn main() { let mut number = 1; while number != 4 { println!("{}", number); number += 1; } println!("EXIT"); }
for 循环
or 循环是最常用的循环结构,常用来遍历一个线性数据据结构(比如数组)。for 循环遍历数组: 实例 fn main() { let a = [10, 20, 30, 40, 50]; for i in a.iter() { println!("值为 : {}", i); } }
loop 循环
身经百战的开发者一定遇到过几次这样的情况:某个循环无法在开头和结尾判断是否继续进行循环,必须在循环体中间某处控制循环的进行。如果遇到这种情况,我们经常会在一个 while (true) 循环体里实现中途退出循环的操作。 Rust 语言有原生的无限循环结构 —— loop: 实例 fn main() { let s = ['R', 'U', 'N', 'O', 'O', 'B']; let mut i = 0; loop { let ch = s[i]; if ch == 'O' { break; } println!("\'{}\'", ch); i += 1; } }
所有权
计算机程序必须在运行时管理它们所使用的内存资源。 大多数的编程语言都有管理内存的功能: C/C++ 这样的语言主要通过手动方式管理内存,开发者需要手动的申请和释放内存资源。但为了提高开发效率,只要不影响程序功能的实现,许多开发者没有及时释放内存的习惯。所以手动管理内存的方式常常造成资源浪费。 Java 语言编写的程序在虚拟机(JVM)中运行,JVM 具备自动回收内存资源的功能。但这种方式常常会降低运行时效率,所以 JVM 会尽可能少的回收资源,这样也会使程序占用较大的内存资源。 所有权对大多数开发者而言是一个新颖的概念,它是 Rust 语言为高效使用内存而设计的语法机制。所有权概念是为了让 Rust 在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念
所有权规则
所有权有以下三条规则: Rust 中的每个值都有一个变量,称为其所有者。 一次只能有一个所有者。 当所有者不在程序运行范围时,该值将被删除。 这三条规则是所有权概念的基础。
变量范围
我们用下面这段程序描述变量范围的概念: { // 在声明以前,变量 s 无效 let s = "runoob"; // 这里是变量 s 的可用范围 } // 变量范围已经结束,变量 s 无效 变量范围是变量的一个属性,其代表变量的可行域,默认从声明变量开始有效直到变量所在域结束。
内存和分配
如果我们定义了一个变量并给它赋予一个值,这个变量的值存在于内存中。这种情况很普遍。但如果我们需要储存的数据长度不确定(比如用户输入的一串字符串),我们就无法在定义时明确数据长度,也就无法在编译阶段令程序分配固定长度的内存空间供数据储存使用。(有人说分配尽可能大的空间可以解决问题,但这个方法很不文明)。这就需要提供一种在程序运行时程序自己申请使用内存的机制——堆。本章所讲的所有"内存资源"都指的是堆所占用的内存空间。 有分配就有释放,程序不能一直占用某个内存资源。因此决定资源是否浪费的关键因素就是资源有没有及时的释放。 我们把字符串样例程序用 C 语言等价编写: { char *s = strdup("runoob"); free(s); // 释放 s 资源 } 很显然,Rust 中没有调用 free 函数来释放字符串 s 的资源(我知道这样在 C 语言中是不正确的写法,因为 "runoob" 不在堆中,这里假设它在) 。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。 这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。
变量与数据交互的方式
变量与数据交互方式主要有移动(Move)和克隆(Clone)两种:
移动
多个变量可以在 Rust 中以不同的方式与相同的数据交互 但如果发生交互的数据在堆中就是另外一种情况: let s1 = String::from("hello"); let s2 = s1; 第一步产生一个 String 对象,值为 "hello"。其中 "hello" 可以认为是类似于长度不确定的数据,需要在堆中存储。 第二步的情况略有不同(这不是完全真的,仅用来对比参考): 如图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 "hello" 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。 前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 "hello" 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。
克隆
Rust会尽可能地降低程序的运行成本,所以默认情况下,长度较大的数据存放在堆中,且采用移动的方式进行数据交互。但如果需要将数据单纯的复制一份以供他用,可以使用数据的第二种交互方式——克隆。 实例 fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); } 运行结果: s1 = hello, s2 = hello 这里是真的将堆中的 "hello" 复制了一份,所以 s1 和 s2 都分别绑定了一个值,释放的时候也会被当作两个资源。 当然,克隆仅在需要复制的情况下使用,毕竟复制数据会花费更多的时间。
涉及函数的所有权机制
对于变量来说这是最复杂的情况了。 如果将一个变量当作函数的参数传给其他函数,怎样安全的处理所有权呢? 下面这段程序描述了这种情况下所有权机制的运行原理: 实例 fn main() { let s = String::from("hello"); // s 被声明有效 takes_ownership(s); // s 的值被当作参数传入函数 // 所以可以当作 s 已经被移动,从这里开始已经无效 let x = 5; // x 被声明有效 makes_copy(x); // x 的值被当作参数传入函数 // 但 x 是基本类型,依然有效 // 在这里依然可以使用 x 却不能使用 s } // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放 fn takes_ownership(some_string: String) { // 一个 String 参数 some_string 传入,有效 println!("{}", some_string); } // 函数结束, 参数 some_string 在这里释放 fn makes_copy(some_integer: i32) { // 一个 i32 参数 some_integer 传入,有效 println!("{}", some_integer); } // 函数结束, 参数 some_integer 是基本类型, 无需释放 如果将变量当作参数传入函数,那么它和移动的效果是一样的。
函数返回值的所有权机制
n main() { let s1 = gives_ownership(); // gives_ownership 移动它的返回值到 s1 let s2 = String::from("hello"); // s2 被声明有效 let s3 = takes_and_gives_back(s2); // s2 被当作参数移动, s3 获得返回值所有权 } // s3 无效被释放, s2 被移动, s1 无效被释放. fn gives_ownership() -> String { let some_string = String::from("hello"); // some_string 被声明有效 return some_string; // some_string 被当作返回值移动出函数 } fn takes_and_gives_back(a_string: String) -> String { // a_string 被声明有效 a_string // a_string 被当作返回值移出函数 } 被当作函数返回值的变量所有权将会被移动出函数并返回到调用函数的地方,而不会直接被无效释放。
引用与租借
引用(Reference)
引用(Reference)是 C++ 开发者较为熟悉的概念。 如果你熟悉指针的概念,你可以把它看作一种指针。 实质上"引用"是变量的间接访问方式。 实例 fn main() { let s1 = String::from("hello"); let s2 = &s1; println!("s1 is {}, s2 is {}", s1, s2); }
垂悬引用(Dangling References)
Slice(切片)类型
结构体
枚举类
组织管理
错误处理
泛型与特性
生命周期
文件与 IO
集合与字符串
泛型与特性
面向对象
并发编程
二、学习心得体会