rust 入门

Cargo

  • Cargo是Rust的构建系统与包管理工具
    • 构建代码,下载依赖的库,构建这些库
  • toml是cargo的配置格式。Rust中代码的包称为crate。
  • 如果创建项目时,没有使用cargo,可以将源代码移到src中
  • cargo check可以用来检查代码,确保通过编译,cargo check比cargo build的要快
  • cargo --release在正式发布时使用,编译时会进行优化

六 枚举

6.4 枚举与模式匹配

  • if let是只处理一种情况的match模式匹配,
    优点是更少的代码,更少的缩进,但是放弃了穷举所有的可能性。可以把if let看作是match的语法糖,还可以搭配else

七 Package,Crate,Module

7.1 Package,Crate, 定义Module

  • Package,Crate,Module,Path
  • 一个Package中的Cargo.toml会描述如何构建这些Crates
  • package中存在binary crate与libary crate,cargo会把crate roo文件交给rustc来构建library或者binary
  • src/main.rs会被默认当作binary crate的crate root
  • src/lib.rs对于libray crate而言,就是crate root
  • 一个Package可以有多个binary crate
  • Crate是将相关功能组合,并防止冲突
  • 而一个Crate中存在多个Module一个module中又定义了两个其他的module
  • binary crate与libary crate都基于它们的root,形成一个模块树

7.2 路径(Path)

  • 为了找到某个条目?,需要使用路径,分为绝对路径与相对路径
    • lib.rs对应这里的crate,lib.rs隐式的组成了crate模块,
      这里的add_to_waitlist()就可以看作一个条目,而这里的hosting错误是由于,hosting是private modulefront_of_houseeat_at_restaurant之间可以不加pub而互相调用,是因为它们都是lib.rs文件同一级
    • 使用相对路径,还是绝对路径,取决于使用条目代码与条目本身在以后是否会一起移动
  • 私有边界,模块不仅可以组织代码,还可以定义私有边界,也就是说在一个模块中的函数或者struct等默认都是私有的(pub关键字可以使其公有化)
  • super关键字用于访问父级模块中的内容,类似于文件系统的..
  • pub struct结构体的公有化,但是struct的字段默认是私有的,想要设为共有,仍然是加上pubseasonal_fruit没加上pub,就不可以访问了
  • pub enum,那么它的变体也是公共的,即使不加上pub

7.4 use 关键字

  • 可以用use关键字将路径导入到作用域内,仍然遵守私有性规则,有点类似于符号链接
  • use的习惯用法as的用法
  • 对于内部的use,仍然是私有的,可以在use 前面加上pub
  • 如何使用外部包(package),
  • 使用嵌套路径清理大量的use语句如果其中一个引用,是另外一个引用的子路径,那么就需要用self可以使用*来引入一个模块的所有条目

7.6 将模块拆分为不同的文件

  • 可以将一个crate中的模块拆分为一个文件(当模块名以;结尾),但是模块树的结构不会变化,对于层级模块,需要建立对应文件夹

八 常用的集合

这些集合与tuple,数组不一样,这些集合的存储区域是heap,也就是说它们的大小可以在运行时动态变化

8.1 Vector

  • Vec
  • 创建Vector,Vec::new();
  • 添加元素, push()方法
  • 删除Vec,离开作用域,就被清理了,
  • 读取Vector元素
    • 索引
    • get方法,返回的是Option的enum,其中的Some中的元素是引用&
    • 索引与get处理访问越界
  • 所有权与借用(创建一个引用的行为)规则,不能在同一作用域内拥有可变与不可变的引用第三行的可变借用第四,五行的不可变借用发生了冲突,第4行与第3行冲突,是由于如果第4行添加了元素,那么可能由于Vec中的元素需要连续存放,进而发生所有元素地址的变换
  • for循环遍历与解引用
  • rust需要在编译时知道Vec的类型,rust需要知道在heap上分配多少内存。并且明确了到底哪些数据存放在Vec上,对于一个操作,如果Vec中的元素类型不同,那么有可能这个操作对于不同类型的合法性不一样

String

  • 字符串是Byte的集合,它有一些方法,能将byte解析为文本
  • Rust只有一种字符串类型,也就是str
  • 创建String,String可以由字符串字面值通过to_string转换而来
  • 更新Stringpush_str()用来附加一个字符串切片,push()是把单个字符附加到String,+用来拼接字符串在用+号拼接之后,那么+左边的String,就发生了move,那么对于对于+右边,是一种叫做deref coercion的技术format不会取得任何参数的所有权
  • 访问,Rust字符串不支持索引形式的语法访问
  • 对于图中的字符,由于utf-8的编码,其中每一个字符占两个字节(汉字一般在utf-8中占3个字节),那么&hello[0]就会获取其第一个字节,得到的结果一般与预期的不一致
  • Rust有三种看待字符串的方法,字节,标量值,字形簇String无法消耗一个O(1)时间通过索引,来的到一个字符
  • 切割字符串这里由于梵文也是3个字节一个字符,那么打印字符需要沿着字符边界切割
  • Rust设计哲学是由程序员处理正确处理好utf-8,可以防止后期涉及非ASCII字符的错误

