0.前言

本文章主要针对博主健忘的毛病, 整理所学的知识点以便复习使用。 当然能够顺便帮助别人就更好啦。

在撰写本文章时, 博主已将Rust程序设计语言( 第二版 )通读并进行了少量代码的编写工作。 但由于学习曲线过于陡峭, 目前还没有办法具有良好的代码编写感觉, 所以打算重新把该书再读一遍, 并加以学习的整理, 希望能学有所成。

按照该书的逻辑, 总共分为四个部分: 入门指南、 基本Rust技能、 Rust编程思想、 高级主题。 正文中会主要分这四个部分进行知识点整理。

1.入门指南

1>杂项

String::new表示new是String类型的一个关联函数(associated function ),关联函数是针对类型实现的, 而不是一个特例。 又叫静态方法。

当使用.foo()语法调用方法时,通过换行加缩进来把长行拆开是明智的。

在使用crate时, 需要先在Cargo.toml文件中添加依赖, 下载好的库会存放在deps目录下。

Cargo.lock会保证程序是可重现的, 用来维持库版本的稳定。

常量与不可变变量的区别: 常量总是不可变, 需注明值的类型, 可以在任意作用域中声明, 只能设置为常量表达式, 而不能是函数调用的结果。

隐藏( shadow )和可变变量mut的区别: 隐藏需要let语句, 实际上创建了一个新变量。 mut不可以改变其类型。

Rust 代码中的函数和变量名使用 snake case 规范风格。

语句(Statements)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。

String::from中的双冒号是运算符, 允许将特定的from函数置于String类型的命名空间下, 而不是使用类似string_from的名字。

2>数据类型

Rust为静态类型语言, 需要在编译时知道所有的变量类型, 即在具有多种可能时注明类型。

1*标量类型(scalar)

  • 整型: i8/u8, i16/u16, i32/u32, i64/u64, isize/usize(arch)。 可以使用如1_000方便阅读, 支持类型后缀如b’A'(u8 only)。i32通常是最快的, 为默认。
  • 浮点型: f32/f64, 默认f64。
  • 布尔型(bool): true/false。
  • 字符型(char): 使用’ ‘, 为unicode。

2*复合类型(compound)

  • 元组型(tuple): 类型不必相同, 可以使用模式匹配来解构。使用a.0来访问。
  • 数组型(array): 类型必须相同, 分配到栈, 可使用a[0]访问。

3>函数

在函数签名中,必须声明每个参数的类型。

需要注明返回值的类型, 空元组表示不返回值。

4>控制流

if不会将非布尔值转换为布尔值, 支持else if和else。

5>所有权(ownership)

所有权保证了Rust即使没有GC也可以实现内存安全,其就是在管理堆数据, 及时清除不用的数据。所有权所遵循的规则如下:

  1. Rust中的每一个值都有一个被称为其所有者(owner)的变量。
  2. 值有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。

当栈内的项在作用域结束时被移出栈, 与其他语言相同。 但在堆中的变量是不一样的。 个人理解, 所有权的意义是指某个变量对它所指的内存的所有。

1*变量与数据的交互方式

  • 移动( move ): 当String::from这种储存在堆上的值在传递时, 实际上只复制了栈上指针的数据, 堆上依旧只有一个数据, 同时第一个变量失效, 避免了double free错误, 即实现了所有权的转移。
  • 克隆( clone ): 同时复制堆上的数据。 但注意在像整形这样直接储存在栈上的类型, 当发生赋值时, 自动调用拷贝, 类似于直接克隆, 故不会有所有权问题。

2*所有权与函数

当一个值被传入到函数时, 其所有权也同样被传入到函数中。 返回值也会同时返回所有权。 简单来说, 每当持有堆上数据值的变量离开某作用域时, 会被自动调用drop处理掉, 除非数据被移动给另一变量所有。

6>引用与借用/slice

引用&实际上是指向原储存在栈上的变量,而不是指向堆中的数据, 故不拥有原变量的所有权。 我们将获取引用作为函数参数称为借用(borrowing), 因为没有所有权, 所以不能更改引用的值。

在一个作用域中只能有一个可变引用&mut, 或是多个不可变引用, 避免数据竞争。

在避免悬垂引用(dangling references)时, 通过所有权检测, 不允许只将一个变量的引用移出函数而本体不移出, 从而避免悬垂。

Slice类型同样是没有所有权的, 作用是允许引用集合中一段连续的元素序列, 如&s[..]。 字符串的字面值就是slice, 如:

let s = “hello,world.”;

上面s类型为&str, 它指向程序特定位置的slice, 是一个不可变引用, 故不能改变它的值。

在传递字符串引用时, 将&String改为&str会更好, 即在传递整个字符串时传进整个字符串的slice即可, 程序兼容性更好。

