Rust权威指南

史蒂夫·克拉伯尼克 卡罗尔·尼科尔斯

Hello, World!

  • 标准Rust风格使用4个空格而不是Tab来实现缩进。
  • Rust中所有以!结尾的调用都意味着你正在使用一个宏而不是普通函数。

Hello, Cargo!

  • Cargo是Rust工具链中内置的构建系统及包管理器
  • Cargo使用TOML(Tom's Obvious, Minimal Language)作为标准的配置格式
  • 在Rust中,我们把代码的集合称作包(crate)
  • cargo build --release在优化模式下构建并生成可执行程序

处理一次猜测

  • 在Rust中,变量都是默认不可变的
  • 关联函数在某些语言中也被称为静态方法(static method)
  • 参数前面的&意味着当前的参数是一个引用
  • 引用与变量一样,默认情况下也是不可变的。因此,你需要使用&mut guess而不是&guess来声明一个可变引用。
  • Result是一个枚举类型

变量与可变性

  • Rust中的变量默认是不可变的
  • 我们不能用mut关键字来修饰一个常量
  • 常量可以被声明在任何作用域中,甚至包括全局作用域
  • 最后,你只能将常量绑定到一个常量表达式上,而无法将一个函数的返回值,或其他需要在运行时计算的值绑定到常量上。
  • 常量在整个程序运行的过程中都在自己声明的作用域内有效
  • 第一个变量被第二个变量隐藏(shadow)了
  • 隐藏机制不同于将一个变量声明为mut,因为如果不是在使用let关键字的情况下重新为这个变量赋值,则会导致编译错误
  • 隐藏机制与mut的另一个区别在于:由于重复使用let关键字会创建出新的变量,所以我们可以在复用变量名称的同时改变它的类型。

数据类型

  • 标量类型(scalar)和复合类型(compound)。
  • Rust是一门静态类型语言,这意味着它在编译程序的过程中需要知道所有变量的具体类型
  • 有符号数是通过二进制补码的形式来存储的。
  • Rust使用术语panic来描述程序因为错误而退出的情形
  • char类型使用单引号指定,而不同于字符串使用双引号指定。
  • Rust中的char类型占4字节,是一个Unicode标量值,这也意味着它可以表示比ASCII多得多的字符内容。
  • Rust提供了两种内置的基础复合类型:元组(tuple)和数组(array)。
  • 元组还拥有一个固定的长度:你无法在声明结束后增加或减少其中的元素数量。
  • 通常而言,当你想在栈上而不是堆上为数据分配空间时,或者想要确保总有固定数量的元素时,数组是一个非常有用的工具
  • 数组由一整块分配在栈上的内存组成

函数

  • Rust代码使用蛇形命名法(snake case)来作为规范函数和变量名称的风格
  • Rust不关心你在何处定义函数,只要这些定义对于使用区域是可见的即可。
  • 在函数签名中,你必须显式地声明每个参数的类型。这是在Rus
  • 函数体由若干条语句组成,并可以以一个表达式作为结尾
  • 由于Rust是一门基于表达式的语言,所以它将语句(statement)与表达式(expression)区别为两个不同的概念
  • 语句指那些执行操作但不返回值的指令
  • 调用函数是表达式,调用宏是表达式,我们用来创建新作用域的花括号({})同样也是表达式
  • 假如我们在表达式的末尾加上了分号,这一段代码就变为了语句而不会返回任何值
  • 但由于语句并不会产生值,所以Rust默认返回了一个空元组,也就是上面描述中的()

控制流

  • Rust不会自动尝试将非布尔类型的值转换为布尔类型
  • 由于if是一个表达式,所以我们可以在let语句的右侧使用它来生成一个值
  • 所有if分支可能返回的值都必须是一种类型的
  • 我们可以将需要返回的值添加到break表达式后面
  • for循环的安全性和简捷性使它成为了Rust中最为常用的循环结构

第4章 认识所有权

  • 正是所有权概念和相关工具的引入,Rust才能够在没有垃圾回收机制的前提下保障内存安全。

什么是所有权

  • Rust采用了与众不同的第三种方式:它使用包含特定规则的所有权系统来管理内存,这套规则允许编译器在编译过程中执行检查工作,而不会产生任何的运行时开销。
  • 所有存储在栈中的数据都必须拥有一个已知且固定的大小。对于那些在编译期无法确定大小的数据,你就只能将它们存储在堆中。
  • 由于多了指针跳转的环节,所以访问堆上的数据要慢于访问栈上的数据
  • • Rust中的每一个值都有一个对应的变量作为它的所有者。• 在同一时间内,值有且仅有一个所有者。• 当所有者离开自己的作用域时,它持有的值就会被释放掉。
  • 作用域是一个对象在程序中有效的范围
  • 为什么String是可变的,而字符串字面量不是?这是因为它们采用了不同的内存处理方式。
  • 对于字符串字面量而言,由于我们在编译时就知道其内容,所以这部分硬编码的文本被直接嵌入到了最终的可执行文件中。
  • 内存会自动地在拥有它的变量离开作用域后进行释放
  • Rust在变量离开作用域时,会调用一个叫作drop的特殊函数。
  • 在C++中,这种在对象生命周期结束时释放资源的模式有时也被称作资源获取即初始化(Resource Acquisition Is Initialization, RAII)。假如你使用过
  • 变量和数据交互的方式:移动
  • 长度字段被用来记录当前String中的文本使用了多少字节的内存。而容量字段则被用来记录String向操作系统总共获取到的内存字节数量
  • Rust不会在复制值时深度地复制堆上的数据
  • 为了确保内存安全,同时也避免复制分配的内存,Rust在这种场景下会简单地将s1废弃,不再视其为一个有效的变量
  • 但由于Rust同时使第一个变量无效了,所以我们使用了新的术语移动(move)来描述这一行为,而不再使用浅度拷贝
  • Rust永远不会自动地创建数据的深度拷贝。因此在Rust中,任何自动的赋值操作都可以被视为高效的。
  • 当你确实需要去深度拷贝String堆上的数据,而不仅仅是栈数据时,就可以使用一个名为clone的方法。
  • 当你看到某处调用了clone时,你就应该知道某些特定的代码将会被执行,而且这些代码可能会相当消耗资源。
  • 这是因为类似于整型的类型可以在编译时确定自己的大小,并且能够将自己的数据完整地存储在栈中,对于这些值的复制操作永远都是非常快速的。
  • 一旦某种类型拥有了Copy这种trait,那么它的变量就可以在赋值给其他变量之后保持可用性。如果一种类型本身或这种类型的任意成员实现了Drop这种trait,那么Rust就不允许其实现Copy这种trait。
  • 任何简单标量的组合类型都可以是Copy的,任何需要分配内存或某种资源的类型都不会是Copy的。
  • 将变量传递给函数将会触发移动或复制,就像是赋值语句一样
  • 函数在返回值的过程中也会发生所有权的转移。
  • 将一个值赋值给另一个变量时就会转移所有权

引用与借用

  • 新的函数签名使用了String的引用作为参数而没有直接转移值的所有权
  • 这些&代表的就是引用语义
  • 与使用&进行引用相反的操作被称为解引用(dereferencing),它使用*作为运算符。
  • 由于引用不持有值的所有权,所以当引用离开当前作用域时,它指向的值也不会被丢弃。
  • 当一个函数使用引用而不是值本身作为参数时,我们便不需要为了归还所有权而特意去返回值,毕竟在这种情况下,我们根本没有取得所有权。
  • 这种通过引用传递参数给函数的方法也被称为借用(borrowing)
  • 对于特定作用域中的特定数据来说,一次只能声明一个可变引用
  • 数据竞争(data race)与竞态条件十分类似,它会在指令满足以下3种情形时发生:• 两个或两个以上的指针同时访问同一空间。• 其中至少有一个指针会向空间中写入数据。• 没有同步数据访问的机制。
  • 我们不能在拥有不可变引用的同时创建可变引用
  • 在任何一段给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用。

切片

  • 除了引用,Rust还有另外一种不持有所有权的数据类型:切片(slice)。
  • 字符串切片是指向String对象中某个连续部分的引用
  • 字符串切片的边界必须位于有效的UTF-8字符边界内
  • 字符串切片的类型写作&str
  • 当我们拥有了某个变量的不可变引用时,我们就无法同时取得该变量的可变引用

总结

  • 所有权、借用和切片的概念是Rust可以在编译时保证内存安全的关键所在。

定义并实例化结构体

  • 一旦实例可变,那么实例中的所有字段都将是可变的。Rust不允许我们单独声明某一部分字段的可变性。
  • 在变量名与字段名相同时使用简化版的字段初始化方法
  • 这里的双点号..表明剩下的那些还未被显式赋值的字段都与给定实例拥有相同的值。
  • 元组结构体同样拥有用于表明自身含义的名称,但你无须在声明它时对其字段进行命名,仅保留字段的类型即可
  • 元组结构体实例的行为就像元组一样:你可以通过模式匹配将它们解构为单独的部分,你也可以通过. 及索引来访问特定字段。
  • 当你想要在某些类型上实现一个trait,却不需要在该类型中存储任何数据时,空结构体就可以发挥相应的作用。
  • 生命周期保证了结构体实例中引用数据的有效期不短于实例本身。

方法

  • 方法总是被定义在某个结构体(或者枚举类型、trait对象,我们会在第6章和第17章分别介绍它们)的上下文中,并且它们的第一个参数永远都是self,用于指代调用该方法的结构体实例。
  • 关联函数常常被用作构造器来返回一个结构体的新实例
  • 每个结构体可以拥有多个impl块。

定义枚举

  • 枚举的变体全都位于其标识符的命名空间中,并使用两个冒号来将标识符和变体分隔开来
  • 枚举允许我们直接将其关联的数据嵌入枚举变体内
  • 每个变体可以拥有不同类型和数量的关联数据

控制流运算符match

  • match,它允许将一个值与一系列的模式相比较,并根据匹配的模式执行相应代码
  • 在if语句中,表达式需要返回一个布尔值,而这里的表达式则可以返回任何类型。
  • Rust中的匹配是穷尽的(exhausitive):我们必须穷尽所有的可能性,来确保代码是合法有效的。

第7章 使用包、单元包及模块来管理日渐复杂的项目

  • 一个包(package)可以拥有多个二进制单元包及一个可选的库单元包。

包与单元包

  • 单元包可以被用于生成二进制程序或库。
  • 首先,一个包中只能拥有最多一个库单元包。其次,包可以拥有任意多个二进制单元包。最后,包内必须存在至少一个单元包(库单元包或二进制单元包)。

通过定义模块来控制作用域及私有性

  • 模块决定了一个条目是否可以被外部代码使用(公共),或者仅仅只是一个内部的实现细节而不对外暴露(私有)

用于在模块树中指明条目的路径

  • 模块不仅仅被用于组织代码,同时还定义了Rust中的私有边界(privacy boundary):外部代码无法知晓、调用或依赖那些由私有边界封装了的实现细节。
  • Rust中的所有条目(函数、方法、结构体、枚举、模块及常量)默认都是私有的。处于父级模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用它所有祖先模块中的条目。
  • 当我们在结构体定义前使用pub时,结构体本身就成为了公共结构体,但它的字段依旧保持了私有状态。我们可以逐一决定是否将某个字段公开。

用use关键字将路径导入作用域

  • 当使用use将结构体、枚举和其他条目引入作用域时,我们习惯于通过指定完整路径的方式引入

将模块拆分为不同的文件

  • 在mod front_of_house后使用分号而不是代码块会让Rust前往与当前模块同名的文件中加载模块内容。

第8章 通用集合类型

  • 与内置的数组与元组类型不同,这些集合将自己持有的数据存储在了堆上。

使用字符串存储UTF-8编码的文本

  • Rust在语言核心部分只有一种字符串类型,那就是字符串切片str,它通常以借用的形式(&str)出现。
  • 编译器可以自动将&String类型的参数强制转换为&str类型
  • Rust中的字符串并不支持索引。

第9章 错误处理

  • 在Rust中,我们将错误分为两大类:可恢复错误与不可恢复错误。

可恢复错误与Result

  • t的返回值是Ok变体时,unwrap就会返回Ok内部的值。而当Result的返回值是Err变体时,unwrap则会替我们调用panic! 宏。下面是一个在
  • 传播错误的模式在Rust编程中非常常见,所以Rust专门提供了一个问号运算符(?)来简化它的语法。
  • ?运算符只能被用于返回Result的函数
  • 使用了?运算符的函数必须返回Result、Option或任何实现了std::ops::Try的类型

第10章 泛型、trait与生命周期

  • 在定义泛型时,使用trait可以将其限制为拥有某些特定行为的类型,而不是任意类型。

泛型数据类型

  • 注意,我们必须紧跟着impl关键字声明T,以便能够在实现方法时指定类型Point。通过在impl之后将T声明为泛型,Rust能够识别出Point尖括号内的类型是泛型而不是具体类型。
  • Rust实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异。
  • 单态化是一个在编译期将泛型代码转换为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码。

trait:定义共享行为

  • trait(特征)被用来向Rust编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为
  • trait与其他语言中常被称为接口(interface)的功能类似,但也不尽相同。
  • trait提供了一种将特定方法签名组合起来的途径,它定义了为达成某种目的所必需的行为集合。
  • 一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。
  • 只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
  • 我们还可以在默认实现中调用相同trait中的其他方法,哪怕这些方法没有默认实现。
  • 注意,我们是无法在重载方法实现的过程中调用该方法的默认实现的
  • 通过+语法来指定多个trait约束
  • 使用where从句来简化trait约束

使用生命周期保证引用的有效性

  • Rust的每个引用都有自己的生命周期(lifetime),它对应着引用保持有效性的作用域。
  • 生命周期最主要的目标在于避免悬垂引用
  • 生命周期的标注并不会改变任何引用的生命周期长度
  • 参数与返回值中的所有引用都必须拥有相同的生命周期。
  • 泛型生命周期'a会被具体化为x与y两者中生命周期较短的那一个
  • 在本例中,最好的解决办法就是返回一个持有自身所有权的数据类型而不是引用,这样就可以将清理值的责任转移给函数调用者了。
  • 这个标注意味着ImportantExcerpt实例的存活时间不能超过存储在part字段中的引用的存活时间。
  • 第一条规则是,每一个引用参数都会拥有自己的生命周期参数。
  • Rust中还存在一种特殊的生命周期'static,它表示整个程序的执行期。

同时使用泛型参数、trait约束与生命周期

  • 因为生命周期也是泛型的一种,所以生命周期参数'a和泛型参数T都被放置到了函数名后的尖括号列表中。

测试的组织结构

  • 在tests模块上标注#[cfg(test)]可以让Rust只在执行cargo test命令时编译和运行该部分测试代码,而在执行cargo build时剔除它们

重构代码以增强模块化程度和错误处理能力

  • 在使用复杂类型更合适时偏偏坚持使用基本类型,是一种叫作基本类型偏执(primitive obsession)的反模式(anti-pattern)。

第13章 函数式语言特性:迭代器与闭包

  • 常见的函数式风格编程通常包括将函数当作参数、将函数作为其他函数的返回值或将函数赋给变量以备之后执行等。

闭包:能够捕获环境的匿名函数

  • Rust中的闭包是一种可以存入变量或作为参数传递给其他函数的匿名函数。
  • 注意,这条let语句意味着expensive_closure变量存储了一个匿名函数的定义,而不是调用该匿名函数而产生的返回值。
  • 它们可以捕获自己所在的环境并访问自己被定义时的作用域中的变量。
  • 当闭包从环境中捕获值时,它会使用额外的空间来存储这些值以便在闭包体内使用

使用迭代器处理元素序列

  • iter方法生成的是一个不可变引用的迭代器,我们通过next取得的值实际上是指向动态数组中各个元素的不可变引用。

比较循环和迭代器的性能

  • 迭代器是Rust语言中的一种零开销抽象(zero-cost abstraction),这个词意味着我们在使用这些抽象时不会引入额外的运行时开销。

第15章 智能指针

  • 引用除了指向数据外没有任何其他功能,也没有任何额外的开销,它是Rust中最为常见的一种指针。
  • 智能指针(smart pointer)则是一些数据结构,它们的行为类似于指针但拥有额外的元数据和附加功能
  • 引用是只借用数据的指针;而与之相反地,大多数智能指针本身就拥有它们指向的数据。

使用Box在堆上分配数据

  • 装箱被释放的东西除了有存储在栈上的指针,还有它指向的那些堆数据。

RefCell和内部可变性模式

  • 内部可变性(interior mutability)是Rust的设计模式之一,它允许你在只持有不可变引用的前提下对数据进行修改;

使用线程同时运行代码

  • 由于绿色线程的M:N模型需要一个较大的运行时来管理线程,所以Rust标准库只提供了1:1线程模型的实现。
  • move闭包常常被用来与thread::spawn函数配合使用,它允许你在某个线程中使用来自另一个线程的数据。
  • 通过在闭包前添加move关键字,我们会强制闭包获得它所需值的所有权,而不仅仅是基于Rust的推导来获得值的借用

使用消息传递在线程间转移数据

  • 不要通过共享内存来通信,而是通过通信来共享内存。
  • send函数会获取参数的所有权,并在参数传递时将所有权转移给接收者

使用Sync trait和Send trait对并发进行扩展

  • 只有实现了Send trait的类型才可以安全地在线程间转移所有权

面向对象语言的特性

  • 面向对象的程序由对象组成。对象包装了数据和操作这些数据的流程。这些流程通常被称作方法或操作。
  • 你可以在Rust中使用泛型来构建不同类型的抽象,并使用trait约束来决定类型必须提供的具体特性。这一技术有时也被称作限定参数化多态(bounded parametric polymorphism)。

所有可以使用模式的场合

  • match表达式必须穷尽(exhaustive)匹配值的所有可能性

模式语法

  • 匹配守卫(match guard)是附加在match分支模式后的if条件语句,分支中的模式只有在该条件被同时满足时才能匹配成功
  • @运算符允许我们在测试一个值是否匹配模式的同时创建存储该值的变量

不安全Rust

  • • 解引用裸指针。 • 调用不安全的函数或方法。 • 访问或修改可变的静态变量。 • 实现不安全trait。
  • 常量和静态变量之间的另外一个区别在于静态变量是可变的