String 与 str 与 &str, &String的区别

  • 参考
  • &String只是指向String的指针,而String本身只是一个结构体

HashMap<K,V>

  • HashMap就是适合通过K(任何类型)来寻找数据,而不是通过索引
  • HashMap不在Prelude中,HashMap是同构的
  • collect方法创建HashMap一定要指明HashMap类型,由于collect()方法会返回许多不同的集合数据结构
  • HashMap和所有权由于String没有实现Copy_trait,那么在将String"放入"HashMap之后,它们会被move
  • 访问HashMap中的值
  • 更新HashMap<K, V>对于已经存在的K,更新会存在三种情况
    • insert方法,覆盖
    • entry方法,需要检查K是否存在or_insert()的返回值就是可变引用
    • 基于现有值更新由于or_insert返回的是可变引用,那么可以修改这个可变引用中的值
  • Hash函数,可以通过修改hasher来切换Hash算法

九 错误处理

9.1 panic(不可恢复的错误)

  • 由于rust的涉及哲学,它具有很高的可靠性,那么错误分为两类
    • 执行panic!宏的时候,程序会立即停止执行panic发生,需要处理好数据的回收,那么有两种方式,即展开调用栈中止调用栈,其中中止调用栈,工作量更小可以通过调用panic!的函数的回溯信息来定位引起问题的代码

9.2 Result枚举与可恢复的错误

  • File::open()返回的就是Result<File, Error>类型Result枚举
  • Result枚举中的Err还可以匹配不同的错误,可以使用闭包实现很多match的匹配
  • unwrap方法是match表达式的一个快捷方法
  • expect方法的功能与unwrap类似,但是可以指定错误信息,容易找到错误信息的位置
  • 传播错误,将错误返回给调用者?运算符传播错误的一种快捷方式这个?与后面的match是一个作用,from函数,它是用于错误之间的转换,from函数用于隐式的转换错误?运算符还可以链式调用其中Box<dyn Error>是trait对象,可以理解为任何可能的错误类型
  • 什么时候使用panic,总体原则,取决于你实现的函数,如果你认为可能出现不可恢复的错误,那么就使用panic,否则使用Result
  • 编写示例,原型代码,测试的时候,适合使用panic!
  • 对于你认为一定不会出错的Result,那么就可以使用unwrap,它一定不会报错应该是因为unwrap的报错无法自定义
  • 错误处理的指导意见一些使用Result与panic的抉择的场景把创建新类型的验证逻辑放入构建实例的函数中比如这里需要保证传入的i32属于1到100

十 泛型,trait,生命周期

10.1 提取函数消除重复的代码

  • 重复代码的危害函数消除重复代码

10.2 泛型

  • rust中的泛型就是模板,将<>中的占位符替换为具体的类型
  • 函数泛型
  • 结构体泛型这里的i32与f64类型,可以在struct中定义多个泛型参数,但是过多的类型参数表明你的代码需要重组为多个更小的单元
  • Enum的泛型
  • 方法中的泛型这里的x1函数只有在i32类型的Point中存在方法与struct中的泛型参数可以不一样
  • Rust中使用泛型代码与使用具体类型的代码的运行速度是一样的?,Rust中的实例化叫做单态化,而cpp中却是不一样的

10.3 Trait

  • 所有的trait都定义了一个隐式的类型Self,它指向了当前实现此接口的类型
  • 关于Self的一些定义
  • Trait bounds是指定为实现了特定行为的类型
  • 类型的行为由该类型可调用的方法决定,当不同的类型具有相同的方法,那么这些类型共享了相同的行为,就是go语言中的interface
  • 在类型上实现trait,类似于为类型实现方法
  • 实现trait的约束无法为外部类型来实现外部的trait
  • Trait可以提供默认实现,默认实现方法可以调用trait中其他的方法,即使这些方法没有默认实现 无法从方法的重写实现里调用方法的默认实现
  • Trait作为参数
    impl Trait语法:适合简单情况Trait bound语法:适合复杂情况
  • 指定多个Trait约束+指定多个Trait约束
  • 为了函数签名的简洁性,还可以使用where子句指定多个Trait bound
  • 使用Trait作为返回类型impl Trait语法只能返回同一种类型,返回可能不同的类型的代码会报错,比如这里的Tweet与NewsArticle都实现了trait Summary,但是会出错
  • 使用Trait Bound有条件的实现方法在这里只有实现了Display与PartialOrd的Pair才具有cmp_display这个方法
  • ?这里的impl 实现是针对所有实现了Display Trait的类型都去实现ToString这个Trait,这里便是覆盖实现