7>使用结构体组织相关联的数据

变量与字段重名时可以只写一个, 如:user,。

使用其他实例中的值可以用..,如..user1。

当想给一个元组取个名字时, 可以使用元组结构体来完成。 没有任何字段的结构体称为类单元结构体(unit-like structs), 因为其类似于()。

在想要查看结构体的各个值时, 可以使用println!中的{:?}功能, 但需要在结构体开头加上#[derive(debug)]注解, 而{:#?}风格会更加易读。

8>方法语法

方法与函数类似, 但不同点为:方法是在结构体/枚举/trait上下文中被定义, 且其第一个参数总为self, 它代表着调用结构体实例。

1*impl块

将某个类型实例能做的事情全部放入impl块(implementation)中, 避免在库中找功能。

之所以Rust中没有->运算符, 即不用显式地对一个对象的指针调用方法, 是因为Rust的self参数已经表示了传入的是引用、可变引用或是所有权本身等, 故在调用方法时可以推断并补全, 不需要显式地说明。

一个结构体可以有多个impl块。

2*关联函数

impl块允许定义不以self做参数的函数, 它们仍然是函数, 因为其不作用于一个结构体的实例。 但称其为关联函数, 因为其与结构体相关联。 使用::来调用, 这个方法位于结构体的命名空间中。

9>枚举(enumeration)

适用于能列举出所有情况, 并且实际情况只取其一。 枚举的成员位于其标识符的命名空间中, 所以用::来使用。 此外, 枚举的优势是每个成员可以处理不同类型和数量的数据, 但结构体不行(可能因为结构体处理的键值只能有一个)。

枚举可以使用impl块定义方法。

Option枚举被包含在prelude中, 不需要显式引入作用域。 如下:

enum Option<T> {
    Some(T),
    None,
}

T泛型意味着Some可以包含任意类型的数据。 在进行匹配时, 需要穷尽所有的可能性, 即需要考虑None情况, 或用_通配符实现。

if let控制流可以匹配一种情况而忽略其他所有情况。 涉及到模式匹配问题。

2.基本Rust技能

1>模块(module)

函数、类型、常量和模块默认都是私有的, 以mod关键字开头。

1*省略写法

通过mod client;引入,并在src目录下创建client.rs文件即可。 当想抽离模块中的模块, 如network模块下的server模块, 则需要创建network目录, 并把network.rs的内容移动至该目录下的mod.rs中, 接着在该目录下创建server.rs即可。

使用extern crate引入库。

2*私有性规则

  1. 如果一个项是公有的,它能被任何父模块访问。
  2. 如果一个项是私有的,它能被其直接父模块及其任何子模块访问。

注意不在任何一个模块中的函数即位于根模块, 它是所有模块的父模块。

3*引入规则

使用use引入命名空间后, 可以直接引用。 这也适用于枚举。

对作用域使用glob运算符(即*)引入所有名称。

使用::module可以从根模块开始, super::module可以从父模块开始。 实际上, use super::module这种用法很普遍。

2>通用集合类型

集合指向的元素是直接储存在堆上的, 故不必在编译时就知道其数量。

1*vector

用来储存一系列的值, 类型为Vec<T>。 vector是使用泛型实现的, 在新建一个vector(Vec::new())时需要标明类型注解, 但有了初值(vec![1,2,3])Rust就可以推断出类型, 便不需要再有类型注解。 具有所有权, 出域会丢弃。

vector有两种索引方式, 区别在于: 第一种访问超上限后会panic, 但第二种会返回None。

  1. let item = &v[2];
  2. let item = v.get(2);

注意无效引用, 在存在对vector的某个元素引用时, 不能push进新元素。 因为在该处内存不够时会将vector数据移至新的位置, 这时对旧元素的引用会指向已释放的内存。

在使用vector引用时, 调用其值需要使用解引用运算符*来获取值。 需要在vector中存放不同类型的值时, 可以将不同类型的值划分到枚举中, 使所有值保持类型相同(枚举类型)。

2*string

Rust核心语言中只有一种字符串类型: str。 String类型是由标准库提供的, 但没有写进核心语言部分。

1**连接字符串

可以使用+或format!连接字符串。 只允许String和&str相加, 但String和&String相加也是可行的, 因为Rust使用解引用强制多态(deref coercion), 会将&String强转(coerced)为&str。

2**索引字符串

String是不支持索引的, 因为其是一个Vec<u8>的封装, 它的索引会取u8长度, 但utf-8每个字符是占用不定长度字节的, 我们想要的是字形簇, 但返回的可能是数据的一部分, 索引便失去了意义。

