元数据
深入浅出Rust
- 书名: 深入浅出Rust
- 作者: 范长春
- 简介: 本书详细描述了Rust语言的基本语法,穿插讲解一部分高级使用技巧,并以更容易理解的方式解释其背后的设计思想。全书总共分五个部分。第一部分介绍Rust基本语法。第二部分介绍属于Rust独一无二的内存管理方式。这部分是本书的重点和核心所在,也是Rust语言的思想内核精髓之处。第三部分介绍Rust的抽象表达能力。第四部分介绍并发模型。在目前这个阶段,对并行编程的支持是新一代编程语言不可绕过的重要话题。第五部分介绍一些实用设施。Rust语言有许多创新,但它绝不是高高在上孤芳自赏的类型,设计者在设计过程中充分考虑了语言的工程实用性。
- 出版时间: 2018-08-01 00:00:00
- ISBN: 9787111606420
- 分类: 计算机-编程设计
- 出版社: 机械工业出版社
- PC地址:https://weread.qq.com/web/reader/97d324d0715ec2fb97d76a6
高亮划线
前言
📌 在强大的内存安全特性的支持下,Rust一举解决了并发条件下的数据竞争(Data Race)问题。它从编译阶段就将数据竞争解决在了萌芽状态,保障了线程安全。 ⏱ 2024-03-30 21:01:15
第1章 与君初相见
📌 所有大功能必须先写好设计文档,讲清楚设计的目标、实现方式、优缺点等,让整个社区参与讨论,然后由“核心组”(Core Team)的成员参与定夺是否接受这个设计。 ⏱ 2024-10-27 16:36:12
📌 先编译警告,再编译错误,这个过程可能会持续好几年,所以Rust的稳定性还是基本上有保证的。 ⏱ 2024-10-27 16:37:03
📌 可以把它理解为一种安全版的编译期语法扩展。这里之所以使用宏,而不是函数,是因为标准输出宏可以完成编译期格式检查,更加安全。 ⏱ 2024-10-27 16:39:15
📌 。每个crate是一个完整的编译单元,它可以生成为一个lib或者exe可执行文件。 ⏱ 2024-10-27 16:39:35
📌 而在crate内部,则是由mod这个概念管理的,所谓mod大家可以理解为namespace。 ⏱ 2024-10-27 16:39:49
第2章 变量和类型
📌 局部变量声明一定是以关键字let开头,类型一定是跟在冒号:的后面。语法歧义更少,语法分析器更容易编写。 ⏱ 2024-10-27 16:41:17
📌 pattern destructure(模式解构) ⏱ 2024-10-27 16:45:30
📌 如果我们需要让变量是可写的,那么需要使用mut关键字 ⏱ 2024-10-27 16:45:18
📌 mut x是一个“模式” ⏱ 2024-10-27 16:46:01
📌 ,在Rust中,一般把声明的局部变量并初始化的语句称为“变量绑定”, ⏱ 2024-10-27 16:46:27
📌 Rust中,每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误 ⏱ 2024-10-27 16:46:33
📌 类型没有“默认构造函数”,变量没有“默认值”。对于let x: i32;如果没有显式赋值,它就没有被初始化,不要想当然地以为它的值是0。 ⏱ 2024-10-27 16:47:18
📌 Rust里面的下划线是一个特殊的标识符,在编译器内部它是被特殊处理的。它跟其他标识符有许多重要区别。 ⏱ 2024-10-27 16:47:40
📌 下划线表达的含义是“忽略这个变量绑定,后面不会再用到了”。 ⏱ 2024-10-27 16:47:48
📌 Rust允许在同一个代码块中声明同样名字的变量。 ⏱ 2024-10-27 16:47:52
📌 我们没有任何办法再去访问前一个x变量,因为它的名字已经被遮蔽了。 ⏱ 2024-10-27 16:48:08
📌 一个“不可变绑定”依然是一个“变量”。虽然我们没办法通过这个“变量绑定”修改变量的值,但是我们重新使用“可变绑定”之后,还是有机会修改的。 ⏱ 2024-10-27 16:48:49
📌 我们可以用type关键字给同一个类型起个别名(type alias)。示例如下: type Age = u32; ⏱ 2024-10-27 21:27:58
📌 Rust中可以用static关键字声明静态变量 ⏱ 2024-10-27 21:28:25
📌 用static声明的变量的生命周期是整个程序,从启动到退出 ⏱ 2024-10-27 21:28:47
📌 在Rust中,如果用户需要使用比较复杂的全局变量初始化,推荐使用lazy_static库。 ⏱ 2024-10-27 21:30:26
📌 使用const声明的是常量,而不是变量 ⏱ 2024-10-27 21:30:36
📌 它与static变量的最大区别在于:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化 ⏱ 2024-10-27 21:30:49
📌 字符类型由char表示。它可以描述任何一个符合unicode标准的字符值。在代码中,单个的字符字面量用单引号包围。 ⏱ 2024-10-28 10:05:07
📌 因为char类型的设计目的是描述任意一个unicode字符,因此它占据的内存空间不是1个字节,而是4个字节。 ⏱ 2024-10-28 10:05:14
📌 我们可以使用一个字母b在字符或者字符串前面,代表这个字面量存储在u8类型数组中 ⏱ 2024-10-28 10:05:26
📌 需要特别关注的是isize和usize类型。它们占据的空间是不定的,与指针占据的空间一致,与所在的平台相关 ⏱ 2024-10-28 10:05:54
📌 在所有的数字字面量中,可以在任意地方添加任意的下划线,以方便阅读 ⏱ 2024-10-28 10:06:08
📌 字面量后面可以跟后缀,可代表该数字的具体类型,从而省略掉显示类型标记 ⏱ 2024-10-28 10:06:24
📌 对于整数类型,如果Rust编译器通过上下文无法分析出该变量的具体类型,则自动默认为i32类型。 ⏱ 2024-10-28 10:07:05
📌 默认情况下,在debug模式下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发panic;在release模式下,不检查整数溢出,而是采用自动舍弃高位的方式。示例如下: ⏱ 2024-10-28 10:07:32
📌 内存分配方式可以取决于使用方式,与类型本身无关。 ⏱ 2024-10-28 13:33:18
📌 Rust提供了一个关键字as,专门用于这样的类型转换: ⏱ 2024-10-28 13:34:19
📌 Rust设计者希望在发生类型转换的时候不是偷偷摸摸进行的,而是显式地标记出来,防止隐藏的bug。 ⏱ 2024-10-28 13:34:29
📌 甚至需要连续写多个as才能转成功,比如&i32类型就不能直接转换为*mut i32类型,必须像下面这样写才可以 ⏱ 2024-10-28 13:34:53
📌 tuple内的元素没有名字。tuple是把几个类型组合到一起的最简单的方式。 ⏱ 2024-10-28 13:35:43
📌 如果元组中只包含一个元素,应该在后面添加一个逗号,以区分括号表达式和元组: ⏱ 2024-10-28 13:35:51
📌 访问元组内部元素有两种方法,一种是“模式匹配”(pattern destructuring),另外一种是“数字索引”: ⏱ 2024-10-28 13:35:59
📌 元组内部也可以一个元素都没有。这个类型单独有一个名字,叫unit(单元类型) ⏱ 2024-10-28 13:36:49
📌 unit类型是Rust中最简单的类型之一,也是占用空间最小的类型之一。空元组和空结构体struct Foo;一样,都是占用0内存空间。 ⏱ 2024-10-28 13:36:55
📌 Rust允许struct类型的初始化使用一种简化的写法。如果有局部变量名字和成员变量名字恰好一致,那么可以省略掉重复的冒号初始化: ⏱ 2024-10-28 13:38:53
📌 tuple struct有名字,而它们的成员没有名字 ⏱ 2024-10-28 13:39:48
📌 它们有一致的内存对齐策略、一致的占用空间规则,也有类似的语法。 ⏱ 2024-10-28 13:40:12
📌 而使用tuple struct做包装,则是创造了一个全新的类型,它跟被包装的类型不能发生隐式类型转换,可以具有不同的方法,满足不同的trait,完全按需而定。 ⏱ 2024-10-28 13:40:49
📌 与C/C++中的枚举相比,Rust中的enum要强大得多,它可以为每个成员指定附属的类型信息。 ⏱ 2024-10-28 13:41:11
📌 Rust的enum与C/C++的enum和union都不一样。它是一种更安全的类型,可以被称为“tagged union”。 ⏱ 2024-10-28 13:41:49
📌 在Rust中,enum和struct为内部成员创建了新的名字空间。如果要访问内部成员,可以使用::符号。 ⏱ 2024-10-28 13:43:59
📌 Rust的enum实际上是一种代数类型系统(Algebraic Data Type, ADT) ⏱ 2024-10-28 13:45:59
📌 enum内部的variant只是一个名字而已,恰好我们还可以将这个名字作为类型构造器使用 ⏱ 2024-10-28 13:46:12
📌 Rust里面的复合数据类型是允许递归定义的。 ⏱ 2024-10-28 13:48:35
📌 我们把产生了递归的那个成员类型改为了指针,这个类型就非常合理了。 ⏱ 2024-10-28 13:49:05
第3章 语句和表达式
📌 一个表达式总是会产生一个值,因此它必然有类型;语句不产生值,它的类型永远是()。 ⏱ 2024-10-28 13:49:19
📌 所谓左值,意思是这个表达式可以表达一个内存地址。因此,它们可以放到赋值运算符左边使用。其他的都是右值。 ⏱ 2024-10-28 13:49:52
📌 逻辑与”、“逻辑或”具备“短路”功能。 ⏱ 2024-10-28 13:50:34
📌 Rust不支持++、—运算符,请使用+= 1、-= 1替代。 ⏱ 2024-10-28 13:52:23
📌 语句和表达式的区分方式是后面带不带分号(;)。如果带了分号,意味着这是一条语句,它的类型是();如果不带分号,它的类型就是表达式的类型。 ⏱ 2024-10-28 13:54:40
📌 语句的效果是一样的,相较于return语句来说没有什么区别,但是更加简洁。 ⏱ 2024-10-28 13:55:03
📌 在if语句中,后续的结果语句块要求一定要用大括号包起来,不能省略,以便明确指出该if语句块的作用范围。这个规定是为了避免“悬空else”导致的bug ⏱ 2024-10-28 13:55:28
📌 在Rust中,没有必要专门设计像C/C++那样的三元运算符(? :)语法,因为通过现有的设计可以轻松实现同样的功能。而且笔者认为这样的语法一致性、扩展性、可读性更好 ⏱ 2024-10-28 13:55:57
📌 Rust中,使用loop表示一个无限死循环。示例如下: ⏱ 2024-10-28 13:56:13
📌 我们可以在loop while for循环前面加上“生命周期标识符”。该标识符以单引号开头,在内部的循环中可以使用break语句选择跳出到哪一层。 ⏱ 2024-10-28 13:57:00
📌 在loop内部break的后面可以跟一个表达式,这个表达式就是最终的loop表达式的值。如果一个loop永远不返回,那么它的类型就是“发散类型” ⏱ 2024-10-28 13:59:03
📌 编译器可以判断出v的类型是发散类型,而后面的打印语句是永远不会执行的死代码。 ⏱ 2024-10-28 13:59:15
📌 loop和while true语句在运行时没有什么区别,它们主要是会影响编译器内部的静态分析结果。 ⏱ 2024-10-28 13:59:42
📌 Rust中的for循环实际上是许多其他语言中的for-each循环。Rust中没有类似C/C++的三段式for循环语句。 ⏱ 2024-10-28 14:00:10
第4章 函数
📌 函数返回可以使用return语句,也可以使用表达式。 ⏱ 2024-10-29 08:08:40
📌 每一个函数都具有自己单独的类型, ⏱ 2024-10-29 08:10:32
📌 虽然add1和add2有同样的参数类型和同样的返回值类型,但它们是不同类型,所以这里报错了。修复方案是让func的类型为通用的fn类型即可: ⏱ 2024-10-29 08:10:53
📌 Rust的函数体内也允许定义其他item,包括静态变量、常量、函数、trait、类型、模块等 ⏱ 2024-10-29 08:11:26
📌 Rust支持一种特殊的发散函数(Diverging functions),它的返回类型是感叹号!。 ⏱ 2024-10-29 08:11:39
📌 发散类型的最大特点就是,它可以被转换为任意一个类型。 ⏱ 2024-10-29 08:11:55
📌 呢?这就是!类型的作用了。因为它可以与任意类型相容,所以编译器的类型检查才能通过。在Rust中,有以下这些情况永远不会返回,它们的类型就是!。 ⏱ 2024-10-29 08:13:03
📌 Rust的设计稍微有点不一样,传递参数和返回状态码都由单独的API来完成, ⏱ 2024-10-29 08:13:18
📌 如果要读取环境变量,可以用std::env::var()以及std::env::vars()函数获得。 ⏱ 2024-10-29 08:15:11
📌 Rust的main函数只支持无参数、无返回值类型的声明方式,即main函数的签名固定为:fn main() → ()。 ⏱ 2024-10-29 08:15:24
📌 函数可以用const关键字修饰,这样的函数可以在编译阶段被编译器执行,返回值也被视为编译期常量。 ⏱ 2024-10-29 08:15:43
📌 谈到递归调用,许多读者都会自然联想到“尾递归优化”这个概念。可惜的是,当前版本的Rust暂时还不支持尾递归优化,因此如果递归调用层次太多的话,是有可能撑爆栈空间的。 ⏱ 2024-10-29 08:16:30
第5章 trait
📌 所有的trait中都有一个隐藏的类型Self(大写S),代表当前这个实现了此trait的具体类型 ⏱ 2024-10-29 08:17:20
📌 没有receiver参数的函数,我们称为“静态函数”(static function),可以通过类型加双冒号::的方式来调用。在Rust中,函数和方法没有本质区别。 ⏱ 2024-10-29 08:17:42
📌 Rust中Self(大写S)和self(小写s)都是关键字,大写S的是类型名,小写s的是变量名。 ⏱ 2024-10-29 22:43:02
📌 对于第一个self参数,常见的类型有self :Self、self : &Self、self : &mut Self等类型。 ⏱ 2024-10-29 22:43:34
📌 针对一个类型,我们可以直接对它impl来增加成员方法,无须trait名字 ⏱ 2024-10-29 22:44:28
📌 trait中可以包含方法的默认实现。如果这个方法在trait中已经有了方法体,那么在针对具体类型实现的时候,就可以选择不用重写 ⏱ 2024-10-29 22:44:51
📌 self参数甚至可以是Box指针类型self : Box
。 ⏱ 2024-10-29 22:45:31 ^22987515-9-3538-3579
📌 impl的对象甚至可以是trait。 ⏱ 2024-10-29 22:45:43
📌 impl Shape for Round和impl<T: Round> Shape for T ⏱ 2024-10-29 22:57:21
📌 在将来,trait object的语法会被要求加上dyn关键字,所以在Rust 2018 edition以后应该写成impl Shape for dyn Round才合理。 ⏱ 2024-10-29 23:21:41
📌 没有receiver参数的方法(第一个参数不是self参数的方法)称作“静态方法” ⏱ 2024-10-29 23:21:55
📌 需要注意的是,即便我们的第一个参数是Self相关类型,只要变量名字不是self,就不能使用小数点的语法调用函数。 ⏱ 2024-10-29 23:22:20
📌 Rust中没有“构造函数”的概念。Default trait实际上可以看作一个针对无参数构造函数的统一抽象。 ⏱ 2024-10-29 23:23:06
📌 在Rust中,定义静态函数没必要使用static关键字,因为它把self参数显式在参数列表中列出来了。 ⏱ 2024-10-29 23:23:19
📌 我们还可以利用trait给其他的类型添加成员方法,哪怕这个类型不是我们自己写的。 ⏱ 2024-10-29 23:23:58
📌 Rust规定了一个Coherence Rule(一致性规则)或称为Orphan Rule(孤儿规则):impl块要么与trait的声明在同一个的crate中,要么与类型的声明在同一个crate中。 ⏱ 2024-10-29 23:24:16
📌 目前版本的Rust规定,在函数参数传递、返回值传递等地方,都要求这个类型在编译阶段有确定的大小。否则,编译器就不知道该如何生成代码了。 ⏱ 2024-10-29 23:25:26
📌 它的具体写法为
::item ⏱ 2024-10-30 00:19:40 ^22987515-9-10194-10229
📌 所谓的“成员方法”也没什么特殊之处,它跟普通的静态方法的唯一区别是,第一个参数是self,而这个self只是一个普通的函数参数而已。 ⏱ 2024-10-30 00:20:23
📌 “隐藏着”的“取引用”步骤。虽然我们看起来源代码长的是这个样子me.start(),但是大家心里要清楚,真正传递给start()方法的参数是&me而不是me,这一步是编译器自动帮我们做的 ⏱ 2024-10-30 00:20:41
📌 Rust的trait的另外一个大用处是,作为泛型约束使用 ⏱ 2024-10-30 00:21:44
📌 。冒号后面加trait名字,就是这个泛型参数的约束条件。它要求这个T类型实现Debug这个trait。 ⏱ 2024-10-30 00:22:29
📌 在标准库中,很多trait之间都有继承关系 ⏱ 2024-10-30 00:23:44
📌 只有实现了Display trait的类型,才能用{}格式控制打印出来;只有实现了Debug trait的类型,才能用{:? } {:#? }格式控制打印出来。它们之间更多的区别如下。 ⏱ 2024-10-30 19:48:20
📌 Rust中的PartialOrd trait实际上就是C++20中即将加入的three-way comparison运算符⇐>。 ⏱ 2024-11-02 10:43:36
📌 一个类型是否满足Sized约束是完全由编译器推导的,用户无权指定。 ⏱ 2024-11-03 15:37:49
📌 Rust中对于动态大小类型专门有一个名词Dynamic Sized Type。我们后面将会看到的[T], str以及dyn Trait都是DST。 ⏱ 2024-11-03 15:38:14
第6章 数组和字符串
📌 数组是一个容器,它在一块连续空间内存中,存储了一系列的同样类型的数据。数组中元素的占用空间大小必须是编译期确定的。 ⏱ 2024-11-03 15:39:39
📌 把数组xs作为参数传给一个函数,这个数组并不会退化成一个指针。而是会将这个数组完整复制进这个函数。函数体内对数组的改动不会影响到外面的数组。 ⏱ 2024-11-03 15:40:18
📌 我们可以直接实现数组的比较操作,只要它包含的元素是可以比较的 ⏱ 2024-11-03 16:40:25
📌 在目前的标准库中,数组本身没有实现IntoIterator trait,但是数组切片是实现了的。所以我们可以直接在for in循环中使用数组切片,而不能直接使用数组本身。 ⏱ 2024-11-03 16:40:45
📌 对数组取借用borrow操作,可以生成一个“数组切片”(Slice)。 ⏱ 2024-11-03 16:41:02
📌 我们可以把数组切片看作专门用于指向数组的指针,是对数组的另外一个“视图”。比 ⏱ 2024-11-03 16:41:10
📌 它不过是在类型系统中丢弃了编译阶段定长数组类型的长度信息,而将此长度信息存储为运行期的值 ⏱ 2024-11-03 16:41:34
📌 Slice与普通的指针是不同的,它有一个非常形象的名字:胖指针(fat pointer)。 ⏱ 2024-11-03 17:55:16
📌 所谓的DST指的是编译阶段无法确定占用空间大小的类型。为了安全性,指向DST的指针一般是胖指针。 ⏱ 2024-11-03 17:55:25
📌 struct中只有最后一个元素可以是DST, ⏱ 2024-11-03 18:02:49
📌 胖指针的设计,避免了数组类型作为参数传递时自动退化为裸指针类型,丢失了长度信息的问题,保证了类型安全; ⏱ 2024-11-03 18:03:05
📌 Rust中的Range代表一个“区间”,一个“范围”,它有内置的语法支持,就是两个小数点..。 ⏱ 2024-11-03 18:03:59
📌 数组和Range之间最常用的配合就是使用Range进行索引操作。 ⏱ 2024-11-03 18:05:24
📌 在许多时候,使用数组的一部分切片作为被操作对象在函数间传递,既保证了效率(避免直接复制大数组),又能保证将所需要执行的操作限制在一个可控制的范围内(有长度信息,有越界检查),还能控制其读写权限,非常有用。 ⏱ 2024-11-03 18:05:41
📌 Rust还提供了一种左闭右闭区间的语法,它使用这种语法来表示..=。 ⏱ 2024-11-03 18:05:56
📌 Rust目前还无法任意索引执行编译阶段边界检查,但是在运行阶段执行了边界检查 ⏱ 2024-11-03 18:06:42
📌 在Rust中,“索引”操作也是一个通用的运算符,是可以自行扩展的。如果希望某个类型可以执行“索引”读操作,就需要该类型实现std::ops::Index trait,如果希望某个类型可以执行“索引”写操作,就需要该类型实现std::ops::IndexMut trait。 ⏱ 2024-11-03 18:07:08
📌 一般情况下,Rust不鼓励大量使用“索引”操作。正常的“索引”操作都会执行一次“边界检查”。从执行效率上来说,Rust比C/C++的数组索引效率低一点,因为C/C++的索引操作是不执行任何安全性检查的,它们对应的Rust代码相当于调用get_unchecked()函数。 ⏱ 2024-11-03 18:08:08
📌 Rust的字符串显得有点复杂,主要是跟所有权有关。Rust的字符串涉及两种类型,一种是&str,另外一种是String。 ⏱ 2024-11-03 18:09:24
📌 &str类型是对一块字符串区间的借用 ⏱ 2024-11-03 18:13:25
📌 这是因为String类型在堆上动态申请了一块内存空间,它有权对这块内存空间进行扩容, ⏱ 2024-11-03 18:13:47
📌 如果用C++来对比,Rust的String类型类似于std::string,而Rust的&str类型类似于std::string_view。 ⏱ 2024-11-03 18:14:40
第7章 模式解构
📌 Rust中模式解构功能设计得非常美观,它的原则是:构造和解构遵循类似的语法,我们怎么把一个数据结构组合起来的,我们就怎么把它拆解开来。 ⏱ 2024-11-03 18:22:52
📌 Rust的“模式解构”功能不仅出现在let语句中,还可以出现在match、if let、while let、函数调用、闭包调用等情景中。 ⏱ 2024-11-03 18:23:11
📌 exhaustive意思是无遗漏的、穷尽的、彻底的、全面的。exhaustive是Rust模式匹配的重要特点。有些时候我们不想把每种情况一一列出,可以用一个下划线来表达“除了列出来的那些之外的其他情况” ⏱ 2024-11-03 18:23:36
📌 上游库作者可以用一个叫作“non_exhaustive”的attribute来标记一个enum或者struct,这样在另外一个项目中使用这个类型的时候,无论如何都没办法在match表达式中通过列举所有的成员实现完整匹配,必须使用下划线才能完成编译。 ⏱ 2024-11-03 18:24:04
📌 下划线还能用在模式匹配的各种地方,用来表示一个占位符,虽然匹配到了但是忽略它的值的情况 ⏱ 2024-11-03 18:24:12
📌 ,下划线更像是一个“关键字”,而不是普通的“标识符”(identifier),把它当成普通标识符使用是会有问题的。 ⏱ 2024-11-03 18:32:03
📌 除了下划线可以在模式中作为“占位符”,还有两个点..也可以在模式中作为“占位符”使用。下划线表示省略一个元素,两个点可以表示省略多个元素。 ⏱ 2024-11-03 18:33:39
📌 我们可以使用或运算符|来匹配多个条件, ⏱ 2024-11-03 18:36:30
📌 可以使用范围作为匹配条件,使用..表示一个前闭后开区间范围,使用..=表示一个闭区间范围 ⏱ 2024-11-03 18:36:39
📌 如果不同分支覆盖范围出现了重叠,各个分支之间的先后顺序就有影响了 ⏱ 2024-11-03 18:39:00
📌 我们可以使用@符号绑定变量。@符号前面是新声明的变量,后面是需要匹配的模式 ⏱ 2024-11-03 18:39:48
📌 如果在使用@的同时使用|,需要保证在每个条件上都绑定这个名字: ⏱ 2024-11-04 08:58:30
📌 ref是“模式”的一部分,它只能出现在赋值号左边,而&符号是借用运算符,是表达式的一部分,它只能出现在赋值号右边。 ⏱ 2024-11-04 08:59:04
📌 Rust中,所有的变量绑定默认都是“不可更改”的。只有使用了mut修饰的变量绑定才有修改数据的能力 ⏱ 2024-11-04 09:02:07
📌 重新绑定与前面提到的“变量遮蔽”(shadowing)是完全不同的作用机制。“重新绑定”要求变量本身有mut修饰,并且不能改变这个变量的类型。“变量遮蔽”要求必须重新声明一个新的变量,这个新变量与老变量之间的类型可以毫无关系。 ⏱ 2024-11-04 09:02:40
📌 因为编译器认为这个match语句把内部的String变量移动出来了,所以后续的打印x的值是错误的行为。 ⏱ 2024-11-04 09:11:18
📌 Rust还提供了if-let语法糖。它的语法为if let PATTERN = EXPRESSION { BODY }。后面可以跟一个可选的else分支。 ⏱ 2024-11-04 09:12:01
📌 :match一定要完整匹配,if-let只匹配感兴趣的某个特定的分支,这种情况下的写法比match简单点。 ⏱ 2024-11-04 09:12:45
第8章 深入类型系统
📌 Rust的类型系统实际上是一种代数类型系统(Algebraic data type)。 ⏱ 2024-11-04 09:14:08
📌 一个类型所有取值的可能性叫作这个类型的“基数” ⏱ 2024-11-04 09:14:52
📌 如果两个类型的基数是一样的,那么我们可以说它们携带的信息量其实是一样的,我们也可以说它们是“同构”的 ⏱ 2024-11-04 09:15:10
📌 空的enum可以类比为数字0; unit类型或者空结构体可以类比为数字1; enum类型可以类比为代数运算中的求和;tuple、struct可以类比为代数运算中的求积;数组可以类比为代数运算中的乘方。 ⏱ 2024-11-04 09:16:44
📌 Rust里面其实也有一个专门的类型来表示never,也就是我们前面提到过的感叹号!。 ⏱ 2024-11-04 09:24:42
📌 null实际上是在类型系统上打开了一个缺口,引入一个必须在运行期特殊处理的特殊“值”。 ⏱ 2024-11-04 13:17:54
📌 而unwrap方法则是从Option
中提取出T。如果当前状态是None,那么这个函数会执行失败导致panic。正因为如此,除非是写不重要的小工具之类的,否则这个方法是不推荐使用的。 ⏱ 2024-11-04 13:36:34 ^22987515-12-13269-13368
📌 哪怕你很确定此时Option的状态一定是Some,最好也用expect方法代替,至少它在发生panic的时候还会打印出一个字符串,方便我们查找原因。 ⏱ 2024-11-04 13:36:46
📌 根据Rust的设计,借用指针&和所有权指针Box从语义上来说,都是不可能为“0”的状态。有些数值是不可能成为这几个指针指向的地址的,它们的取值范围实际上小于isize类型的取值范围。 ⏱ 2024-11-04 13:38:17
📌 Option<Box
>的实际内部表示形式是Option<NonZero<*const T>>。因此编译器就有能力将这个类型的占用空间压缩到与*const T类型占用的空间一致。 ⏱ 2024-11-05 08:25:23 ^22987515-12-16763-16878
📌 相对于裸指针,使用Option包装的指针类型的执行效率不会降低,这是“零开销抽象”。 ⏱ 2024-11-05 08:49:40
第9章 宏
📌 Rust的“宏”(macro)是一种编译器扩展,它的调用方式为some_macro! (…)。宏调用与普通函数调用的区别可以一眼区分开来,凡是宏调用后面都跟着一个感叹号。 ⏱ 2024-11-05 08:51:33
📌 这就是所谓的“过程宏”(procedural macro)。它是直接用Rust语言写出来的,相当于一个编译器插件。 ⏱ 2024-11-05 09:02:41
第10章 内存管理基础
📌 发生panic的时候,此处就是确定性的第一现场,我们可以根据call stack信息很快找到事发地点,然后修复。panic是防止更严重内存安全错误的重要机制。 ⏱ 2024-11-05 09:11:36
第11章 所有权和移动语义
📌 在Rust里面,不可以做“赋值运算符重载”,若需要“深复制”,必须手工调用clone方法。 ⏱ 2024-11-05 09:18:17
📌 Rust中的变量绑定操作,默认是move语义 ⏱ 2024-11-05 09:19:27
📌 我们可以把移动语义想象成执行了一个memcpy,但真实的汇编代码未必如此 ⏱ 2024-11-05 09:20:41
📌 默认的move语义是Rust的一个重要设计,但是任何时候需要复制都去调用clone函数会显得非常烦琐。对于一些简单类型,比如整数、bool,让它们在赋值的时候默认采用复制操作会让语言更简单 ⏱ 2024-11-05 09:21:14
📌 对于自定义类型,默认是没有实现Copy trait的,但是我们可以手动添上。 ⏱ 2024-11-05 09:21:54
📌 Rust提供了一个编译器扩展derive attribute,来帮我们写这些代码,其使用方式为#[derive(Copy, Clone)]。 ⏱ 2024-11-05 09:22:37
📌 Box类型是Rust中一种常用的指针类型。它代表“拥有所有权的指针”,类似于C++里面的unique_ptr(严格来说,unique_ptr
更像Option<Box >)。 ⏱ 2024-11-05 09:22:51 ^22987515-16-8387-8496
📌 目前稳定的有四个,它们是Copy、Send、Sized、Sync ⏱ 2024-11-05 09:23:46
📌 一旦一个类型实现了Copy trait,那么它在变量绑定、函数参数传递、函数返回值传递等场景下,都是copy语义,而不再是默认的move语义。 ⏱ 2024-11-05 09:24:07
📌 我们可以认为,Rust中只有POD(C++语言中的Plain Old Data)类型才有资格实现Copy trait。在 ⏱ 2024-11-05 09:28:25
📌 clone方法一般用于“基于语义的复制”操作。 ⏱ 2024-11-05 09:29:49
📌 对于实现了copy的类型,它的clone方法应该跟copy语义相容,等同于按字节复制。 ⏱ 2024-11-05 09:30:09
📌 Copy trait是给编译器用的,告诉编译器这个类型默认采用copy语义,而不是move语义 ⏱ 2024-11-05 09:30:46
📌 在Rust中编写“析构函数”的办法是impl std::ops::Drop ⏱ 2024-11-05 09:32:44
📌 对于具有多个局部变量的情况,析构函数的调用顺序是:先构造的后析构,后构造的先析构。因为局部变量存在于一个“栈”的结构中,要保持“先进后出”的策略。 ⏱ 2024-11-05 09:33:02
📌 RAII手法是比GC更通用的资源管理手段,GC只能管理内存,RAII可以管理各种资源。 ⏱ 2024-11-05 09:33:12
📌 drop函数不需要任何的函数体,只需要参数为“值传递”即可。将对象的所有权移入函数中,什么都不用做,编译器就会自动释放掉这个对象了。 ⏱ 2024-11-05 09:35:32
📌 变量遮蔽(Shadowing)不会导致变量生命周期提前结束,它不等同于drop。 ⏱ 2024-11-05 09:36:41
📌 另外还有一个小问题需要提醒读者注意,那就是下划线这个特殊符号。请注意:如果你用下划线来绑定一个变量,那么这个变量会当场执行析构,而不是等到当前语句块结束的时候再执行。下划线是特殊符号,不是普通标识符。 ⏱ 2024-11-05 09:36:50
📌 带有析构函数的类型都是不能满足Copy语义的。因为我们不能保证,对于带析构函数的类型,使用memcpy复制一个副本一定不会有内存安全问题。所以对于这种情况,编译器是直接禁止的。 ⏱ 2024-11-05 09:37:46
📌 :首先判断一个变量是否可能会在多个不同的路径上发生析构,如果是这样,那么它会在当前函数调用栈中自动插入一个bool类型的标记,用于标记该对象的析构函数是否已经被调用 ⏱ 2024-11-08 18:42:12
第12章 借用和生命周期
📌 变量对其管理的内存拥有所有权。这个所有权不仅可以被转移(move),还可以被借用(borrow)。 ⏱ 2024-11-08 18:43:08
📌 借用指针与普通指针的内部数据是一模一样的,唯一的区别是语义层面上的 ⏱ 2024-11-08 18:44:10
📌 如果mut修饰的是变量名,那么它代表这个变量可以被重新绑定;如果mut修饰的是“借用指针&”,那么它代表的是被指向的对象可以被修改。 ⏱ 2024-11-08 18:45:03
📌 ❏ 如果只有&型借用指针,那么能同时存在多个;如果存在&mut型借用指针,那么只能存在一个;如果同时有其他的&或者&mut型借用指针存在,那么会出现编译错误。 ⏱ 2024-11-08 18:51:36
📌 因为p的存在,此时对x的改变被认为是非法的。 ⏱ 2024-11-08 18:53:10
📌 这里的关键是,Rust的引用类型是支持“协变”的。在编译器眼里,生命周期就是一个区间,生命周期参数就是一个普通的泛型参数,它可以被特化为某个具体的生命周期。 ⏱ 2024-11-08 19:02:33
读书笔记
第11章 所有权和移动语义
划线评论
📌 Rust的赋值和C++的赋值有重大区别。 ^15826765-7Vtam7yK2 - 💭 默认 move 语意 - ⏱ 2024-11-05 09:17:47