10.5 生命周期

  • 生命周期就是要引用保持自己有效的作用域。一般情况下,生命周期是隐式的,可以被推断的。当引用的生命周期可能以不同的方式相互关联时,这时候难以推断,需要手动标注生命周期
  • 生命周期存在的目的是避免悬垂引用,也就是引用已经释放的资源
  • 借用检查器,用于比较作用域来判断所有的借用是否合法此处r的生命周期是'a,而变量x的生命周期是'b。也就是被引用者的生命周期短于引用者的生命周期
  • 函数中的泛型生命周期加上生命周期标识
  • 生命周期的标注不会影响生命周期长度,只是描述了多个引用的生命周期间的关系
  • 生命周期的标注语法单个生命周期标注本身没有意义
  • 函数声明中的生命周期标注这里表明了两个参数的存活时间必须不能短语'a,而函数返回值的'a 也表明了返回值的生命周期不能够短于'a,这里并没有改变传入的值与返回值的生命周期,只是向借用检查器指出了一些可以用于检测非法调用的约束,longest函数本身并不需要知道x与y的存活时长,而只需要某个作用域可以用来代替'a,同时满足函数的约束即可。如果longest被函数外部的代码引用,那么rust编译器本身难以确定参数与返回值的生命周期就难以做到,函数所使用的生命周期在每次都可能发生变化,所以需要手动对生命周期进行标注。'a的生命周期等于x与y两者之间生命周期较短的那一个由于此时result的生命周期等于string2的生命周期,那么在{}之后,result已经被清理了,
  • 指定生命周期参数的方式依赖于函数所作的事情返回的引用必须执行参数,否则就会是悬垂引用。生命周期的语法从根本上讲就是用来关联函数的不同参数以及返回值之间的生命周期的,一旦它们之间产生了某种联系,rust就获得了足够的信息来支持保证内存安全的操作,并阻止了那些可能会导致悬垂指针或者是其他违反内存安全的行为
  • struct定义中的生命周期标注,如果Strcut中的包含引用,那么就需要添加生命周期标注此处i的生命周期短于first_sentence的生命周期
  • 生命周期的省略,每个引用都有生命周期,并且需要为使用生命周期的函数或者struct指定生命周期参数,但是编译器对其做了优化,也就是生命周期省略规则,对于模糊的生命周期,仍然需要进行标注规则是,其中与输入输出生命周期有关
  • 输入输出生命周期
  • 在第二个例子中编译器无法计算出输出生命周期,那么编译器报错
  • 方法中的生命周期标注?
  • 静态生命周期'static表示整个程序的执行时间,生命周期也是泛型的一种

十一 编写自动化测试

11.1 编写与运行测试

  • 一般的测试函数包括三个步骤
    • 准备测试数据/状态
    • 运行被测试代码
    • 断言(Assert)结果
  • 测试的本质是测试函数需要使用test属性(attribute)进行标注,类似python的注解
  • cargo test来运行所有的测试函数
  • 测试失败,每一个测试都在一个新的线程中执行,测试失败会引发panic

11.2 Assert

  • 调用panic会测试失败,那么assert!宏会检测结果,用来测试某个状态是否为true,如果false,就调用panic!,测试失败
  • assert_eq!和assert_ne!,断言失败会自动打印两个参数的值,是调用debug格式打印参数,所以需要参数实现了PartialEqDebug Traits,一般自定义结构体需要自己实现

11.3 自定义错误信息

11.4 should_panic检测恐慌

  • should_panic属性,用来验证代码在特定情况下是否发生了panic
  • 为了使should_panic更精确

11.5 测试中的Result<T,E>

  • Result中的Err可以表示失败不要在编写的测试中标注#[should_panic],因为Result不会引发panic
  • 控制字测试如何运行,cargo test默认在测试通过的时候,不显示像println!这种宏的输出,失败的时候会显示输出

11.6 并行/连续运行测试

  • 由于测试之间是并行运行的,所以需要保证它们之间是不互相依赖,且一起依赖于某个共享状态
  • --test-threads可以控制线程个数
  • 测试通过不会打印显示println!的内容

11.7 按名称运行测试

  • 将测试的名称作为cargo test的参数

11.8 忽略测试

  • 忽略某些测试,用ignore属性也可以单独运行被标记为ignore的测试

