computer_knowledge_notes/Languages/Rust/rust_lang_advance.md

12 KiB
Raw Blame History

11测试

编写测试

测试函数的三种操作:设置数据或状态,运行测试代码,断言结果。

fn前加上#[test],就是给这个函数带上了test属性。当执行cargo test命令时Rust会调用标记了test属性的函数,并报告测试的结果。

assert!宏由标准库提供,它对一个布尔值进行判断,如为真则什么也不做,如为假则调用panic!宏。

assert_eq!assert_ne!宏用于测试两个值是否相等,如断言失败则打印出这两个值的内容。

可以向assert!assert_eq!assert_ne!宏传递可选的信息参数,在它们的必需参数之后的参数都会传递给format!宏从而打印出来。

fn前加上#[should_panic]属性则函数panic的时候测试通过函数没有panic的时候测试失败。这用来测试函数是否生成了panic。还可以给should_panic属性增加一个可选的expected参数,用以输出一些信息,在测试不通过时显示它从而方便定位问题。

也可以把Result<T, E>作为返回值编写测试,此时测试通过则返回Ok(()),测试失败则返回带有StringErr

控制测试

cargo test生成的二进制文件默认是并行运行的,所以测试不能相互依赖、或依赖任何共享的状态。

可以在#[test]后面增加#[ignore]来忽略某些测试。

详情可参考man cargo-test命令。

测试的组织结构

单元测试存在于src目录下,与被测代码放在同一文件,位于用#[cfg(test)]标准的tests模块中。

#[cfg(test)]在执行cargo test才编译和运行测试代码,在执行cargo build时不这么做。因为单元测试的代码和源码在同一文件中,所以才需要#[cfg(test)]

在单元测试里,是允许测试私有函数的。

集成测试存在于tests目录cargo会把tests目录下的每一个文件当作一个单独的crate来编译。

集成测试把要测的库看成是外部的,所以需要用use来导入它。

tests目录对于cargo来说是一个特殊的目录在执行cargo test时会自动编译这个目录下的文件。

tests目录中的子目录不会作为单独的crate编译也不会作为测试结果的一部分出现在测试输出中。这对于集成测试中的子模块来说是有用的。使用子模块和使用其它模块没有区别都是使用mod关键字。

13 函数式编程

闭包

数学上的闭包,个人理解,就是对某种运算封闭的一些对象的最小集合。在数据库里也有闭包的概念。Rust里的闭包是一个匿名函数可以以变量或参数的形式存在闭包可以捕获调用者作用域中的值。

使用函数获取结果,这个函数一定会在确定的位置被执行。而如果使用闭包,只有当需要这个结果的时候闭包才会执行。

闭包的定义以一对竖线开始,竖线之间是闭包的参数,如有多个参数则以逗号分隔;接下来则是闭包的语句块,如果是多行语句要使用大括号,如果只有一行则可省略大括号。

使用let语句定义一个变量为闭包意味着这个变量是此闭包的定义而不是此闭包的返回值。

调用闭包和调用函数的形式是一样的。

在定义闭包的时候,不必在参数和返回值上注明类型。因为,和函数不一样,闭包的接口不是暴露在外的,而是只关联于小范围的上下文中,编译器能可靠地推断参数和返回值的类型。注意,闭包的接口类型是固定的,如果调用两次闭包而给它传不同类型的参数,则编译器会报错。

为了提高计算效率可以缓存闭包的返回值这样在多次调用闭包的时候就可以避免重复计算。Rust的解决方案是用一个结构体来存放闭包及其调用结果。为了在结构体里定义闭包需要使用泛型特性绑定

Fn是由std::ops提供的特性。所有的闭包都要实现特性FnFnMutFnOnce中的一个。

在结构体中使用闭包的形式需要定义一个泛型这泛型要用where语法特性绑定Fn(或者FnMutFnOnce)。在结构体中需要定义一个此泛型的字段用以代表闭包的实例。在结构体中还需要定义一个Option类型的字段来保存闭包的结果。

在结构体中使用闭包的逻辑过程:在结构体里的闭包执行前,结构体里Option类型的字段值是None。第一次请求闭包,Option类型的字段值会存储闭包的结果。如果再次请求闭包的结果,则闭包不再执行,而是返回该Option字段的值。

在结构体里使用闭包,结构体里的字段应该是私有的,这样可以避免调用者不合理地改变字段的值。应创建new方法接收闭包并返回该结构体的实例。应该创建一个方法检查Option类型字段的值,如果是None则执行闭包并保存它的返回值,如果有值则直接返回该值。

使用值缓存的方法有两个小问题:1,一旦Option字段里有了值,则不管给闭包传入什么参数,返回值都不会改变。解决方法是用哈希映射来取代单独一个值。2,闭包传入的值和返回的值类型要一致。解决方法是引入范型。

闭包可以捕获它的调用者里的变量,而函数则不行。因为闭包和它的调用者在相同的作用域里。

闭包捕获其调用者环境的三种方式,对应三个特性:

  1. FnOnce - 获取所有权。因为同一变量的所有权只能转移一次所以叫Once。
  2. FnMut - 获取可变的借用值。
  3. Fn - 获取不可变的借用值。

move关键字可以强制闭包获取其调用者环境中值的所有权。

迭代器

迭代器都实现了Iterator特性,其定义如下:

pub trait Iterator{
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 方法的实现
}

type Item是说Iterator特性要求定义一个Item类型,作为next方法的返回值类型。next方法用于返回迭代器中的一个项。

Iterator特性中调用了next方法的方法被称为消费适配器。

Iterator特性中把当前迭代器变为其它类型迭代器的方法称为迭代器适配器。

只要实现了迭代器的next方法,就可以使用标准库中定义的其它Iterator特性的方法了,因为它们都使用了next方法的功能。

循环VS迭代器

闭包和迭代器是Rust的零成本抽象之一。

15 智能指针

引用是最简单的指针,智能指针是带有额外数据的引用。

智能指针与普通结构体的区别:DerefDrop特性。Deref特性使智能指针表现得像引用一样。Drop特性定义了当智能指针离开作用域时运行的代码。

标准库中常用的智能指针有:

  • Box<T>,用于在堆上分配值
  • Rc<T>,一个引用计数类型,其数据可以有多个所有者
  • Ref<T>RefMut<T>,通过RefCell<T>访问。

内部可变性模式,把不可变类型暴露出来以改变其内部值。引用循环会泄漏内存。

Box<T>

智能指针Box<T>把值放在堆上,把指向数据的指针留在栈上。

Deref特性

重载解引用运算符,把智能指针当作常规引用来对待。

Drop特性

Drop特性包含在prelude中所以无需导入。

Rc<T>

rc是引用计数的缩写用于启动多所有权。

RefCell<T>

内部可变性是Rust中的一个设计模式它允许改变不可变引用的数据。

引用循环

16 无畏并发

并发编程是指程序的不同部分独立执行,并行编程是指程序的不同部分同时执行。

使用线程实现并发

使用tread::spawn创建新线程。

使用handle.join()方法阻塞当前线程。

在闭包之前使用move,强制闭包获取其使用的值的所有权。

使用消息传递实现并发
使用共享状态实现并发
使用Sync和Send特性实现并发

Send特性允许在线程间转移所有权。

Sync特性允许多线程访问。

17 面向对象

面向对象语言的特征

面向对象编程要求对象中包含数据和行为。Rust语言可以使用impl块为结构体和枚举带上方法从这个角度来看Rust是面向对象的。

面向对象编程的思想封装即对象的实现细节不能被外部代码获取。Rust语言可以使用pub使模块、类型、函数和方法是公有的默认情况下其它一切都是私有的。从这个角度来看Rust是面向对象的。

面向对象编程的机制继承即对象之间可以相互继承。Rust语言使用的是特性而不是继承。从这个角度来看Rust不是面向对象的。

trait对象

18 模式

模式的位置

match分支里包含了模式。

if let表达式里包含了模式。

while let表达式里包含了模式。

for循环里包含了模式。

let语句里包含了模式。

函数参数也可以是模式。

模式的两种形式

模式的两种形式:可反驳的,不可反驳的。

不可反驳的能匹配任何值的模式。函数参数、let语句和for循环只能接受此种模式。

可反驳的匹配某些值的时候会失败的模式。if let、while let只能接受此种模式。

match匹配里前面的分支必须使用可反驳模式最后一个则要支持不可反驳模式。

模式的语法

匹配字面值。

匹配命名变量。

匹配多个模式。

匹配值的范围。

解构结构体、枚举、嵌套、元组。

忽略模式中的值。

匹配守护。

绑定。

19 高级特征

不安全性

使用unsafe关键字开启一个不安全的代码块。它将允许五类超级操作:

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现不安全特性
  • 访问union的字段
高级特性

关联类型。

为泛型指定一个默认的类型。

使用完全限定语法,以消除歧义。

超特性。

使用newtype模式在外部类型上实现外部特性。

高级类型

使用newtype模式。

使用type关键字给现有类型另一个名字。

使用!在函数从不返回的时候充当返回值。

使用Sized特性来处理动态大小类型。

高级函数和闭包

使用函数指针把函数传给函数。

使用trait对象来返回闭包。

  • 宏包括了声明宏(declarative macros)和过程宏( procedural macros)。

    声明宏用macro_rules!

    过程宏包含自定义宏#[derive]、类属性宏(Attribute-like macros)和类函数宏(Function-like macros)。

  • 函数和宏的区别

    函数必须声明参数个数与类型,而宏能接受不同数量的参数。

    宏可以在预编译阶段展开,而函数是在编译阶段被编译的。

    宏定义比函数定义更难阅读、理解和维护。

    使用宏之前必须先有它的定义,而函数的定义可以出现在使用之后。

  • 定义声明宏

    #[macro_export]
    macro_rules! vec {
        ( $( $x:expr ),* ) => {
            {
                let mut temp_vec = Vec::new();
                $(
                    temp_vec.push($x);
                )*
                temp_vec
            }
        };
    }
    

    #[macro_export]使用宏可用。没有该属性宏不能被引入作用域。

    macro_rules! <name> {() => {};}是宏定义的基本结构。

    =>之前是要匹配的模式。$()用于捕获满足模式的值,模式$x:expr的意思是匹配任意的表达式,并给表达式取名为$x。随后的逗号表示在满足模式的代码之后可以出现逗号(也可以没有逗号)*的意思是可以匹配0次或多次。

    =>之后是匹配到该模式后要执行的代码。