义乌建设局网站,想要一个网站,企业定制网站建设公司哪家好,网站开发应走什么科目8 集合
Rust 标准库中包含一系列被称为 集合#xff08;collections#xff09;的非常有用的数据结构。大部分其他数 据类型都代表一个特定的值#xff0c;不过集合可以包含多个值。不同于内建的数组和元组类型#xff0c;这些 集合指向的数据是储存在堆上的#xff0c;这…8 集合
Rust 标准库中包含一系列被称为 集合collections的非常有用的数据结构。大部分其他数 据类型都代表一个特定的值不过集合可以包含多个值。不同于内建的数组和元组类型这些 集合指向的数据是储存在堆上的这意味着数据的数量不必在编译时就已知并且还可以随着 程序的运行增长或缩小。
常见的三种集合
vector 允许我们一个挨着一个地储存一系列数量可变的值字符串string 是字符的集合。我们之前见过 String 类型哈希 maphash map 允许我们将值与一个特定的键key相关联。这是一个叫做 map的更通用的数据结构的特定实现。
8.1 Vector
vector 允许我们在一个单独的数据结构中储存多于一个的值它在内存中彼此相邻地排列所有的值。vector 只能储存相同类型的值。
fn main() {// 1.新建一个空vectorlet _v1: Veci32 Vec::new();// 2.新建一个包含初值的 vectorlet _v2 vec![1, 2, 3];// 3.使用 push 方法向 vector 增加值let mut _v3: Veci32 Vec::new();_v3.push(3);_v3.push(4);_v3.push(5);// 4. 读取vector元素let v vec![1, 2, 3, 4, 5];let third: i32 v[2];println!(Third element is {third});let third: Optioni32 v.get(2);match third {Some(i) println!(Third element is {i}),None println!(There is no third element)}// let not_exist: Optioni32 v[100]; 索引越界程序会异常// 当 get 方法被传递了一个数组外的索引时它不会 panic 而是返回 None //当偶尔出现超过 vector 范围的访问属于正常情况的时候可以考虑使用它let not_exist: Optioni32 v.get(100);match not_exist {Some(i) println!(Third element is {i}),None println!(There is no third element)}}当我们获取了 vector 的第一个元素的不可变引用并尝试在 vector 末尾增加一个元素的时候如果尝试在函数的后面引用这个元素是行不通的
fn main() {let mut v vec![1, 2, 3, 4, 5];let first v[0];v.push(6);println!(The first element is: {first});
}不能这么做的原因是由于 vector 的工作方式在 vector 的结尾增加新元素时在没有足够空间将所有元素依次相邻存放的情况下可能会要求分配新内存并将老的元素拷贝到新的空间中。这时第一个元素的引用就指向了被释放的内存。借用规则阻止程序陷入这种状况。
遍历 vector 中的元素
fn main() {let mut v vec![100, 32, 22];for x in v { // x是v中元素的不可变引用println!({x});}// 为了修改可变引用所指向的值
// 在使用 运算符之前必须使用解引用运算符* 获取 x 中的值for x in mut v { // x是v中元素的可变引用*x 50;}for x in v { // 遍历完 v 中元素的所有权就被转移了println!({x});}println!({}, v[0]); // error// 但是数组这样是可以的因为使用 for 循环来遍历一个数组时在底层它隐式地创建并接着消费了一个迭代器let a [11, 22, 33, 44, 55];for x in a { // x是a中元素的不可变引用println!({x});}println!({}, a[0]);
}使用枚举来储存多种类型
定义一个枚举以便能在 vector 中存放不同类型的数据
enum SpreadsheetCell {Int(i32),Float(f64),Text(String),
}
fn main() {let _row vec![SpreadsheetCell::Int(3),SpreadsheetCell::Text(String::from(blue)),SpreadsheetCell::Float(10.12),];
}类似于任何其他的 struct vector 在其离开作用域时会被释放所有其内容也会被丢弃这意味着这里它包含的整数将被清理。
fn main() {{let v vec![1, 2, 3, 4];// do stuff with v} // - v goes out of scope and is freed here}
}8.2 String
rust 的核心语言层面只有一个字符串类型字符串 slice str 即 str 字符串切片对存储在其他地方、UTF-8 编码的字符串的引用
字符串字面值存储在二进制文件中也是字符串切片
String 类型
来自 标准库 而不是核心语言可增长、可修改、可拥有UTF-8 编码
通常说的字符串是 String 和 str
其他类型的字符串
rust 的标准库里还包含了很多其他的字符串类型例如OsStringOsStrCStringCstr
上述以 String 结尾的类型通常可获得所有权的以 Str 结尾的类型通常是指可借用的
String 的使用
fn main() {// 1.新建 Stringlet data initial contents;let _s data.to_string();let _s initial conents.to_string();let _s String::from(initial contents);// 2.更新字符串let mut s String::from(foo);s.push_str(tball ); // 向 String 附加字符串 slices.push(A); // push只能添加一个字符println!({s});// 3.拼接字符串
/* 运算符使用了类似 fn add(self, s: str) - String {...} 的方法
s1作为self的参数而s2是 StringString引用类型并不是参数里的 str 类型
这里编译通过的原因是Rust 使用了一个被成为 Deref 强制转换的技术将 String 强转成 str
因为add没有获取参数的所有权因此 s2 在操作后仍然是有效的
*/let s1 String::from(Hello, );let s2 String::from(world!);let s3 s1 s2; // 注意 s1 被移动了不再拥有所有权不能继续使用
// 相加过程获取 s1 的所有权附加上从 s2 中拷贝的内容并返回结果的所有权这个实现比拷贝要更高效// println!({s1}); errorprintln!({s2}); // Okprintln!({s3});// 级联多个字符串let str1 String::from(tic);let str2 String::from(tac);let str3 String::from(toe);let _str str1.clone() - str2 - str3;println!({_str});// 宏format! 使用引用所以不会获取任何参数的所有权let _str: String format!({str1}-{str2}-{str3});println!({_str});}字符串索引与字符串在内部表现
rust 中是无法对字符串进行索引的 let s1 String::from(hello); // 4个字节let h s1[0]; // Error内部表现 String 是一个 Vecu8 的封装。 let hello String::from(Здравствуйте); // 24个字节对于这样一个字符串它的字节数是 24也就是一个字符占用了2个字节因此无法按照统一的方式对所有字符串进行读取为了避免返回意外的值并造成不能立刻发现的 bugRust 根本不会编译这些代码并在开发过程中及早杜绝了误会的发生。
字节、标量值和字形簇
Rust 有三种看待字符串的方式
字节标量值字形簇最接近所谓的 “字母”
fn main() {let s नमस्ते;for x in s.bytes() { // 字节的形式println!({}, x);}for x in s.chars() { // unicode 标量的形式println!({}, x);}
}rust 不允许对 String 进行索引的最后一个原因
索引操作应消耗一个常量时间 O(1)String 无法保证需要遍历所有内存来确定有多少个合法的字符
字符串 slice
索引字符串在rust中是不被允许的因为字符串索引应该返回的类型是不明确的字节值、字符、字形簇或者字符串 slice。因此如果你真的希望使用索引创建字符串 slice 时Rust 会要求你更明确一些。为了更明确索引并表明你需要一个字符串 slice相比使用 [] 和单个值的索引可以使用 [] 和一个 range 来创建含特定字节的字符串 slice
fn main() {let hello Здравствуйте;let s hello[0..4];
}这里s 会是一个 str 它包含字符串的头四个字节。早些时候我们提到了这些字母都是两个字节长的所以这意味着 s 将会是 “Зд”。 如果获取 hello[0..1] 会发生什么呢答案是Rust 在运行时会 panic就跟访问 vector中的无效索引时一样。
遍历字符串的方法
操作字符串每一部分的最好的方法是明确表示需要字符还是字节。对于单独的 Unicode 标量值使用 chars() 方法。
// unicode 标量形式
for c in Зд.chars() {println!({c});
}
/*
З
д
*/// 字节形式
for b in Зд.bytes() {println!({b});
}
/*
208
151
208
180
*/8.3 HashMap
哈希映射HashMapK, V它存储了从 K 类型键到 V 类型值之间的映射 关系。哈希映射在内部实现中使用了哈希函数 这同时决定了它在内存中存储键值对的方式。
use std::collections::HashMap; // 使用use将HashMap从标准库的集合部分引入当前作用域fn main() {// 1. 创建一个新的HashMaplet mut scores: HashMapString, u8 HashMap::new();// 如果有insert添加元素的语句那么上边可以不显示地指定类型否则必须指定scores.insert(String::from(Bob), 90);scores.insert(String::from(Lucy), 89);// 2. 使用 collect 方法创建 HashMaplet terms vec![String::from(Blue), String::from(Black)];let initial_score vec![90, 99];let _scores: HashMap_,_ // collect可以作用于许多不同的数据结构, 因此这里的显示指定不能省略terms.iter().zip(initial_score.iter()).collect();}HashMap 和所有权
对于实现了 Copy trait 的类型例如 i32值会被复制到 HashMap 中对于拥有所有权的值例如 String值会被移动所有权会转移给 HashMap
use std::collections::HashMap; // 使用use将HashMap从标准库的集合部分引入当前作用域fn main() {let field_name String::from(Favorite color);let field_value String::from(Blue);let mut map HashMap::new();map.insert(field_name, field_value);// filed_name和field_value从这一刻开始失效println!({field_name}); // Errormap.insert(field_name, field_value);// 插入值的引用不会转移所有权 println!({field_name} {field_value}); // Ok
}访问 HashMap 中的值
使用 get 方法取出 K 对应的 V
参数K返回OptionV
use std::collections::HashMap;fn main() {let mut scores HashMap::new();scores.insert(String::from(Blue), 10);scores.insert(String::from(Yellow), 50);let team_name String::from(Blue);// 返回 Option 类型let score: Optioni32 scores.get(team_name);// 如果有值就用match取出match score {Some(i) println!({i}),None println!(not exits),};
}遍历 HashMap
use std::collections::HashMap;fn main() {let mut scores HashMap::new();scores.insert(String::from(Blue), 10);scores.insert(String::from(Yellow), 50);for (k, v) in scores { // 不加元素所有权会被转移或者用iter()方法println!({k} {v});}
}更新 HashMap
split_whitespace 是一个用于字符串处理的方法它可以将字符串按空白字符空格、制表符、换行符等分割成一个个单词或者单词片段并返回一个迭代器。
这个方法属于 str 类型的方法因此可以直接在字符串上调用。它不会修改原始字符串而是返回一个迭代器这个迭代器会产生原始字符串中每个单词或者单词片段的引用。
use std::collections::HashMap;fn main() {let mut scores HashMap::new();scores.insert(String::from(Blue), 10);// 1.覆盖旧值即替换现有的Vscores.insert(String::from(Blue), 25);println!({:?}, scores);// 2.如果不存在则插入保留现有的 V忽略新的 V// Entry的or_insert方法被定义为返回一个Entry键所指向值的可变引用// 假如这个值不存在就将参数作为新值插入哈希映射中并把这个新值的可变引用返回。scores.entry(String::from(Yellow)).or_insert(50); // 不存在Yellow会插入scores.entry(String::from(Blue)).or_insert(50); // 已经存在Bule不会插入println!({:?}, scores);// 3.基于旧值来更新值let text hello world wonderful world;let mut map HashMap::new();for word in text.split_whitespace() {// 方法or_insert实际上为我们传入的键返回了一个指向关联值的可变引用mut V// 这个可变引用进而被存储到变量count上let count: mut i32 map.entry(word).or_insert(0);// 为了对这个值进行赋值操作我们必须首先使用星号*来对count进行解引用*count 1;}println!({:?}, map);
}9 rust 错误处理
大部分情况下在编译时提示错误并处理
错误的分类
可恢复例如文件未找到可再次尝试不可恢复bug例如访问的索引超出范围
Rust 没有类似异常的机制
可恢复错误RreultT,E不可恢复错误中止运行的 panic! 宏
不可恢复错误与panic!
程序会在 panic! 宏执行时打印出一段错误提示信息展开并清理当前的 调用栈然后退出程序。 panic中的栈展开与终止
程序展开调用栈默认工作量大- rust 沿着调用栈往回走- 清理每个遇到的函数中的数据
立即中止调用栈- 不需要清理直接停止程序- 内存需要 os 进行清理假如项目需要使最终二进制包尽可能小可以在 Cargo.toml 文件中的 [profile] 区域添加 panic abort 来将 panic 的默认行为从展开切换为终止。
// 在发布模式中使用终止模式在配置文件中加入
[profile.release]
panic abort可恢复错误与 Result
Result处理可能出现错误的操作的一种标准方式是一个枚举类型 这里的 T 和 E 是泛型参数
enum ResultT,E {Ok(T), // 操作成功时Ok变体返回的数据类型是TErr(E), // 操作失败时Err变体返回的数据类型是E
}打开文件的例子
// 和 Option 一样Result 及其变体也是由 prelude 带入作用域因此不需要显示声明
use std::fs::File;
use std::io::ErrorKind;fn main() {let open_result File::open(hello.txt);let open_result match open_result {Ok(file) file,Err(error) match error.kind() {ErrorKind::NotFound match File::create(hello.txt) {Ok(fc) fc,Err(e) panic!(Error opening the file: {:?}, e),}other_error panic!(Error opening the file: {:?}, other_error),},};
}unwrap 和 expect
unwrap
当 Result 的返回值是 Ok 变体unwrap 就会返回 Ok 内部的值当 Result 的返回值是 Err 变体unwrap 则会替我们调用 panic! 宏。 let open_result File::open(hello.txt).unwrap(); // 无法自定义错误信息expect
与 unwrap 类似但是可以指定 panic! 附带的错误信息 let open_result File::open(hello.txt).exoect(打开文件失败);传播错误
当编写的函数中包含了一些可能会执行失败的调用时除了可以在函数中处理这个错误还可以将这个错误返回给调用者让他们决定应该如何做进一步处理。这个过程也被称作传播错误在调用代码时它给了用户更多的控制能力。与编写代码时的上下文环境相比调用者可能会拥有更多的信息和逻辑来决定应该如何处理错误。
use std::fs::File;
use std::io::{self, Read};fn read_username_from_file() - ResultString, io::Error {let f: ResultFile, io::Error File::open(hello.txt);let mut f: File match f {Ok(file) file,Err(e) return Err(e),};let mut s String::new();match f.read_to_string(mut s) {Ok(_) Ok(s),Err(e) Err(e),} // 函数返回此match表达式的值
}fn main() {let _ read_username_from_file();
}传播错误的快捷方式? 运算符只能用于返回类型为 Result 类型的函数
use std::fs::File;
use std::io::{self, Read};fn read_username_from_file() - ResultString, io::Error {
// 使用 ? 运算符当Err发生时会return Err否则会返回读取的 File
// 因此这里的 f 不需要使用 Result 类型let mut f: File File::open(hello.txt)?;let mut s: String String::new();f.read_to_string(mut s)?;Ok(s)
}fn main() {let _ read_username_from_file();
}? 与 from 函数
Trait std::convert::From 上的 from 函数用于错误之间的转换
被 ? 运算符所接收的错误值会隐式地被 from 函数处理。当 ? 调用 from 函数时?所接收的错误类型会被转化为当前函数返回类型所定义的错误类型。当一个函数拥有不同的失败原因却使用了统一的错误返回类型来同时进行表达时这个功能会十分有用。前提是每个错误类型都实现了转换为返回错误类型的 from 函数这时 ? 运算符就会自动帮我们处理所有的转换过程。
举例来说假设我们有一个函数 fn foo() - Resulti32, CustomError而在其中调用了一个可能返回 Resulti32, OtherError 类型的函数并使用了 ?运算符来处理错误。如果 OtherError 类型可以转换为 CustomError 类型那么编译器将会自动调用 From 实现来进行转换从而使得 Resulti32, OtherError 类型的错误被转换为 Resulti32, CustomError 类型。
链式调用
错误处理的开销通常不是程序性能的瓶颈链式调用一般只会影响代码可读性
fn read_username_from_file() - ResultString, io::Error {let mut s String::new();File::open(hello.txt)?.read_to_string(mut s)?;Ok(s)
}何时使用 panic!
总体原则
在定义一个可能失败的函数时优先考虑返回 Result否则就 panic!认为自己可以代替调用者决定某种情形是不可恢复的
场景建议
用户调用你的代码传入无意义的参数值panic!调用外部不可控代码返回非法状态你无法修复panic!当你的代码对值进行操作首先应该验证这些值panic!如果失败是可预期的Result例如解析字符串成数字
创建自定义类型来进行有效性验证
pub struct Guess {value: i32,
}impl Guess {pub fn new(value: i32) - Guess {// 对数据范围进行验证if value 1 || value 100 {panic!(Guess value must between 1 and 100);}Guess {value}}// 读取接口 getterpub fn value(self) - i32 {self.value}
}10.1 泛型
结构体
struct PointT, U { // 使用两个参数x和y的类型可以相同也可以不同x: T,y: U,
}fn main() {let both_integer Point { x: 5, y: 10 };let both_float Point { x: 1.0, y: 4.0 };let integer_and_float Point { x: 5, y: 4.0 };
}OptionT 枚举
enum OptionT { // 一个泛型参数Some(T),None,
}ResultT, E 枚举
enum ResultT, E {Ok(T),Err(E),
}方法定义中的泛型
例1
struct PointT {x: T,y: T,
}// 参数 T 放在impl后边表示在类型 T 上实现方法
implT PointT {// get_x方法是针对所有T类型都可以调用的fn get_x(self) - T { // 第一个参数为self因此是方法self.x}
}
// 具体类型不需要在 impl 后声明
impl Pointf32 {// 而distance_from_origin()方法只有参数为 f32 时才能调用fn distance_from_origin(self) - f32 {(self.x.powi(2) self.y.powi(2)).sqrt()}
}fn main() {let p Point { x: 5, y: 10 };println!(p.x {}, p.get_x());let p2: Pointf32 Point{ x: 5.0, y : 12.0};let dis p2.distance_from_origin();println!({dis})
}
例2
struct PointT {x: T,y: T,
}implT PointT {// get_x方法是针对所有T类型都可以调用的fn get_x(self) - T {self.x}
}impl Pointf32 {// 而distance_from_origin()方法只有参数为f32时才能调用fn distance_from_origin(self) - f32 {(self.x.powi(2) self.y.powi(2)).sqrt()}
}fn main() {let p Point { x: 5, y: 10 };println!(p.x {}, p.get_x());let p2: Pointf32 Point{ x: 5.0, y : 12.0};let dis p2.distance_from_origin();println!(distance_from_origin {dis});
}泛型代码的性能
Rust 通过在编译时进行泛型代码的 单态化monomorphization来保证效率。单态化是一个通过填充编译时使用的具体类型将通用代码转换为特定代码的过程。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。 例
let integer Some(5);
let float Some(5.0);当 Rust 编译这些代码的时候它会进行单态化。编译器会读取传递给 OptionT 的值并发现有两种 OptionT 一个对应 i32 另一个对应 f64 。为此它会将泛型定义 OptionT 展开为两个针对 i32 和 f64 的定义接着将泛型定义替换为这两个具体的定义。
enum Option_i32 {Some(i32),None,
}
enum Option_f64 {Some(f64),None,
}
fn main() {let integer Option_i32::Some(5);let float Option_f64::Some(5.0);
}使用泛型没有运行时开销当代码运行时它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。
10.2 Trait
Trait 告诉 rust 编译器某种类型具有哪些并且可以与其他类型共享的功能Trait抽象的定义共享行为Trait bounds约束泛型类型参数指定为实现了特定行为的类型Trait 与其他语言的接口interface类似但有些区别。
定义一个 Trait
Trait 的定义把方法签名放在一起来定义实现某种目的所必需的一组行为
关键字trait只有方法签名没有具体实现trait 可以有多个方法每个方法签名占一行以 ; 结尾实现该 trait 的类型必须提供具体的方法实现
文件名src/lib.rs
pub trait Summary {fn summarize(self) - String;
}为类型实现 trait
文件名src/lib.rs
// 声明为 pub以便依赖这个 crate 的 crate 也可以使用这个 trait
pub trait Summary {
// summarize 函数在Summary Trait中是一个关联函数在NewsArticle类型上实现后它是一个方法fn summarize(self) - String;
}pub struct NewsArticle {pub headline: String,pub location: String,pub author: String,pub content: String,
}
// 在NewsArticle类型上实现Summary这个trait
// trait 类型 T
impl Summary for NewsArticle {fn summarize(self) - String {format!({}, by {} ({}), self.headline, self.author, self.location)}
}
pub struct Tweet {pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}
// 在Tweet类型上实现Summary这个trait
impl Summary for Tweet {fn summarize(self) - String {format!({}: {}, self.username, self.content)}
}文件名main.rs
use _013_generics::Summary;
use _013_generics::Tweet;fn main() {let tweet Tweet {username: String::from(horse_ebooks),content: String::from(of course, as you probably already know),reply: false,retweet: false,};println!(1 new tweet: {}, tweet.summarize());
}实现 trait 的约束
可以在某个类型上实现某个 trait 的前提条件是这个类型或这个 trait 是在本地 crate 里定义的属于当前的 crate。例如可以为 _013_generics crate 的自定义类型 Tweet 实现如标准库中的Display trait这是因为 Tweet 类型位于 _013_generics crate 本地的作用域中。类似地也可以在 _013_generics crate 中为 VecT 实现 Summary 这是因为 Summary trait 位于 _013_generics crate 本地作用域中。
无法为外部类型来实现外部的 trait例如不能在 _013_generics crate 中为 VecT 实现 Display trait。这是因为 Display 和 VecT 都定义于标准库中它们并不位于 _013_generics crate 本地作用域中。这个限制是被称为 相干性coherence的程序属性的一部分或者更具体的说是 孤儿规则orphan rule其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码反之亦然。没有这条规则的话两个 crate 可以分别对相同类型实现相同的 trait而 Rust 将无从得知应该使用哪一个实现。
默认实现
有时为 trait 中的某些或全部方法提供默认的行为而不是在每个类型的每个实现中都定义自 己的行为是很有用的。这样当为某个特定类型实现 trait 时可以选择保留或重载每个方法的 默认行为。
文件名src/lib.rs
// 声明为 pub以便依赖这个 crate 的 crate 也可以使用这个 trait
pub trait Summary {
// 为 Summary trait 的 summarize 方法指定一个默认的字符串值fn summarize(self) - String {String::from((Read more...))}
}pub struct NewsArticle {pub headline: String,pub location: String,pub author: String,pub content: String,
}
// 对 NewsArticle 实例使用默认实现指定一个空的 impl 块即可
impl Summary for NewsArticle {}pub struct Tweet {pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}
// 在Tweet类型上自定义实现Summary这个trait默认实现的重写实现
// 虽然定义trait时创建了默认实现自定义该trait仍可以使用
impl Summary for Tweet {fn summarize(self) - String {format!({}: {}, self.username, self.content)}
}文件名src/main.rs
use _013_generics::{self, NewsArticle, Summary, Tweet};fn main() {let article NewsArticle {headline: String::from(Penguins win the Stanley Cup Championship!),location: String::from(Pittsburgh, PA, USA),author: String::from(Iceburgh),content: String::from(The Pittsburgh Penguins once again are the best \hockey team in the NHL.),};println!(New article available! {}, article.summarize());println!(-----------------);let tweet Tweet {username: String::from(horse_ebooks),content: String::from(of course, as you probably already know),reply: false,retweet: false,};println!(1 new tweet: {}, tweet.summarize());
}默认实现允许调用相同 trait 中的其他方法哪怕这些方法没有默认实现。为了使用这个版本的 Summary 只需在实现 trait 时定义 summarize_author 即可 文件名src/lib.rs
pub trait Summary {fn summarize_author(self) - String;
// summarize_author并没有进行默认实现但是在summarize里可以直接调用fn summarize(self) - String {format!((Read more from {}...), self.summarize_author())}
}pub struct Tweet {pub username: String,pub content: String,pub reply: bool,pub retweet: bool,
}
// 自定义实现summarize_author即可
impl Summary for Tweet {fn summarize_author(self) - String {format!({}, self.username)}
}文件名src/main.rs
use _013_generics::{self, Summary, Tweet};fn main() {let tweet Tweet {username: String::from(horse_ebooks),content: String::from(of course, as you probably already know),reply: false,retweet: false,};println!(1 new tweet: {}, tweet.summarize());
}trait 作为参数
impl Trait 语法适用于简单情况
// 定义Summary trait
pub trait Summary {
}
// 定义结构体并实现自定义Summary trait
pub struct NewsArticle {
}
impl Summary for NewsArticle {
}pub struct Tweet {
}
impl Summary for Tweet {
}// 实现了Summary这个Trait的类型才可以作为参数
// 1.impl Trait 语法
pub fn notify1(item: impl Summary) {println!(Breaking news! {}, item.summarize());
}// 2.Trait Bound 语法
pub fn notify2T: Summary(item: T) {println!(Breaking news! {}, item.summarize());
}
// 实际上impl trait 语法是 trait bound 的语法糖// 约束参数类型拥有更多的 trait使用 运算符即可
pub fn notify3(item: (impl Summary Display)) {println!(Breaking news! {}, item.summarize());
}pub fn notify4T: Summary Display(item: T) {println!(Breaking news! {}, item.summarize());
}// Trait bound 使用 where 子句
// 不使用 where
pub fn some_function1T: Display Summary, U: Clone Debug(t: T, u: U) - String {format!(Breaking news! {}, t.summarize())
}
// 使用 where
pub fn some_function2T, U(t: T, u: U) - String
whereT: Display Summary,U: Clone Debug,
{format!(Breaking news! {}, t.summarize())
}实现 largest 函数
使用 trait 编写求最大值的函数约束类型拥有 PartialOrd trait这样就能进行比较 关键词切片 slice解引用的使用
// largest 函数的参数类型是 [T]也就是说它接受一个切片slice而不是一个 Vec
// 编译后参数变成 list: [String]
fn largestT: PartialOrd(list: [T]) - T {let mut max_word: T list[0];// item应该是 T类型在本测试用例中即为 String for item in list.iter() {if *item *max_word {// item解引用后为 T 类型本例中即为 Stringmax_word item; // 将max_word这个借用更新为指向新的数据item}}max_word
}
fn main() {let str_list: VecString vec![String::from(hello), String::from(world)];let result largest(str_list);println!(the largest word is {}, result);
}trait 作为返回类型
返回值类型指定为 impl Summary
fn returns_summarizable() - impl Summary {Tweet {username: String::from(horse_ebooks),content: String::from( of course, as you probably already know, people),reply: false,retweet: false,}
}注意impl Trait 只能返回确定的一种类型返回可能不同类型的代码会报错。也就是说如果在 returns_summarizable() 函数中编写的代码既可能返回 Tweet 也可能返回 NewsArticle 这时是无法编译通过的。
使用 trait bound 有条件地实现方法
在使用泛型类型参数的 impl 块上使用 Trait bound可以约束具有特定 trait 的类型才能拥有某些方法
use std::fmt::Display;
struct PairT {x: T,y: T,
}
// 无论PairT是什么类型都实现了 new 这个关联函数
implT PairT {fn new(x: T, y: T) - Self {Self { x, y }}
}
// 针对PairT对 T 约束trait必须同时实现了Display PartialOrd两个trait
// 满足条件的类型 PairT才拥有方法 cmp_display
implT: Display PartialOrd PairT {fn cmp_display(self) {if self.x self.y {println!(The largest member is x {}, self.x);} else {println!(The largest member is y {}, self.y);}}
}约束类型具有某些 trait才能拥有实现的 trait这叫 覆盖实现blanket implementations
// 对拥有 Display trait 的类型实现 ToString trait
implT: Display ToString for T {// --snip--
}10.3 生命周期
生命周期的主要目标是避免悬垂引用dangling references。
fn main() {let r;{let x 5;r x;} // r 引用的值在尝试使用之前就离开了作用域println!(r: {}, r);
}上述代码是无法编译通过的
Rust 编译器有一个 借用检查器borrow checker它比较作用域来确保所有的借用都是有效的。
fn main() {let r; // ----------- a// |{ // |let x 5; // --- b |r x; // | |} // - |// |println!(r: {}, r); // |
} // ---------r 和 x 的生命周期注解分别叫做 a 和 b可以看出被引用的对象比它的引用者存在的时间更短。
生命周期的标注
生命周期的标注不会改变引用生命周期长度当指定了泛型生命周期参数函数可以接收带有任何生命周期的引用生命周期的标注描述了多个引用的生命周期间的关系但不影响生命周期
语法
以 开头通常全小写且非常短例如 a在引用的 符号后使用空格将标注和引用类型分开
例子
i32 // 没有生命周期参数的引用
a i32 // 带有显式生命周期的引用
a mut i32 // 带有显式生命周期的可变引用单个的生命周期注解本身没有多少意义因为生命周期注解告诉 Rust 多个引用的泛型生命周 期参数如何相互联系的。
fn main() {let string1 String::from(abcd);let string2 xyz;let result longest(string1.as_str(), string2);println!(The longest string is {}, result);
}
// 在函数名后使用标注生命周期
// 这表示参数xy和返回值这些引用都有相同的生命周期
// 进行生命周期标注并没有改变任何传入值或返回值的生命周期而是指出任何不满足这个约束条件的值都将被借用检查器拒绝
fn longesta(x: a str, y: a str) - a str {if x.len() y.len() {x} else {y}
}通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用 a 的实际含义是 longest 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致
fn main() {let string1 String::from(long string is long);let result;{let string2 String::from(xyz);result longest(string1.as_str(), string2.as_str());}// string2在此处失效而result是与传入参数生命周期较小者保持一致因此result也在此处失效println!(The longest string is {}, result);
}fn longesta(x: a str, y: a str) - a str {if x.len() y.len() {x} else {y}
}深入理解生命周期
如果返回值固定为 x那么就不需要指定参数 y 的生命周期了
fn longesta(x: a str, y: str) - a str {x
}当从函数返回一个引用返回值的生命周期需要与一个参数的生命周期相匹配。如果返回的引用 没有指向任何一个参数那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用因为它将会在函数结束时离开作用域。
fn longesta(x: str, y: str) - a str {let result String::from(really long string);result.as_str()
}
// longest 函数的结尾将离开作用域并被清理而我们尝试从函数返回一个 result 的引用
//无法指定生命周期参数来改变悬垂引用而且 Rust 也不允许我们创建一个悬垂引用// 这时我们可以直接返回这个值把所有权移交给函数的调用者
fn longesta(x: str, y: str) - String {let result String::from(really long string);result
}综上生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦它们形成了某种关联Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
结构体定义中的生命周期标注
Struct 里可包括
自持有的类型i32String…引用需要在每个引用上添加生命周期的标注
struct ImportantExcerpta {part: a str,
}
fn main() {let novel String::from(Call me Ishmael. Some years ago...);let first_sentence novel.split(.).next().expect(Could not find a .);// first_sentence 这个被引用者的生命周期可以覆盖 i 这个结构体的生命周期因此编译可以通过let i ImportantExcerpt {part: first_sentence,};
}生命周期的省略
这个函数没有声明生命周期但编译能通过这是因为 rust 编译器考虑了一些特殊场景这些场景是可预测的并且遵循几个明确的模式。如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。
fn first_word(s: str) - str {let bytes s.as_bytes();for (i, item) in bytes.iter().enumerate() {if item b {return s[0..i];}}s[..]
}生命周期省略规则不会提供完整的推断
如果应用规则后引用的生命周期仍然模糊不清 - 编译错误解决办法手动添加生命周期标注表面引用间的相互关系
输入、输出生命周期
输入生命周期在函数/方法的参数中 输出生命周期在函数/方法的返回值中
生命周期省略的三个规则
规则 1每个引用类型的参数都有自己的生命周期函数有几个引用参数就有几个生命周期规则 2如果只有 1 个输入生命周期参数那么该生命周期被赋给所有的输出生命周期参数规则 3如果有多个输入生命周期参数但其中一个是 self 或 mut self 是方法那么 self 的生命周期会给赋给所有的输出生命周期参数
编译器使用 3 个规则在没有显示标注生命周期的情况下来确定引用的生命周期
规则 1 应用于输入生命周期规则 2、3 应用与输出生命周期如果编译器应用完 3 个规则之后仍然有无法确定生命周期的引用 - 就会报错这些规则适用于 fn 定义和 impl 块
方法定义中的生命周期标注
在 struct 上使用生命周期实现方法语法和泛型参数的语法一样
在哪声明和使用生命周期参数依赖于生命周期参数是否和字段、方法的参数或返回值有关
struct 字段的生命周期名
在 impl 后声明在 struct 名后使用这些生命周期是 struct 类型一部分
impl 块内的方法签名中
引用必须绑定于 struct 字段引用的生命周期或者引用是独立的也可以生命周期的省略规则经常使得方法中的生命周期标注不是必须的
struct ImporExcerpta {part: a str,
}impla ImportantExcerpta {
// 根据第一条省略规则我们可以不用为方法中的self引用标注生命周期fn level(self) - i32 {3}
// 这里有两个输入生命周期所以Rust通过应用第一条生命周期省略规则给了self和announcement各自的生命周期
// 由于其中一个参数是self返回类型被赋予了self的生命周期
// 因此所有的生命周期就都被计算出来了fn announce_and_return_part(self, announcement: str) - str {println!(Attention please: {}, announcement);self.part}
}静态生命周期
static 是一个特殊的生命周期整个程序的持续时间。 例如所有的字符串字面值都拥有 static 生命周期 let s: static str I have a static lifetime.;为引用指定 static 生命周期前要三思是否需要引用在程序整个生命周期内都存活。
use std::fmt::Display;fn longest_with_an_announcementa, T(x: a str, y: a str, ann: T) - a strwhere T: Display
{println!(Announcement! {}, ann);if x.len() y.len() {x} else {y}
}11 自动化测试
// 创建一个新的库项目 adder
cargo new adder --lib // 运行测试
cargo test
// 0 measured 统计是针对性能测试的0 filtered out 是指没有过略掉任何测试使用 assert! 宏检查测试结果
assert! 宏来自标准库用来确定某个状态是否为 true测定为 false 时调用 panic!
struct Rectangle {width: u32,height: u32,
}impl Rectangle {fn can_hold(self, other: Rectangle) - bool {self.width other.width self.height other.height}
}#[cfg(test)]
mod tests {use super::*; // 使用 super 允许我们引用父模块中的已知项#[test]fn larger_can_hold_smaller() {let larger Rectangle {width: 8,height: 7,};let smaller Rectangle {width: 5,height: 1,};assert!(larger.can_hold(smaller));}
}assert_eq! 和 assert_ne! 俩个宏分别用来判断两个参数是否相等 或 不等相当于用 assert! 宏加 或 ! 来判断使用这两个宏的好处是断言失败时自动打印出两个参数的值这也要求了参数必须实现了 PartalEq 和 Debug Traits 所有基本类型和标准库里大部分都实现了
可以传递一个可选的失败信息参数可以在测试失败时将自定义失败信息一同打印出来。
验证代码发生错误should_panic
发生 panic 时测试通过否则测试不通过
expect 函数用来具会确保错误信息中包含其提供的文本
pub struct Guess {value: i32,
}
// --snip--impl Guess {pub fn new(value: i32) - Guess {if value 1 {panic!(Guess value must be greater than or equal to 1, got {}., value);} else if value 100 {panic!(Guess value must be less than or equal to 100, got {}., value);}Guess { value }}
}# [cfg(test)]
mod tests {use super::*;#[test]#[should_panic(expected less than or equal to 100)]// 发生 panic 时错误信息必须包含该字符串才能算通过测试fn greater_than_100() {Guess::new(200);}
}使用 ResultT, E
无需 panic使用 ResultT, E 作为返回类型编写测试Ok 测试通过Err 测试失败
#[cfg(test)]
mod tests {#[test]fn it_works() - Result(), String {if 2 2 4 {Ok(())} else {Err(String::from(two plus two does not equal four))}}
}因为使用 Result 无需发生 panic因此此时不要使用 #[should_panic]
控制测试如何运行
改变 cargo test 的行为添加命令行参数
默认行为并行运行所有测试不显示所有输出
命令行参数 针对 cargo test 的参数紧跟在 cargo test 后 针对 测试可执行程序放在 -- 后
cargo test --help 提示 cargo test 的有关参数 cargo test -- --help 提示在分隔符后能够使用的有关参数 cargo test -- --test-threads1将测试线程设置为 1 告诉程序不要使用任何并行机制。在有共享的状态时测试就不会潜在的相互干扰。
默认情况下当测试通过时Rust 的测试库会截获打印到标准输出的所有内容。如果测试失败了则会看到所有标准输出和其他错误信息。但是如果我们也想看到成功时的输出就可以使用下边的命令 cargo test -- --show-output 显示成功测试的输出
指定只运行某个测试
pub fn add_two(a: i32) - i32 {a 2
}
#[cfg(test)]
mod tests {use super::*;#[test]fn add_two_and_two() {assert_eq!(4, add_two(2));}#[test]fn add_three_and_two() {assert_eq!(5, add_two(3));}#[test]fn one_hundred() {assert_eq!(102, add_two(100));}
}只运行名为 one_hundred 的测试
cargo test one_hundred
// test tests::one_hundred ... ok只运行 tests 模块里的测试
cargo test tests
/*
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
*/运行 add 开头的测试
cargo test add
/*
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
*/忽略某些测试
给要忽略的测试加上 ignore 属性 #[test]#[ignore]fn add_three_and_two() {assert_eq!(5, add_two(3));}
// 执行 cargo test
/*
test tests::add_three_and_two ... ignored
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
*/执行完这些测试又想执行具有 ignore 属性的测试注意是命令行里的参数是 ignored 而不是 ·ignore
cargo test -- --ignored测试的分类
单元测试 小、专注 一次对一个模块进行隔离的测试 可测试 private 接口 集成测试 在库外部。和其他外部代码一样使用你的代码 只能使用 public 接口 可能在每个测试中使用到多个模块
单元测试
测试模块和 #[cfg(test)]
#[cfg(test)] 注解告诉 Rust 只在执行 cargo test 时才编译和运行 tests 模块的测试代码而在运行 cargo build 时不这么做
集成测试在不同的目录它不需要 #[cfg(test)] 标注
cfg: configuration 配置
告诉 rust 下面的条目只有在指定的配置选项下才被包含配置选项 test由 rust 提供用来编译和运行测试。
集成测试
在 rust 中集成测试完全位于被测试库的外部目的是测试被测试库的多个部分是否能正确的一起工作集成测试的覆盖率很重要。
tests 目录
为了编写集成测试需要在项目根目录创建一个 tests 目录与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件Cargo 会将每一个文件当作单独的 crate 来编译。只有调用 cargo test 命令时才会编译 tests 目录下的文件。
运行指定的集成测试
运行一个特定的集成测试cargo test 函数名运行某个测试文件内的所有测试cargo test --test 文件名
集成测试的子模块
直接在 tests 目录下创建 common.rs 会导致被 rust 认为是测试代码tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。一旦拥有了 tests/common/mod.rs就可以将其作为模块以便在任何集成测试文件中使用。
二进制 crate 的集成测试
如果项目是二进制 crate 并且只包含 src/main.rs 而没有 src/lib.rs
不能在 tests 目录创建集成测试无法把 src/main.rs 的函数导入作用域因为只有库 crate 才会向其他crate 暴露了可供调用和使用的函数二进制 crate 意味着单独运行
这就是许多 Rust 二进制项目使用一个简单的 src/main.rs 调用 src/lib.rs 中的逻辑的原因之一。因为通过这种结构集成测试 就可以 通过 extern crate 测试库 crate 中的主要功能了而如果这些重要的功能没有问题的话src/main.rs 中的少量代码也就会正常工作且不需要测试。
12 项目实例命令行程序
重构改进模块性和错误处理
为了改善我们的程序这里有四个问题需要修复而且它们都与程序的组织方式和如何处理潜在错误有关。
第一main 现在进行了两个任务它解析了参数并打开了文件。对于一个这样的小函数这并不是一个大问题。然而如果 main 中的功能持续增加main 函数处理的独立任务也会增加。当函数承担了更多责任它就更难以推导更难以测试并且更难以在不破坏其他部分的情况下做出修改。最好能分离出功能以便每个函数就负责一个任务。这同时也关系到第二个问题query 和 file_path 是程序中的配置变量而像 contents 则用来执行程序逻辑。随着 main 函数的增长就需要引入更多的变量到作用域中而当作用域中有更多的变量时将更难以追踪每个变量的目的。最好能将配置变量组织进一个结构这样就能使它们的目的更明确了。第三个问题是如果打开文件失败我们使用 expect 来打印出错误信息不过这个错误信息只是说 Should have been able to read the file 。读取文件失败的原因有多种例如文件不存在或者没有打开此文件的权限。目前无论处于何种情况我们只是打印出“文件读取出现错误”的信息这并没有给予使用者具体的信息第四我们不停地使用 expect 来处理不同的错误如果用户没有指定足够的参数来运行程序他们会从 Rust 得到 index out of bounds 错误而这并不能明确地解释问题。如果所有的错误处理都位于一处这样将来的维护者在需要修改错误处理逻辑时就只需要考虑这一处代码。将所有的错误处理都放在一处也有助于确保我们打印的错误信息对终端用户来说是有意义的。
让我们通过重构项目来解决这些问题。
二进制项目关注点分离的指导性原则
将程序拆分成 main.rs 和 lib.rs 并将程序的业务逻辑放入 lib.rs 中。当命令行解析逻辑比较少时可以保留在 main.rs 中。当命令行解析开始变得复杂时也同样将其从 main.rs 提取到 lib.rs 中。
程序已经编写了测试代码并添加了大小写敏感的环境变量检测代码
文件名 src/main.rs
use std::env;// 一般只引入至父模块即可使用时env::args()
use std::process;
use _010_miniGrep::Config;
use _010_miniGrep::run;fn main() {
// 使用collect函数产生集合但需要显示指明变量的集合类型这里为VecStringlet args: VecString env::args().collect();
// env::args()函数无法处理非法的unicode字符可以使用args_os()// println!({:?}, args); // 打印输入的参数列表let config Config::new(args).unwrap_or_else(|err| {// 标准错误输出函数会将错误输出到终端上而不是output.txt文件里这样就将错误和输出分开了eprintln!(Problem parsing arguments: {}, err);process::exit(1);});if let Err(e) run(config) {eprintln!(Application error: {e});process::exit(1);}
}文件名 src/lib.rs
use std::error::Error;
use std::fs;// 处理文件相关的事务
use std::env;// ()表示空元组什么都不返回
// 否则返回一个实现了 error 这个trait的类型可以理解为 “任何类型的错误”
pub fn run(config: Config) - Result(), Boxdyn Error {// 使用 ? 运算符传播错误当Err发生时会return Err否则会返回()空元组let contents fs::read_to_string(config.filename)?;let res if config.case_sensitive {search_case_sensitive(config.query, contents)} else {search_case_insensitive(config.query, contents)};for line in res {println!({line});}Ok(())
}// 改进模块
pub struct Config {pub query: String,pub filename: String,pub case_sensitive: bool,
}
impl Config {pub fn new(args: [String]) - ResultConfig, static str {
// 这里的 args 是指向 VecString 的切片if args.len() 3 {return Err(not enough arguments);}let query args[1].clone();let filename args[2].clone();// env::var()函数检测环境变量并返回result调用 is_err() 检查该环境变量是否被设置// 如果没有被设置就是err函数即为返回truetrue 对应的是调用 case_sensitivelet case_sensitive env::var(CASE_INSENSITIVE).is_err();Ok(Config {query, filename, case_sensitive})}
}// 使用search在contents中寻找包含duct字符串的行并返回所有行
pub fn search_case_sensitivea(query: str, contents: a str) - Veca str {let mut res Vec::new();for line in contents.lines() {if line.contains(query) {res.push(line);}}res
}pub fn search_case_insensitivea(query: str, contents: a str) - Veca str {let mut res Vec::new();let query query.to_lowercase();for line in contents.lines() {if line.to_lowercase().contains(query) {res.push(line);}}res
}#[cfg(test)]
mod tests {use super::*;#[test]fn case_sensitive() {let query duct;let contents \
Rust:
safe, fast, productive.
Pick three.
Duct tape.;assert_eq!(vec![safe, fast, productive.], search_case_sensitive(query, contents));}#[test]fn case_insensitive() {let query rUst;let contents \
Rust:
safe, fast, productive.
Pick three.
Trust me.;assert_eq!(vec![Rust:, Trust me.], search_case_insensitive(query, contents));}
}// 进行测试
cargo run to poem.txt
/* 输出
Are you nobody, too?
How dreary to be somebody!
*/// 设置环境变量存在实际上代码的逻辑是检测到此环境变量存在就会认为大小写不敏感
$env:CASE_INSENSITIVE1// 进行测试
cargo run to poem.txt
/* 输出
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
*/// 将环境变量移除
Remove-Item Env:\CASE_INSENSITIVE