11.9 测试的组织

  • 测试分类
    • 单元测试#cfg[test]标注就是单元测试,通常把被测试代码与测试代码放到一个src目录下的一个文件。使用#[cfg(test)]标注的模块只有在运行cargo test时才会编译与运行代码,运行cargo build时不会
    • 集成测试,集成测试在不同的目录下,不需要#[cfg(test)]标注指定的配置选项在这里指的是test
  • 测试私有函数

11.10 集成测试

  • 集成测试完全位于被测试库外部覆盖率很重要
  • 需要创建tests目录,cargo会将tests目录进行特殊处理,tests目录下面的内容只会在cargo test命令时进行编译
  • 运行指的集成测试
  • 集成测试中的子模块,tests目录下的每个文件会被编译成单独的crate,这些文件不共享行为
  • 针对binary crate的集成测试,binary crate就是只包含src/main.rs,libray crate才可以把函数暴露给其他crate使用,binary crate意味着要独立运行

十二 实例:接收命令行参数

12.3 重构 改善模块化

  • 二进制程序关注点分离的指导下原则
    • 将程序拆分为main.rs和lib.rs,将业务逻辑放入lib.rs
    • 当命令行解析逻辑少,将它放入main.rs亦可
    • 命令行逻辑变复杂的时,需要将它们从main.rs提取到lib.rs

12.4 使用TDD(测试驱动开发)开发库功能

十三 函数式语言特性:迭代器和闭包

13.1 闭包-使用闭包创建抽象行为

  • 闭包:可以捕获其所在环境(环境中的变量)的匿名函数
    • 匿名函数
    • 可以像普通数据类型一样,保存为变量,作为参数
    • 可以在一个地方创建闭包,然后再另一个地方调用闭包完成运算
    • 可以其定义的作用域捕获值这里定义了一个闭包,也就是cpp中的lambda

13.2 闭包-闭包类型推断和标注

  • 闭包的类型推断,闭包可以不标注参数和返回值类型
  • 函数与闭包的定义语法对比
  • 闭包的参数类型被推断了一次之后,其参数与返回值类型就确定了这里第一次推断出了参数与返回值类型是String,之后的i32类型会出错,不是泛型

13.3 闭包-使用泛型参数和Fn Trait来存储闭包

  • 创建一个struct来缓存闭包以及其第一次计算的结果,这被叫做记忆化延迟计算
  • 在struct中存储闭包,需要知道所有字段的类型,也就是需要指明闭包的类型
  • Fn Trait,Fn,FnMut,FnOnce

13.4闭包-使用闭包捕获环境

  • 闭包可以访问定义它的作用域内的变量,但是会产生内存开销
  • 闭包捕获值的方式有三种Trait,
    • FnOnce,取得所有权
    • FnMut,可变借用
    • Fn,不可变借用
  • 所有实现了Fn的闭包都实现了FnMut?(可变引用与不可变引用不是冲突了吗?)所有实现了FnMut的闭包都实现了FnOnce
  • move关键字,可以强制闭包取得它所使用的环境值的所有权可是FnOnce Trait不是已经实现了所有权的获取吗?Fn的这些Trait到底有啥用
  • Fn Trait最佳实践这里的equal_to_x 具有 Fn trait

13.5 迭代器

  • 迭代器模式,对一些列执行某些任务,迭代器负责遍历每个项,确定序列(遍历)何时完成。Rust种的迭代器是Lazy的,除非调用消费迭代器的方法,否则迭代器本身没有任何效果
  • 所有的迭代器都实现了iter trait,其中实现了与item关联的类型iterator trait只需要实现next方法
  • iter方法与into_iter方法与iter_mut方法区别

13.6 消耗和产生迭代器

  • 一些默认实现Iterator tarit的方法,其中一些方法调用了next方法,调用next方法的方法叫做消耗型迭代器
  • 需要调用消耗型迭代器将新产生的迭代器消耗,得到新的集合

13.7 使用闭包捕获环境

  • filter方法,迭代器适配器返回一个过滤后的迭代器

13.8 创建自定义迭代器

  • 也就是要提供next方法这个自定义的迭代器的next方法返回的是u32类型的Option,也就是Self::Item可以理解为u32
  • zip方法将两个new()方法产生的迭代器合成一个生成tuple元素的新迭代器,skip(1)就是跳过一个元素,map函数又产生一个新的迭代器,然后filter又产生新的迭代器,sum函数有消耗迭代器

13.10 循环与迭代器的性能比较

  • 迭代器编译后,生成了与手写循环几乎一样的产物。这就是零开销抽象也就是迭代器的抽象不会造成额外开销

十四 cargo,crates.io

14.1 通过release profile来自定义构建

  • 优化编译的程度对于dev而言,一般选择优化程度低编译时间短,而release选择优化程度高,编译时间长