索引可以使用slice, 但有可能会遇到上述情况而直接导致panic。 可以使用.char()方法转化, for进行遍历得到。

3*hash map

类型为HashMap<K,V>, 通过哈希函数实现映射。 没有收录prelude, 需要导入:

use std::collections::HashMap;

哈希表会获取值的所有权, 当将某值的引用导入时, 注意生命周期限制。

3>错误处理

  • panic!与不可恢复错误。
  • Result与可恢复错误。

Result是一个枚举, 有以下两个成员:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

失败时panic的简写为unwrap和expect。 unwrap即完成了Ok时返回值, Err时panic的功能。 expect的不同是可以自定义错误信息。

传播错误可以用?简写, 在出错时由整个区块返回错误, 但只适用于返回值为Result类型的时候。

4>泛型(generics)与trait

Rust中类型命名采用CamelCase。impl<T>会决定块内方法的泛型。

trait决定某个特定类型拥有可能与其他类型共享的功能。 对于不同的类型, 可以自定义不同的实现方式。 注意trait限制只有在对应类型位于crate本地时才能为其实现trait, 也就是不允许外部类型实现外部trait, 被称为孤儿规则(orphan rule), 使别人不会更改自己的代码。

Trait Bounds代表了为泛型提供的trait, 在泛型的冒号后注明。 每一个泛型都可以有自己的trait, 也可以使用where从句来表达, 使trait叠加的语句更加整洁。

5>生命周期(lifetime)与引用有效性

大多时候生命周期是隐含并可以推断的, 不需要编写注明, 但有时在多种类型或类型间生命周期不同时, 就需要使用泛型生命周期参数进行注明。

在一个函数想要传出数据时, 若内部存在控制流, 会让编译器无法知道究竟返回值的生命周期应该如何对应, 故需要用参数注明。 单个的生命周期注解本身没有多少意义:生命周期注解告诉 Rust 多个引用的泛型生命周期参数如何相互联系。

在注明几个变量的生命周期参数都为’a时, 等于告诉编译器’a与生命周期最短的一个变量保持一致。 当然, 如果函数逻辑没有涉及到某个变量, 便不用为该参数设定生命周期参数。

1*生命周期省略规则(lifetime elision rules)

  1. 每一个是引用的参数都有它自己的生命周期参数。
  2. 如果只有一个输入生命周期参数, 那么它被赋予所有输出生命周期参数。
  3. 如果方法有多个输入生命周期参数, 不过其中之一因为方法的缘故为&self或&mut self,那么self的生命周期被赋给所有输出生命周期参数。

2*静态生命周期

使用’static表示该值存活于整个程序期间。

3*整合程序

将泛型、trait bounds、生命周期三者整合进一个函数例子如下:

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

6>测试

1*单元测试(unit tests)

属性是代码片段的元数据, 需要在fn行之前加上#[test]标记, 并使用cargo test来开始测试。 assert!宏是测试条件为true的好工具, 可以使用assert_eq!和assert_ne!来测试相等。 should_panic用来测试panic功能。

可以使用–来分割想要给cargo test传递的参数和想要给测试二进制传递的参数。 后置参数有: -test–thread=*设置线程数量。 能看到通过的测试中打印的值使用–nocapture。

#[ignore]可以忽略。 #[cfg(test)]表示只有在cargo test时才编译运行, cargo build时是忽略的。

2*集成测试(integration tests)

集成测试的用法为在根目录下创立tests目录, 该目录下创建任意测试文件即可。 使用–test 来制定具体的集成测试文件。

当想要为其他测试创造一个crate时, 可以像tests/common/mod.rs这样命名来代替如tests/common.rs, 这会使Rust不把该文件作为单独的测试文件来看待。

3.Rust编程思想

本专题下涉及到的是函数式编程风格, 通常包含将函数作为参数值或其他函数的返回值、将函数赋值给变量以供之后执行等。

1>闭包(closure)

在一对竖线中指定闭包的参数, 多于一个参数可用逗号分隔。 参数后为存放闭包体的大括号, 而如果闭包体只有一行则大括号可以省略。

闭包不要求像函数一样注明类型, 因为其并不用于暴露在外的接口: 他们储存在变量中并被使用, 不用命名他们或暴露给库的用户调用。 当然为了增加明确性可以添加类型注解。 此外, 如果没有设置泛型, 则不能以不同的类型调用闭包, 因为在第一次调用闭包时, 其使用的类型会被锁定入闭包中。

闭包可通过三种方式捕获其环境, 他们直接对应函数的三种获取参数的方式: 获取所有权、 可变借用和不可变借用。 这三种捕获值的方式被编码为如下三个Fn trait。

  • FnOnce: 消费从周围作用域捕获的变量, 闭包在定义时将所有权移动进闭包。 其名称的Once部分代表闭包不能多次获取想通变凉的所有权的事实。
  • FnMut: 获取可变的借用值所以可以改变其环境。
  • Fn: 从其环境获取不可变的借用值。