14.2 发布crate到crates.io

  • 一般第三方依赖库都是从crates.io下载,托管开源代码
  • 文档注释,用于生成html文档,使用///,支持markdown语法cargo doc --open打开
  • 常用章节
  • 为包含注释的项添加文档注释

14.3 使用pub use导出方便使用的公共API

  • 对于crate程序结构的多层结构,在开发的时候对于使用者很不方便,可以使用pub use重新导出将lib.rs中的条目(也就是函数或枚举等)重新导出

14.4 发布crate到crate.io

  • 创建crates.io上的账号,通过token与命令cargo login进行验证登录
  • 发布crates之前,需要在Cargo.toml的[package]区域添加元数据
  • 一旦发布,无法覆盖
  • 发布已经存在的crate的新版本
  • cargo yank撤回一个版本cargo yank -vers 1.0.1 --undo来取消撤回

14.5 Cargo工作空间

  • cargo工作空间就是一套共享同一个Cargo.lock和输出文件夹的包,是一种Package?
  • 创建工作空间
  • Cargo.toml中添加crate,target目录中存储编译产出物,那么adder目录就没有它自己的target了
  • cargo不会显示指明crate之间的依赖
  • 在binary crate的adder的main.rs中的Cargo.toml这里的依赖是add-one, 而使用起来是add_one
  • 在工作空间中依赖外部crate,外部crate依赖存储在Cargo.lock文件,在工作空间的顶层目录对于一个外部crate,所有的内部crate依赖的都是同一个版本,工作空间中内部的所有的crate都是相互兼容的
  • 为工作空间添加测试

14.6从crate.io安装二进制crate

  • crate install安装二进制crate,如果使用rustup安装Rust,没有任何配置,那么二进制存放目录是$HOME/.cargo/bin,那么这个二进制程序可以直接运行,需要确保存放目录$HOME/.cargo/bin在PATH环境变量中
  • 使用cargo子命令扩展

十五 智能指针

  • rust中的引用就是指针
  • 通过引用计数来记录所有者的数量在没有所有者的时候自动清理数据
  • 智能指针是拥有它所指向的数据
  • String的Vec都是智能指针,它们会保证数据的合法性
  • 智能指针的struct实现了Deref traitDrop trait
  • 智能指针的定义是他们的表现类似指针,但是也拥有额外的元数据和功能

15.1 Box指向Heap上的数据

  • Box的使用场景
  • Box在heap上存储数据这个5现在存在heap上面了,而不是stack
  • 使用Box赋能递归类型,在编译时,Rust需要知道一个类型所占空间的大小
  • rust的enum的大小是枚举中变体占内存空间最大的一个,rust是如此确定非递归类型的枚举的大小,对于递归类型可以使用Box存储

15.2 Deref Trait

  • 实现Deref Trait可以是我们自定义解引用运算符*常规引用也是一种指针
  • Box被定义为拥有一个元素的tuple struct但是需要实现deref trait
  • deref trait只需要实现一个方法deref方法,其返回一个指向内部数据的引用实现了deref的struct,那么*y就是*(y.deref())
  • Deref Coercion,隐式解引用此处&m 的类型是 &MyBox 类型,那么由于Deref Coercion,其可以通过deref方法转换成&String,又由于String实现了Deref Trait,那么&String可以隐式的deref为&str如果没有Deref Coercion,那么就需要写成14行的样子
  • 解引用转换与可变性对于DerefMut Trait的实现?

15.3 Drop Trait

  • Drop Trait可以自定义当值离开作用域时发生的动作,就是CPP中RAII的析构函数。需要实现drop方法,Drop Trait在preludedrop功能很难直接调用,rust不允许直接调用Drop trait的drop方法,但是可以直接调用std::mem::drop函数,来提前drop值。并且rust不会double free

15.4 Rc:引用计数智能指针

  • Rc用于支持多重所有权
  • 对于无法确定哪个部分使用完这些数据的场景,适合Rc,一般的场景中,onwership的转移,可以最后确定什么时候释放数据内存,Rc只适用于单线程
  • Rc不再预导入模块中存在强引用与弱引用
  • List和Cons,Nil都不是关键字use crate::List::{Cons, Nil};的作用是可以不用List::Cons而直接用Cons
  • a.clone()是深度克隆,Rc.clone(a)只会增加引用计数
  • Rc通过不可变引用,使你可以在程序不同部分之间共享只读数据
  • Rc&的区别是什么?

15.4 RefCell和内部可变性

  • 内部可见性使用了unsafe代码来绕过Rust正常的可变性和借用规则
  • RefCell代表其持有数据的唯一所有权,RefCell只会在运行时检查借用规则
  • 借用规则在不同阶段进行检查的比较
  • 即使RefCell本身不可变,但是仍然可以修改其中存储的值Box支持可变和不可变的借用
  • 无法可变的借用一个不可变的值
  • RefCell有两个安全接口,borrowborrow_mut
  • RefCell会记录可变借用计数与不可变借用计数。borrow方法会修改不可变借用计数,borrow_mut会修改不可变计数的值任何一个给定时间,只能有一个可变引用与多个不可变引用
  • 将Rc和RefCell结合使用来实现一个拥有多重所有权的可变数据?
  • test double(测试提替身)的概念中的mock对象是一个类型,那么修改这个类型的值,来断言操作的正确性此处的message就是mock对象,它的类型是MockMessenger,其中包含一个RefCell<Vec<String>>mock对象会承担记录测试中的工作
  • 其他可实现内部可变性的类型
  • *value.borrow_mut()会得到一个mut的i32变量,对于value采用了deref coercion,得到了RefCell,borrow_mut方法将其解引用为Ref_Mut*又将其解引用为mut 的 i32

15.6 循环引用可导致内存泄漏

  • Rc和RefCell可能创造循环引用
  • Option匹配中变量取得是引用
  • 防止内存泄漏需要依靠开发者来保证,用Weak来保证

十六 无畏并发

  • Concurrent是并发,Parallel是并行,rust可以避免细微的编写并发的Bug
  • Rust需要权衡运行时的支持,由于需要保证运行时较小,且方便与c语言交互,那么rust选择1:1编程模型,这样实现线程就不需要为了实现语言自己的线程而增加运行时,rust编程语言实现线程的方式有两种
    • 通过OS的API来创建线程1:1模型,优点是需要较小的运行时
    • 语言自己实现线程(绿色线程),M:N模型,M个绿色线程对于N个系统线程,不过需要较大的运行时
  • rust通过thread::spawn函数创建新线程,参数显而易见就是闭包
  • thread::spawn函数返回值是JoinHandle,其持有值的所有权,调用join方法可以等待其对应其他线程完成
  • move闭包通常和thread::spawn函数一起使用,它允许你使用其他线程的数据此处的闭包中的v是借用的主线程中的v,但是闭包中借用的v的寿命要长于主线程中的v使用move,就获得了v的ownership了

16.2 使用消息传递来跨线程传递数据

  • 消息传递是一种流行且能保证安全并发的技术,Rust标准库提供了channel
  • mpsc::channel来创建channel,其表示multiple producer,single consumer,这个函数返回一个两个元素的元组,也就是发送端与接收端,send方法recv方法try_recv方法来不会阻塞,有数据,立即返回,否则返回错误(当然这里可以continue)
  • 在send方法中会转移发送元素的ownership
  • 可以通过clone创建多个发送者这里通过clone复制了一个producer

16.3 共享状态的并发

  • 通过共享内存来实现并发,也就是共享状态Channel类似单所有权:一旦将值的所有权转移至Channel,就无法使用它了,共享内存类似于多所有权,也就是多个线程同时访问同一块内存
  • 使用互斥锁mutex来实现线程的互斥访问
  • Mutex是一个智能指针
  • MutexGuard实现了自动解锁,也就是类似于cpp中的lock_guard,MutexGuard实现了Drop Trait,那么其离开作用域的时候,会自动析构
  • 这个counter已经move到第一个thread中了,为了实现counter变量在多个thread中的多重所有权使用Rc仍然出错,因为其并不是线程安全的,也就是没有实现Send Trait,那么需要Arc,也就是原子的Rc
  • 多线程的多重所有权,使用Arc实现原子的引用计数
  • RefCell/Rc 与 Mutex/ARcMutex提供了内部可变性,那么它就是可以是实现互斥的RefCell

16.4 通过Send核Sync Trait来实现扩展并发

  • Send允许线程间转移所有权,几乎所有Rust类型都实现了Send,但是Rc没有实现
  • Sync Trait实现了多线程访问手动实现Send和Sync是unsafe

十七 Rust的面向对象编程特性

  • 基于四人帮的设计模式,Rust是面向对象的,struct,enum是包含数据,impl可以实现enum的方法?
  • rust的pub关键字实现封装特性
  • rust中没有继承,继承可以实现代码复用,但是默认trait已经可以实现** 代码共享了,继承还可以实现多态,也即一些对象拥有某些共同特征,那么在运行时,它们可以相互替换,可以使用泛型与trait约束来实现多态**