想要强制闭包获取其使用的环境值的所有权, 可以使用move关键字, 常用于将闭包传递给新线程以将数据移入新线程中。

2>迭代器(iterator)

迭代器用于实现遍历序列中的每一项和决定序列何时结束的逻辑。 Rust中迭代器是惰性的, 故直到调用方法消费迭代器之前它都不会有效果。 迭代器使用next方法用来记录序列位置的状态, 即代码消费了迭代器。

对于需要获取所有权并返回所有权、 迭代可变引用、 迭代不可变引用, 可以分别使用以下三种方法。

  • into_iter
  • iter_mut
  • iter

迭代器是Rust的零成本抽象, 这代表了抽象并不会强加运行时开销。

3>智能指针(smart pointers)

智能指针是一类数据结构, 拥有额外的元数据和功能。 实际上String和Vec<T>也是智能指针, 因为它们拥有一些数据并允许修改它们, 并且带有元数据( 如容量 )和额外的功能或保证。

1*Box<T>

Box为最简单直接的智能指针, 允许你将一个值放在堆上而不是栈上。

使用Box::new()可以快速创建一个Box值。 一般来说, Box可以用在递归类型中, 很少将栈中的数据直接放在堆中, 没有意义。

2*Deref trait

这种trait可以重载解引用运算符, 实现被当做常规引用来对待。 数字的引用与数字是不同的类型, 无法进行比较。 在使用*运算符时, Rust在需要调用deref方法时就会进行调用, 底层看起来像*(y.deref()), 不必自己调用。

解引用强制多态使得编写函数和方法调用时不必增加过多显式使用&和*的引用和解引用。 因为这些解析发生在编译时, 故没有运行时惩罚。

当与可变性交互时, Rust提供了DerefMut trait来重载*运算符。 以下是三种调用解引用强制多态的情况:

  • 当T: Deref<Target=U>时从&T到&U。
  • 当T: DerefMut<Target=U>时从&mut T到&mut U。
  • 当T: Deref<Target=U>时从&mut T到&U。

不可能从不可变引用转到可变引用, 因为一个数据最多只能有一个可变引用。

3*Drop trait

不允许在正常情况下调用drop函数, 因为编译器会自动调用, 将导致double free错误。 可以使用std::mem::drop来提早丢弃值, 它位于prelude中。

4*Rc<T>引用计数(reference counting)

它会记录一个值引用的数量来知晓这个值是否仍然在被使用, 如果某值为零引用, 则就代表没有任何有效引用并可以被清理, 只能用于单线程场景。 这类指针在实现生命周期不同的情况下经常使用, 克隆Rc<T>会增加引用计数。 不允许多个可变引用, 但允许修改数据。

5*RefCell<T>和内部可变性模式

内部可变形( interior mutability )是一个设计模式, 允许即使在有不可变引用时修改数据, 使用unsafe代码来模糊Rust的可变性与借用规则。

使用RefCell<T>时, 出现违反规则的情况时不同于编译错误, 而是会panic!, 并且只适用于单线程场景。 使用borrow和borrow_mut方法, 这些属于RefCell<T>安全API的一部分, 分别返回Ref和RefMut类型智能指针。

结合Rc<T>和RefCell<T>便可以拥有多个所有者并可以修改值。 标准库中有一些提供内部可变性的类型, 如Cell<T>会将值拷贝进和拷贝出。 Mutex<T>, 可以提供线程间安全的内部可变性。

6*引用循环与内存泄露

制造出永远也不会被清理的内存被称为内存泄露, Rust认为内存泄露是内存安全的, 引用循环会造成内存泄露, 严重时会导致内存用尽。 避免引用循环可以将Rc<T>变为Weak<T>, 这是一个弱引用, 无需计数为0时就能使Rc实例被清理。

4>无畏并发

未完待续。。。

5.类型方法总结

io::stdin().readline()读取输入值。

String.trim()可消除\n换行符, .parse()将文字解析为数字。

a.iter()将数组的每个值返回, 可用于for循环。.enumerate()方法包装iter()的结果, 并返回一个元组。 (1..4).rev()用于反转。

String.push_str()用于末尾增加字符。

.clear()清空字符串, .to_string()可以将”hello”转化str类型, .push_str()可以在末尾增加字符串。

哈希表提供entry, 此API可以判断表内有无该键, 常见下列用法:

scores.entry(String::from(“Yellow”)).or_insert(50);

6.The End

发表评论

电子邮件地址不会被公开。 必填项已用*标注