17.1 使用trait来存储不同值

  • 多态的例子,draw绘制,rust没有继承,需要定义一个trait,trait用于抽象某些共有行为Box<dyn Draw>dyn Draw表示Box中的元素实现了Draw trait其与泛型的区别是,T只能表示一种类型,比如如果在Vec中,第一个元素是Button,那么其之后的元素都得是Button了,而Box 就可以放很多实现了Draw trait的不同类型,就像在cpp中一个放父类指针的vector
  • dyn Traitimpl Trait的区别在于,前者是动态派发,后者是静态派发,参考,
  • trait对象执行的是动态派发,而泛型实现的是静态派发的单态化,使用trait对象会产生运行时开销
  • Trait对象必须保证对象安全trait Clone就不是dyn safe的

17.3 实现面向对象的设计模式

  • 状态模式,一个值拥有的内部状态由数个状态对象表达二叉,而值的行为则随着内部状态的改变而改变。业务需求变化时,不需要修改持有状态的值的代码,只需要更新状态对象内部的代码
  • take()方法move所有权,那么self: Box<Self>为第一个参数,那么这个方法只能被当前类型的Box包裹的实例调用这个例子中,对于三种状态draft,pendingreview,published,当业务需求改变时,不需要修改值持有状态或使用值得代码,只需要更新某个状态中得代码来改变其规则,或者增加更多得状态对象
  • content方法的第二个参数,需要接收Post的引用作为参数,那么返回值是Post的切片作为返回值,那么返回值的生命周期与post参数的生命周期是相关的
  • 但是状态模式中的一些状态之间是相互耦合的,Rust可以将状态和行为编码为类型,那么rust类型检查系统会通过编译时错误来阻止用户使用无效的状态修改之后,只有post有content方法,只有DraftPost有request_review方法只有PendingReviewPost类型有approve方法,也就是在相应的方法做相应的事情

十八 模式匹配

  • 模式是Rust一种特殊语法,用于匹配复杂和简单类型的结构,模式由以下元素组成

18.1 用到模式的地方

  • match的arm就是match的分支_用于匹配最后一个arm,且不会绑定到变量
  • if let表达式不会检查穷尽性
  • while let条件循环这里能够取出数,就不停止
  • for循环这里就是模式enumerate返回一个struct,其中存有两个字段
  • let语句也是模式, let PATTERN = EXPRESSION;
  • 函数参数也可以是模式

18.2 可辩驳性:模式是否会无法匹配

  • 无可辩驳的模式是指能匹配任何可能传递的模式,对于可能出现无法匹配的模式:可辨驳的 if let和 while let可以接收可辨驳与不可辩驳的模式比如如果a是None,那么就无法匹配改成if let就好了。那么match表达式最后一个表达式必须是不可辩驳的,也就是不允许失败,因为其需要匹配所有的情况

18.3 模式匹配

  • 模式可以直接匹配字面值
  • 匹配命名变量,匹配值是无可辩驳模式,也就是可能出现无法匹配的情况Some(y)中的y是5
  • 多种模式匹配,|管道语法1|2也就是1或者2都可以匹配
  • ..=匹配一个范围1..=5匹配1到5的所有数
  • 结构以分解值,可以结构struct,enum,tuple,从而引用这些类型值的不同部分这里匹配x随意,y是0的模式
  • 结构enum,根据枚举的变体实现match匹配
  • 结构嵌套的enum和struct
  • 结构struct和tuple
  • 在模式中忽略值
    • 使用_来忽略整个值
    • 使用嵌套的_来忽略值的一部分此处不用管Some(_)中的值,只需要知道其中一些数
  • 使用_来忽略未使用的变量
  • 使用..来忽略值得剩余部分
  • 使用match的arm来提供额外的条件此处要求x小于5多种模式匹配与match的arm结合
  • @绑定通过在 3..=7 之前指定 id_variable @,我们捕获了任何匹配此范围的值并同时测试其值匹配这个范围模式。

十九 高级类型

19.1 unsafe rust

  • unsafe rust不保证强制内存安全保证,由于静态分析过于保守
    • 解引用原始指针*是指针类型的一部分创建了原始指针,可以在unsafe代码块创建原始指针,但是只能在unsafe块中解引用原始指针,使用原始指针可以和C语言进行接口,并构建借用检查器无法理解的安全抽象
  • 调用unsafe函数与方法
  • 创建unsafe代码的安全抽象,将unsafe代码包裹在安全函数中不是一个常见的抽象split_at_mut将给定的索引分割为两个切片这里实现了一个切片的两次可变借用,rust这里认为这里存在两个可变借用,那么报错这里这个函数本身没有标记为unsafe,但是其中存在不安全的代码,那么这里就是不安全代码的安全抽象
  • 使用extern函数调用外部代码extern关键字,简化创建和使用外部函数接口(FFI foreign function interface),其允许一种编程语言定义函数,并让其他编程语言调用这些函数,extern块中申明的函数是不安全的这里的"C"指明了外部函数使用的是C语言的abi
  • 从其他语言调用Rust函数, #[no_mangle]注解避免rust再编译时改变它的名称,mangle对应了一个特殊的编译阶段,此阶段,编译器会修改函数名称从而使其包含更多可用于后续编译的信息,这些改变后的名称一般难以阅读,所以为了要其他语言正常识别rust函数,要紧张rust改名这个函数被编译链接后,就可以被c语言访问了,这一类extern不需要使用unsafe
  • rust中的全局变量就是static变量staic变量声明时必须标明类型,其命名规范是大写的snake_case静态变量只能储存拥有 'static 生命周期的引用
  • static变量有相同的内存地址访问和修改可变的static变量是unsafe的操作
  • 当某个trait存在至少一个编译器无法校验的不安全因素时,就称这个trati是不安全的

19.2 高级Trait

  • 关联类型是Trait中类型占位符这里的Item就是关联类型
  • 可以为一个类型多次实现Trait这里为Counter结构体实现了两种类型的Iterator2,每次实现的时候要标注类型
  • 关联类型无法多次实现这里为Counter实现Iterator中,关联类型指定了对于Counter的Iterator Trait这里就不能实现不同的关联类型
  • 默认泛型参数与和运算符重载,可以为泛型参数指定默认具体类型,常用于运算符重载此处具体指明了泛型参数类型
  • Self和Ouput不是都是Point吗?这里的Self应该指的是self: Self
  • 关联类型的主要应用场景
  • 完全限定语法,如何调用同名方法这里的定义了三个fly方法,可以这样区分调用对于这种无参的trait方法,无法靠传递self参数区分是struct自己的关联方法还是trait的方法这个时候可以使用完全限定语法改成这样
  • 使用spuertrait来要求trait附带其他trait的功能这里要求trait OutlinePrint的实现类型,也实现Display trait这里报错,就是没有实现Display这个trait
  • 使用newtype模式在外部类型上实现外部trait,之前是孤儿规则,只有当trait或类型定义在本地包时,才能为这个类型实现这个trait此处trait display和Vec都定义在外部

19.3 高级类型

  • 使用newtype模式实现类型安全和抽象
    • 用来静态的保证各种值之间不会混淆并表明值得单位
    • 为类型得某些细节提供抽象能力
    • 通过轻量级粉装来隐藏内部实现细节
  • 类型别名,使用type关键字,主要是用来减少代码字符重复这样命名函数,减少了很多代码量由于Result枚举得第二个泛型参数都是Error,那么可以为此取类型别名
  • Never类型,有一个!的特殊类型,空类型(empty type),它在不返回的函数中充当返回类型,不返回值的函数被称为发散函数loop的返回值就是!类型
  • 动态大小和Sized Trait,那么str(字符串切片),也就是大小不确定的,因为其分配在堆上,
    这里s1与s2的大小不一致,对于同一类型,rust需要确定它们的大小是一样的,那么&str类型的大小是确定的。所以对于动态大小类型的通用方式是存储额外的元数据
  • trait也是一种动态大小类型,那么需要将trait 对象(trait 对象指的是实现了某种trait的strcut)放置在某种指针之后
  • Sized trait,Rust会为每一个泛型函数隐式的添加Sized约束上面的函数会被隐式转换为下面的函数,默认情况下泛型函数只能被用于编译时已经知道大小的类型,但是可以通过特殊语法解除这个限制也就是?Size trait约束这里的?Size就表明类型T可以是非确定大小的,所以参数是&T,因为此时T的大小不确定

19.4 高级函数和闭包

  • fn类型就是函数指针函数指针类型需要指明参数类型与返回值类型
  • 闭包与fn类型,fn类型实现了全部3种闭包
  • 返回闭包,闭包使用trait进行表达,无法在函数中直接返回一个闭包,可以将一个实现了改trait的具体类型作为返回值转化为指针就有固定大小了

19.5 宏

  • 是指一组相关特性的集合称谓
    • 声明宏,使用macro_rules!构建快弃用了标注意味着其所处的包被引入作用域之后,才可以使用这个宏这个宏会生成下方的代码
    • 过程宏,其会接收操作输入的Rsut代码,并生成另一些Rust代码作为结果,宏定义必须单独放在它们自己的包中,并使用特殊的包类型
      • 自定义#[derive]宏,用于strcut和enum,可以为其指定随derive属性添加的代码
      • 在任何条目上添加自定义属性
      • 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作
  • 函数和宏的区别,宏是元编程(可以用来生成其他代码的代码),函数在定义签名时,必须声明参数的个数与类型,宏可以处理可变的参数,编译器会在解释代码前展开宏,宏的定义比函数复杂的多,难以阅读,维护,理解
posted @ 2022-06-02 14:46  抿了抿嘴丶  阅读(578)  评论(0编辑  收藏  举报