基本操作方法
- !符号,系统宏使用这个
- use std::io 引入 想象成 std.io
- 变量名前添加
mut(mutability,可变性)let mut bananas = 5; ::new那一行的::语法表明new是String类型的一个关联函数- read_line(&mut guess)
&表示这个参数是一个引用 {}也可以打印多个值:第一对{}使用格式化字符串之后的第一个值,第二对则使用第二个值,- use rand::Rng
Rng是一个 trait,它定义了随机数生成器应实现的方法 - 区间表达式采用的格式为
start..end 1..=100 cargo doc --open命令来构建所有本地依赖提供的文档;- 不过 Rust 允许用一个新值来遮蔽
- loop 循环 会持续循环内容 知道 break 退出 continu 中断进入下一次循环
- match 可以比较多次情况,选项为 Ok 和 Err 时能代替 expect
1、基本概念
1-1、变量和可变性
1-1-1-1、 可变性
let space = " ";
let space = space.len();
这样可以遮蔽 但如果你使用变量 则会报错 因为我们不能修改变量的类型 涉及到变量类型的修改需要遮蔽
错误示例
let mut spaces = " ";
spaces = spaces.len();
1-2、数据类型
1-2-1、基本数据类型
标量 ( scalar )类型表示单个值。Rust 有 4 个基本的标量类型:整型、浮点型、布尔型和字符
i&u 可选值 8,16,32,64,128,size
整型
i 是英文单词 integer 的首字母,与之相反的是 u,u无符号 i有符号
| 类型 | 存储数字范围 | 十进制 |
|---|---|---|
| i8 | -(2 ^7^ ) ~ 2^7^ - 1 | -128 ~ 127 |
| u8 | 0 ~ 2^8^ - 1 | 0 ~ 255 |
数字字面量还可以使用 _ 作为可视分隔符以方便读数,如 1_000,此值和 1000 相同
整数溢出 在 u8 的情况下,256 变成 0,257 变成 1,依此类推
| 类型 | 知识点 |
|---|---|
| 浮点 | f64(default)更加精准 |
| 布尔 | bool |
1-2-2、符合数据类型
1、元组类型
let tup: (i32, f64, u8) = (500, 6.4, 1);
tup.0
let (x,y,z) = tup
2、数组类型
明确元素数量不需要改变时,数组会更有用,大小允许增长或缩小。如果不确定是使用数组还是 vector,那就应该使用一个 vector
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a=[3;5] 等效于let a = [3, 3, 3, 3, 3]
1-3、函数
Rust 代码中的函数和变量名使用下划线命名法( snake case ,直译为蛇形命名法)规范风格 another_function
函数调用是一个表达式。宏调用是一个表达式。我们用来创建新作用域的大括号(代码块) {} 也是一个表达式,表达式的结尾没有分号。如果在表达式的末尾加上分号,那么它就转换为语句,而语句不会返回值。
返回值
- 在 Rust 中,函数的返回值等同于函数体最后一个表达式的值
- 使用
return关键字和指定值,可以从函数中提前返回;但大部分函数隐式返回最后一个表达式。
let y = {
let x = 3;
x + 1
};
println!("这是y{}", y);//这个是成立的 但加上;后,会从表达式变成语句,会报错
fn five() -> i8 {
return -2 + 2;
}
等效于
fn five() -> i8 {
-2 + 2
}
不能加;
1-4、控制流
1-4-1、if
if 是一个表达式,我们可以在 let 语句的右侧使用它来将结果赋值给一个变量;
if的每个分支的可能的返回值都必须是相同类型;
let condition = false;
let number = if condition { 5 } else { 6 };
1-4-2、循环
Rust 有三种循环:loop、while 和 for。
loop
loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。停止关键字 break
fn loopfn() {
let mut x = 0;
let _res = loop {
if x == 100 {
break x;
}
x += 1;
};
println!("值{}", x)
}//loop新婚换可以使用break 返回出值 类似于 if
while
在程序中计算循环的条件也很常见。当条件为真,执行循环。当条件不再为真,调用 break 停止循环。这个循环类型可以通过组合 loop、if、else 和 break 来实现;
for
可以使用 for 循环来对一个集合的每个元素执行一些代码。
Range,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列,start..end 包左不包右
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
2、所有权
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者 ( owner )的变量。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
2-1、所有权基本概论
2-1-1、string
Rust 有第二个字符串类型,String。这个类型管理被分配到堆上的数据,所以能够存储在编译时未知大小的文本。可以使用 from 函数基于字符串字面量来创建 String
双冒号(::)运算符允许我们将特定的 from 函数置于 String 类型的命名空间(namespace)下,而不需要使用类似 string_from 这样的名字
let s = String::from("hello");
s.push_str(", world!"); // push_str() 在字符串后追加字面值
2-1-2、移动
栈内存
所以这两个 5 被放入了栈中
let x = 5;
let y = x;
以下的代码会对指针产生移动,为了性能考虑,并不会赋值堆内存,而是移动其指针
Rust 同时使第一个变量无效了,这个操作被称为 移动 ( move )
let s1 = String::from("hello");
let s2 = s1;
//这段代码不能运行
//当变量离开作用域后,Rust 自动调用 drop 函数并清理变量的堆内存。
两个指针指向同一个堆,这样会导致在离开时进行两次释放,这是一个叫做 二次释放 ( double free )的错误
Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。
2-1-3、克隆
可以使用一个叫做 clone 的通用函数
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
2-1-4、拷贝
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
像整型这样的在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的
Rust 有一个叫做 Copy trait 的特殊标注,可以用在类似整型这样的存储在栈上的类型上
2-1-5、所有权函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() {
let s: String = String::from("hello");
let s1: String = s.clone();
takes_ownership(s);
println!("smain{}", s1);
let x = 5; // x 进入作用域
makes_copy(x);
println!("xmain{}", x);
}
fn takes_ownership(s: String) {
println!("s,{}", s)
}
fn makes_copy(some_integer: i32) {
// some_integer 进入作用域
println!("{}", some_integer);
}
传入值进入函数 栈类型会重新赋值
传入堆的值,会将指针改变指向,因为后续指针变了所以 s 的值就没有了。形参 s 的指针就指向实参的堆了
返回值也可以转移所有权
我们可以使用元组来返回多个值
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度
(s, length)
}
2-2、引用
fn main() {
println!("Hello, world!");
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("len{},s1{}", len, s1);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
//传入后都不会改变指针位置
& 符号就是 引用 ,它们允许你使用值但不获取其所有权。
注意:与使用
&引用相反的操作是 解引用 ( dereferencing ),它使用解引用运算符,*。
函数签名使用 & 来表明参数 s 的类型是一个引用;
可变引用
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
可变引用有一个很大的限制:在同一时间,只能有一个对某一特定数据的可变引用。尝试创建两个可变引用的代码将会失败
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
数据竞争,rust不会编译
* 两个或更多指针同时访问同一数据。
* 至少有一个指针被用来写入数据。
* 没有同步数据访问的机制。
可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能 同时 拥有
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用
let r2 = &mut s;
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题
println!("{}, {}, and {}", r1, r2, r3);
可变会通过指针访问堆数据,如果不用 mut,&仅仅是借用,无法进行修改,没有存在所有权在&中;
不能在拥有不可变引用的同时拥有可变引用,多个不可变引用是可以的
一个引用的作用域从声明的地方开始一直持续到最后一次使用为止
let mut s = String::from("hello");
let r1 = &s; // 没问题
let r2 = &s; // 没问题
println!("{} and {}", r1, r2);
// 此位置之后 r1 和 r2 不再使用
let r3 = &mut s; // 没问题
println!("{}", r3);
let r4 = &mut s; // 没问题
println!("r4{}", r4);
//不可变引用 r1 和 r2 的作用域在 println! 最后一次使用之后结束,这也是创建可变引用 r3 的地方。它们的作用域没有重叠,所以代码是可以编译的
2-2-1、悬垂指针
悬垂指针是其指向的内存可能已经被分配给其它持有者
fn dangle() -> &String { // dangle 返回一个字符串的引用
let s = String::from("hello"); // s 是一个新字符串
&s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!
2-2-2、引用总结
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效的。
- 如果要有多个可用,需要不在一个作用域内
- 一个引用的作用域从声明的地方开始一直持续到最后一次使用为止
2-3、切片 slice
另一个没有所有权的数据类型是 slice 。slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。
2-3-1、字符串 slice
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
let slice = &s[0..2];
let slice = &s[..2];
let slice = &s[3..len];
let slice = &s[3..];
let slice = &s[0..len];
let slice = &s[..];
“字符串 slice” 的类型声明写作 &str
字符串 slice,正如你想象的那样,是针对字符串的。不过也有更通用的 slice 类型。考虑一下这个数组
3、结构体组织关联数据
3-1、定义
3-1-1、结构体
类似于对象
#[derive(Debug)]
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
email: String::from("hello.com"),
active: true,
username: String::from("姬霓太美"),
sign_in_count: 123456,
};
user1.email = String::from("我是你的谁.com");
// println!("{:?}", user1);
test_fn(&mut user1);
println!("{:?}", user1);
}
fn test_fn(test: &mut User) {
test.sign_in_count = 5;
println!("{:?}", test);
}
注意 所有权机制
字段初始化简写语法
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值 ..user1 必须放在最后
.. 语法指定了剩余未显式设置值的字段应有与给定实例对应字段相同的值。
let user2 = User {
username: String::from("马喽"),
..user1
};
所有权会发生移动
3-1-2、元组
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
//元组实际使用场景
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
3-1-3、单元结构体
你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
3-1-4、打印结构体
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
在结构体定义之前加上外部属性 #[derive(Debug)]
两种风格 {:?} {:#?}
dbg!宏打印
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
//[src/main.rs:14] &rect1 = Rectangle {
//width: 60,
//height: 50,
//}
3-2、方法语法
方法 与函数类似:它们使用 fn 关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文,将分别在第 6 章和第 17 章讲解),并且它们第一个参数总是 self,它代表调用该方法的结构体实例。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {//impl 块中的所有内容都将与 Rectangle 类型相关联
fn area(&self) -> u32 {
self.width * self.height
}//在 area 的签名中,使用 &self 来替代 rectangle: &Rectangle,&self 实际上是 self: &Self 的缩写
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
println!("Hello, world!{}", rect1.width());同名的情况下,()是方法,不加()是值
println!("Hello, world!{}", rect1.width);
}
如果想要在方法中改变调用方法的实例,需要将第一个参数改为
&mut self通过仅仅使用self作为第一个参数来使方法获取实例的所有权是很少见的 这种技术通常用在当方法将self转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
当我们在
rect1.width后面加上括号时。Rust 知道我们指的是方法width。当我们不使用圆括号时,Rust 知道我们指的是字段width。
3-2-1、关联函数
所有在 impl 块中定义的函数被称为 关联函数 ( associated function )
我们可以定义不以 self 为第一参数的关联函数(因此不是方法)
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。第 7 章会讲到模块。
区别 关联函数需要通过::语法调用
3-2-2、多个 impl 块
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
4、枚举和模式匹配
4-1、枚举
枚举::枚举值
4-1-1、定义枚举
#[derive(Debug)]
enum IpAddrkind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrkind,
address: String,
}
fn main() {
let four = dbg!(IpAddrkind::V4);
let six = IpAddrkind::V6;
dbg!(four);
route(IpAddrkind::V4);
route(IpAddrkind::V6);
let home = IpAddr {
kind: IpAddrkind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
fn route(ip_type: IpAddrkind) {}
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 IpAddrKind::V4 和 IpAddrKind::V6 都是 IpAddrKind 类型的。例如,接着可以定义一个函数来获取任何 IpAddrKind:
4-1-2、枚举成员
#[derive(Debug)]
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
println!("home,{:?}", home)
我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了
每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 V4 地址存储为四个 u8 值而 V6 地址仍然表现为一个 String,这就不能使用结构体了。枚举则可以轻易地处理这个情况:
莫非 模板字符串出现了
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
兼容各种类型
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
/////////
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
4-1-3、枚举的 impl
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
println!("hello")
}
}
fn impl_test() {
let msg = Message::Write(String::from("wukong"));
println!("msg{:?}", msg);
msg.call()
}
4-1-4、Option 枚举
Option 类型应用广泛是因为它编码了一个非常普遍的场景,即一个值要么有值要么没值
Rust 并没有很多其他语言中有的空值功能。 空值 (Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>,枚举可以直接拿出来
可以推论出不同类型
可以在函数中直接使用 None,而不需要写成 Option::None,这是因为 Rust 提供了一个方便的语法糖,使得 None 可以直接作为 Option 类型的值使用
enum Option<T> {
Some(T),
None,
}
let some_number = Some(5);
let some_string = Some("a string");
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
//这段代码不能编译,因为它尝试将 Option<i8> 与 i8 相加
option 是内置的,可以直接调用
#[derive(Debug)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
None,
}
fn main() {
println!("Hello, world!");
let val = Value_in_cents(Coin::Penny);
let val: Option<u8> = Value_in_cents(Coin::None);
println!("{:?}", val)
}
fn Value_in_cents(coin: Coin) -> Option<u8> {
match coin {
Coin::Penny => {
println!("Lucky penny!");
Some(1)
}
Coin::Dime => Some(5),
Coin::Nickel => Some(10),
Coin::Quarter => Some(25),
Coin::None => None,
}
}
4-2-5、例子
pub mod staff_mod {
use crate::const_v::arr_enum::staff::StaffEnum;
use std::collections::HashMap;
#[derive(Debug)]
pub struct Company {
department: HashMap<String, Vec<String>>,
}
impl Company {
pub fn new() -> Self {
Company {
department: HashMap::new(),
}
}
pub fn add(&mut self, name: String, department: String) {
self.department
.entry(department)
.or_insert(vec![])
.push(name)
}
pub fn get_department(&self, department_name: String) -> Option<Vec<String>> {
let res: Option<&Vec<String>> = self.department.get(&department_name);
let res: Option<Vec<String>> = res.cloned();
if let Some(mut vec) = res {
vec.sort();
return Some(vec);
} else {
return None;
}
// match res {
// Some(vec) => println!("{:?}", vec),
// None => println!("None"),
// }
}
}
}
4-2、match
可以把 match 表达式想象成某种硬币分类器:硬币滑入有着不同大小孔洞的轨道,每一个硬币都会掉入符合它大小的孔洞。同样地,值也会通过 match 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
4-2-1、绑定值
#[derive(Debug)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
None,
}
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}
fn main() {
println!("Hello, world!");
let val = Value_in_cents(Coin::Penny);
let val: Option<u8> = Value_in_cents(Coin::None);
let val: Option<u8> = Value_in_cents(Coin::Quarter(UsState::Alabama));
println!("{:?}", val)
}
fn Value_in_cents(coin: Coin) -> Option<u8> {
match coin {
Coin::Penny => {
println!("Lucky penny!");
Some(1)
}
Coin::Dime => Some(5),
Coin::Nickel => Some(10),
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
Some(25)
}
Coin::None => None,
}
}
可以传入枚举值 在 match 中的回调获取当前传入的值
4-2-2、Option 匹配
#[derive(Debug)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
None,
}
#[derive(Debug)] // 这样可以立刻看到州的名称
enum UsState {
Alabama,
Alaska,
// --snip--
}
fn main() {
println!("Hello, world!");
let val = Value_in_cents(Coin::Penny);
let val: Option<u8> = Value_in_cents(Coin::None);
let val: Option<u8> = Value_in_cents(Coin::Quarter(UsState::Alabama));
println!("{:?}", val);
let five: Option<i32> = dbg!(Some(5));
println!("{:?}", five);
let six = plus_one(five);
println!("{:?}", six);
let none = plus_one(None);
}
fn Value_in_cents(coin: Coin) -> Option<u8> {
match coin {
Coin::Penny => {
println!("Lucky penny!");
Some(1)
}
Coin::Dime => Some(5),
Coin::Nickel => Some(10),
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
Some(25)
}
Coin::None => None,
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
可以通过使用 some,来对值进行处理
Rust 知道我们没有覆盖所有可能的情况甚至知道哪些模式被忘记了!Rust 中的匹配是 穷举式的 ( exhaustive ):必须穷举到最后的可能性来使代码有效。特别的在这个 Option<T> 的例子中,Rust 防止我们忘记明确的处理 None 的情况,这让我们免于假设拥有一个实际上为空的值,从而使之前提到的价值亿万的错误不可能发生。
4-2-3、通配模式和_占位符
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
最后一个分支则涵盖了所有其他可能的值,模式是我们命名为 other 的一个变量。other 分支的代码通过将其传递给 move_player 函数来使用这个变量。
因为最后一个模式将匹配所有未被特殊列出的值
Rust 还提供了一个模式,当我们不想使用通配模式获取的值时,请使用 _ ,这是一个特殊的模式,可以匹配任意值而不绑定到该值。
我们必须将通配分支放在最后,因为模式是按顺序匹配的。
other 和*区别 *获取不到参数 ,而 other 则是最后一位,能获取到参数
4-2-4、if let
if let 获取通过等号分隔的一个模式和一个表达式。
fn main() {
let mut some_u8_value = 3;
if let 3 = some_u8_value {
println!("three");
} else {
some_u8_value += 1;
println!("some_u8_value{}", some_u8_value)
}
}
使用 if let 意味着编写更少代码,更少的缩进和更少的样板代码。然而,这样会失去 match 强制要求的穷尽性检查。
可以在 if let 中包含一个 else。else 块中的代码与 match 表达式中的 _ 分支块中的代码相同,这样的 match 表达式就等同于 if let 和 else
5、包、create、模块
5-1、包和 create
5-1-1、包
一个包中至多 只能 包含一个库 crate(library crate);包中可以包含任意多个二进制 crate(binary crate);包中至少包含一个 crate,无论是库的还是二进制的
在此,我们有了一个只包含 src/main.rs 的包,意味着它只含有一个名为 my-project 的二进制 crate。如果一个包同时含有 src/main.rs 和 src/lib.rs ,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate:每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。
将一个 crate 的功能保持在其自身的作用域中,可以知晓一些特定的功能是在我们的 crate 中定义的还是在 rand crate 中定义的,这可以防止潜在的冲突。例如,rand crate 提供了一个名为 Rng 的特性(trait)。我们还可以在我们自己的 crate 中定义一个名为 Rng 的 struct。因为一个 crate 的功能是在自身的作用域进行命名的,当我们将 rand 作为一个依赖,编译器不会混淆 Rng 这个名字的指向。在我们的 crate 中,它指向的是我们自己定义的 struct Rng。我们可以通过 rand::Rng 这一方式来访问 rand crate 中的 Rng 特性(trait)。
5-1-2、模块的作用域和私有性
通过执行 cargo new --lib restaurant,来创建一个新的名为 restaurant 的库。然后将示例 7-1 中所罗列出来的代码放入 src/lib.rs 中,来定义一些模块和函数
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
src/main.rs 和 src/lib.rs 被称为 crate 根。如此称呼的原因是,这两个文件中任意一个的内容会构成名为 crate 的模块,且该模块位于 crate 的被称为 模块树 的模块结构的根部
5-1-3、路径用于引用模块树中的项
路径有两种形式:
- 绝对路径 ( absolute path )从 crate 根部开始,以 crate 名或者字面量
crate开头。 - 相对路径 ( relative path )从当前模块开始,以
self、super或当前模块的标识符开头。
包的移动用相对
包内移动用绝对
作用域
Rust 中默认所有项(函数、方法、结构体、枚举、模块和常量)都是私有的。父模块中的项不能使用子模块中的私有项,但是子模块中的项可以使用他们父模块中的项。
pub
可以通过使用 pub 关键字来创建公共项,使子模块的内部部分暴露给上级模块
add_to_waitlist 函数是私有的。私有性规则不但应用于模块,还应用于结构体、枚举、函数和方法
#[cfg(test)]
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
我们还可以使用 pub 来设计公有的结构体和枚举,不过有一些额外的细节需要注意。如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
pub fn eat_at_restaurant() {
// 绝对路径
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat");
//不能修改seasonal_fruit 因为它是私有的
println!("I'd like {} toast please", meal.toast);
}
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
枚举
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
如果枚举成员不是公有的,那么枚举会显得用处不大;给枚举的所有成员挨个添加 pub 是很令人恼火的,因此枚举成员默认就是公有的。结构体通常使用时,不必将它们的字段公有化,因此结构体遵循常规,内容全部是私有的,除非使用 pub 关键字。
super
我们还可以使用 super 开头来构建从父模块开始的相对路径。这么做类似于文件系统中以 .. 开头的语法。
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
}
我们认为 back_of_house 模块和 serve_order 函数之间可能具有某种关联关系,并且,如果我们要重新组织这个 crate 的模块树,需要一起移动它们。因此,我们使用 super,这样一来,如果这些代码被移动到了其他模块,我们只需要更新很少的代码
5-1-4、use
在作用域中增加 use 和路径类似于在文件系统中创建软连接(符号连接,symbolic link)。通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。通过 use 引入作用域的路径也会检查私有性,同其它路径一样
use 和相对路径来将一个项引入作用域
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
//use front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist;
}
其他包引入
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
//使用as关键字来解决
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
如你所见,使用父模块可以区分这两个 Result 类型。如果我们是指定 use std::fmt::Result 和 use std::io::Result,我们将在同一作用域拥有了两个 Result 类型,当我们使用 Result 时,Rust 则不知道我们要用的是哪个。
我们可以使用 as 关键字来提供新名称
重导出
当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pub 和 use。这个技术被称为 “ 重导出 ( re-exporting )”
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
// pub use,我们可以使用一种结构编写代码,却将不同的结构形式暴露出来。这样做使我们的库井井有条,方便开发这个库的开发者和调用这个库的开发者之间组织起来
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。如果没有指定 pub use,eat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但外部代码则不允许使用这个新路径。
使用外部包
use rand::Rng;
use std::collections::HashMap;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..101);
}
注意标准库(std)对于你的包来说也是外部 crate。因为标准库随 Rust 语言一同分发,无需修改 Cargo.toml 来引入 std,不过需要通过 use 将标准库中定义的项引入项目包的作用域中来引用它们,比如我们使用的 HashMap:
嵌套路径
use std::cmp::Ordering;
use std::io;
等同于
use std::{cmp::Ordering, io}; 一样的可以合在一起
glob
use std::collections::*;
这个 use 语句将 std::collections 中定义的所有公有项引入当前作用域。使用 glob 运算符时请多加小心!Glob 会使得我们难以推导作用域中有什么名称和它们是在何处定义的。
5-1-5 分割不同的文件
/lib.rs
mod front_of_house;
需要再同级目录下有这个文件名
/front_of_house.rs
pub mod hosting;
//需要再同名目录下有这个文件名
/front_of_house/hosting
pub fn add_to_waitlist() {}
上述后两步等效
pub mod hosting {
pub fn add_to_waitlist() {}
}
在 mod front_of_house 后使用分号,而不是代码块,这将告诉 Rust 在另一个与模块同名的文件中加载模块的内容。继续重构我们例子,将 hosting 模块也提取到其自己的文件中,仅对 src/front_of_house.rs 包含 hosting 模块的声明进行修改:
lib.rs 是根目录,会直接在 src 下面寻找 ,而不是根目录的包 则需要在同名的文件名下有这个包名
引入包后才能开始使用
crate 可以在子目录中寻找父级
6、常见集合
6-1、vec
是的,Rust 中的 Vec(动态数组)也要求其所有元素必须是同一类型。Vec 是 Rust 标准库中提供的一种动态大小的数组类型,但它仍然遵循 Rust 的类型系统规则,即所有元素必须是同质的。
类似于任何其他的 struct,vector 在其离开作用域时会被释放,如示例 8-4 所标注的
6-1-1、语法
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
Rust 提供了 vec! 宏。这个宏会根据我们提供的值来创建一个新的 Vec
修改值必须使用 mut 关键字使其可变。放入其中的所有值都是 i32 类型的,
注:用此方法超出范围不会报错
match v.get(100) {
Some(x) => println!("{:?}", x),
None => println!("None"),
}
| 方法 | ||
|---|---|---|
| 新增 | v.push | |
| 索引 | &v[index],超出范围报错 | v.get(index),返回一个 Option<&T>,超出范围返回none |
//报错
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
//修正
let mut v = vec![1, 2, 3, 4, 5];
v.push(6);
let first = &v[0];
println!("The first element is: {}", first);
在读取元素后再添加,内存会发生变化,前面的代码不知有它,不认过去,君生我未生;
6-1-2、循环
for i in &mut v {
*i += 50
}
为了修改可变引用所指向的值,在使用 += 运算符之前必须使用解引用运算符(*)获取 i 中的值
6-1-3、枚举结合 vec
enum spreadsheetceel {
Int(i32),
Float(f64),
Text(String),
};
let row = vec![
spreadsheetceel::Int(3),
spreadsheetceel::Text(String::from("blue")),
spreadsheetceel::Float(10.12),
];
用枚举生成 vec,可以存储不同的类型,另外 可以使用 match 来对不同的值进行匹配 确保不会报错
6-2、字符串
6-2-1、初始化
以下两个方法等效
字符串是 UTF-8 编码的,所以可以包含任何正确编码的数据
let s = "initContent".to_string();
let s = String::from("initial contents");
println!("{}", s);
6-2-2、更新字符串
可以通过 push_str 方法来附加字符串 slice,从而使 String 变长,如示例 8-15 所示。
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);
s2 是 slice,我们不能获取其所有权,避免自身所有权失效
push 方法被定义为获取一个单独的字符作为参数,并附加到 String 中
let mut s = String::from("lo");
s.push('l');
拼接,遵循所有权规则
let s1 = String::from("Hello, ");
let s2 = String::from("world");
let res = s1 + &s2;
println!("{},{}", res, s2);
+ 运算符使用了 add 函数,这个函数签名看起来像这样:
self 的所有权会被获取
fn add(self, s: &str) -> String {
s2 使用了 &,意味着我们使用第二个字符串的 引用 与第一个字符串相加。这是因为 add 函数的 s 参数:只能将 &str 和 String 相加,不能将两个 String 值相加。
&String 可以被 强转 ( coerced )成 &str。当 add 函数被调用时,Rust 使用了一个被称为 解引用强制转换 ( deref coercion )的技术,你可以将其理解为它把 &s2 变成了 &s2[..]
6-2-3、 format! 宏
format! 与 println! 的工作原理相同,不过不同于将输出打印到屏幕上,它返回一个带有结果内容的 String。这个版本就好理解的多,并且不会获取任何参数的所有权。
let s1 = String::from("tic");
let s2 = String::from("tic");
let s3 = String::from("tic");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s);
6-2-4、索引字符串
Rust 不允许使用索引获取 String 字符的原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String 不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
比如一些非英文字符,在字节码中不能确保是第一位
我们可以用 slice 来解决
let hello = "Здравствуйте";
let s = &hello[0..4];
println!("{}", s)
这里,s 会是一个 &str,它包含字符串的头 4 个字节。早些时候,我们提到了这些字母都是 2 个字节长的,所以这意味着 s 将会是 “Зд”。
6-2-5、遍历字符串
如果你需要操作单独的 Unicode 标量值,最好的选择是使用 chars 方法。对 “नमस्ते” 调用 chars 方法会将其分开并返回六个 char 类型的值,接着就可以遍历其结果来访问每一个元素了:
for c in hello.chars() {
println!("{}", c);
}
bytes 方法返回每一个原始字节,这可能会适合你的使用场景
for b in "नमस्ते".bytes() {
println!("{}", b);
}
6-3、hash-map
像 vector 一样,哈希 map 将它们的数据储存在堆上,这个 HashMap 的键类型是 String 而值类型是 i32。类似于 vector,哈希 map 是同质的:所有的键必须是相同类型,值也必须都是相同类型。
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
6-3-1、元组构建
是使用一个元组的 vector 的 collect 方法,其中每个元组包含一个键值对。collect 方法可以将数据收集进一系列的集合类型,包括 HashMap。例如,如果队伍的名字和初始分数分别在两个 vector 中,可以使用 zip 方法来创建一个元组的 vector,其中 “Blue” 与 10 是一对,依此类推。接着就可以使用 collect 方法将这个元组 vector 转换成一个 HashMap,如示例 8-21 所示:
必须规定类型
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let score: HashMap<_, _> = teams.iter().zip(initial_scores).collect();
println!("{:?}", score);
在类型中规定借用即可
let a = [1, 2, 3];
let b = [4, 5, 6];
let zipped: Vec<(&i32, &i32)> = a.iter().zip(b.iter()).collect();
这里 HashMap<_, _> 类型标注是必要的,因为 collect 有可能当成多种不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap 所包含的类型。
6-3-2、hashma 和所有权
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效,
// 尝试使用它们看看会出现什么编译错误!
当 insert 调用将 field_name 和 field_value 移动到哈希 map 中后,将不能使用这两个绑定。
6-3-3、访问 hashmap 中的值
通过 get 访问 hashmap 的值,注意所有权及借用
get 返回 Option<V>,所以结果被装进 Some如果没有对应值,会返回 None
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(&field_name, &field_value);
let val = map.get(&field_name);
println!("{:?}", val);
使用与 vector 类似的方式来遍历哈希 map 中的每一个键值对,也就是 for 循环:
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
6-3-4、键入
覆盖值
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
没有对应值
哈希 map 有一个特有的 API,叫做 entry,它获取我们想要检查的键作为参数。entry 函数的返回值是一个枚举,Entry
let mut map: HashMap<&String, &String> = HashMap::new();
map.insert(&field_name, &field_value);
let val = map.get(&field_name);
println!("{:?}", val);
let str: String = String::from("haha");
let name: String = String::from("haha");
map.entry(&field_name).or_insert(&str);
let enter_v = map.entry(&name).or_insert(&str);
println!("{:?}", enter_v);
println!("{:?}", map);
map 和 get 不能一起,map.entry(name.clone()) 对 map 进行了可变借用,而 map.get(&name) 对 map 进行了不可变借用。
6-3-5、根据旧值更新
or_insert 方法事实上会返回这个键的值的一个可变引用(&mut V),所以为了赋值必须首先使用星号(*)解引用 count
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
7、处理错误
7-1、Result 与可恢复的错误
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
7-1-1、panic 简写:unwrap 和 expect
unwrap,它的实现就类似于示例 9-4 中的 match 语句。如果 Result 值是成员 Ok,unwrap 会返回 Ok 中的值。如果 Result 是成员 Err,unwrap 会为我们调用 panic!。这里是一个实践 unwrap 的例子:
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
使用 expect 而不是 unwrap 并提供一个好的错误信息可以表明你的意图并更易于追踪 panic 的根源。expect 的语法看起来像这样
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
expect 在调用 panic! 时使用的错误信息将是我们传递给 expect 的参数,而不像 unwrap 那样使用默认的 panic! 信息,expect 是自定义的,帮助我们更容易找到错误信息
7-1-2、错误传播简写 ?UI 运算符
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
? 运算符消除了大量样板代码并使得函数的实现更简单。我们甚至可以在 ? 之后直接使用链式方法调用来进一步缩短代码,如示例 9-8 所示:
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
7-1-3、main 的?处理
main 函数是特殊的,其必须返回什么类型是有限制的。main 函数的一个有效的返回值是 (),同时出于方便,另一个有效的返回值是 Result<T, E>,如下所示:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
main 函数是特殊的,其必须返回什么类型是有限制的。main 函数的一个有效的返回值是 (),同时出于方便,另一个有效的返回值是 Result<T, E>
7-1-4、可以帮助函数中断
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
8、泛型、trait、和生命周期
8-1、泛型
8-1-1、函数泛型
fn largest<T: PartialOrd + Copy>(list: &Vec<T>) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
涉及到 trait
8-1-2、结构体和枚举
struct PointA<T> {
x: T,
y: T,
}
//两个类型一致
struct Point<T, U> {
x: T,
y: U,
}
//可以不一致
enum Result<T, E> {
Ok(T),
Err(E),
}
8-1-3、方法
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
必须在 impl 后面声明 T,这样就可以在 Point<T> 上实现的方法中使用它了。在 impl 之后声明泛型 T ,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
可以用类型推断,多个类型会根据方法参数的类型进行推断
8-2、trait
trait 类似于其他语言中常被称为 接口 ( interfaces )的功能,虽然有一些不同。
8-2-1、定义 trait
如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合
pub mod _trait {
pub trait Summary {
fn summarize(&self) -> String;
}
}
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
8-2-2、结构体
mod _trait;
pub use _trait::_trait::Summary;
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
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,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性 ( coherence ) 的程序属性的一部分,或者更具体的说是 孤儿规则 ( orphan rule ),其得名于不存在父类型。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。
不能修改其他的 trait
8-2-3、默认实现
pub mod _trait {
pub trait Summary {
fn summarize(&self) -> String;
fn default_summarize(&self) -> String {
String::from("Read more ...")
}
}
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{},by {} ({})", self.headline, self.author, self.author)
}
fn default_summarize(&self) -> String {
format!("默认实现取消")
}
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}:{}", self.username, self.content)
}
}
该 impl 如果重新定义默认实现的函数 则优先调用定义后的值,
无法从相同方法的重载实现中调用默认方法
8-2-4、可作为参数
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
8-2-5、trait Bound 语法
可以通过作为泛型的值来进行简写
<T:trait>
pub fn notify(item: &impl Summary, item2: &impl Summary) {
// println!("{:?}", item)
}
pub fn notify2<T: Summary>(item: &T, item2: &T) {
// println!("{:?}", item)
}
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.",
),
};
notify(&article, &article);
notify2(&article, &article);
// notify();
println!("Hello, world!");
}
8-2-6、通过+指定多个 bound
两个 debug 都需要
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct NewsArticle {
.....
}
pub fn notify<T: Summary + Clone + Debug>(item: &T, item2: &T) {
let res = format!("{}", item.clone().summarize());
let res2 = item2.clone();
println!("{},{:#?}", res, res2)
}
简写
pub fn notify3<T, U>(item: &T, item2: &U)
where
T: Summary,
U: Summary + Debug + Clone,
{
let res = format!("{}", item.summarize());
let res2 = item2.clone();
println!("{},{:#?}", res, res2)
}
8-2-7、retrun 定义 trait
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,
}
}
&dyn Summary表示一个实现了Summarytrait 的动态类型引用。impl Summary表示一个实现了Summarytrait 的具体类型
returns_summarizable(&mut article);
fn returns_summarizable(_item: &mut NewsArticle) -> &dyn Summary {
_item
}
只适用于返回单一类型的情况
8-2-7、可选 impl
只有满足打印 Display,且比较 PartialOrd 此函数才会生效
impl<T: Display + PartialOrd> Pair<T> {
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);
}
}
}
标准库
impl<T: Display> ToString for T {
// --snip--
}
我们可以对任何实现了 Display trait 的类型调用由 ToString 定义的 to_string 方法,可以将整型转换为对应的 String 值,因为整型实现了 Display
8-3、生命周期
主要用来处理引用数据类型发生的错误
8-3-1、函数中的泛型生命周期
生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。'a 是大多数人默认使用的名称。生命周期参数标注位于引用的 & 之后,并有一个空格来将引用类型与生命周期标注分隔开。
'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
let contents = fs::read_to_string(filename).expect("Error reading file");
println!("With text:\n{}", contents);
parse_config(query, filename);
}
fn parse_config<'a>(query: &'a str, filename: &'a str) -> (&'a str, &'a str) {
// let query = &args[1];
// let filename = &args[2];
(query, filename)
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
成功
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
失败
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
该函数的生命周期与较短的保持一致,说明在 string2 生命周期结束时,rusult 的生命周期就结束了
报错,如果直接返回 String 不会报错 ,as_str 返回的是一个&str 类型 是一个引用,没有所有权
fn longest2() -> &str {
let result = String::from("really long string");
let res = result.as_str();
res
}
最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
8-3-2、结构体定义的生命周期
这是一个引用值,我们可以在 struct 中定义它的生命周期,novel 在上面 他的生命周期比 import 的生命周期更长,当 import 退出的时候 novel 都不会退出 所以 他的引用值是有效的
struct ImportantExcerpt<'a> {
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 '.'");
let i = ImportantExcerpt { part: first_sentence };
}
在 Rust 中,如果你有一个结构体包含引用类型的字段,你必须为这些引用字段指定生命周期参数。这是因为 Rust 需要确保这些引用的生命周期是有效的,并且不会在结构体实例的生命周期内变得无效。
生命周期如何添加,应该注意其返回值又没有对其是否进行操作
8-3-3、省略生命周期
生命周期省略规则
第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。
第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32。
第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,说明是个对象的方法(method)(译者注: 这里涉及 Rust 的面向对象,参见第 17 章), 那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
8-3-4、方法的生命周期
impl 之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 self 引用的生命周期。
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
这里是一个适用于第三条生命周期省略规则的例子:
因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
8-3-5、静态生命周期
这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面量都拥有 'static 生命周期,我们也可以选择像下面这样标注出来:
let s: &'static str = "I have a static lifetime.";
因为生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中
8-3-6、生命周期与 trait
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
}
}
这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。
如果不加 Display,无法确定泛型类型,也就无法判断能否打印
总结 trait 帮其规定为泛型时候的行为
9、测试
9-1、编写测试
9-1-1、#[test]
#[test]:这个属性表明这是一个测试函数
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
函数体通过使用 assert_eq! 宏来断言 2 加 2 等于 4。一个典型的测试的格式,就是像这个例子中的断言一样。接下来运行就可以看到测试通过。
9-1-2、assert!
assert! 宏由标准库提供,在希望确保测试中一些条件为 true 时非常有用。需要向 assert! 宏提供一个求值为布尔值的参数
#[cfg(test)]
mod tests {
use 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));
}
}
注意在 tests 模块中新增加了一行:use super::*;。tests 是一个普通的模块,它遵循第 7 章 “路径用于引用模块树中的项” 部分介绍的可见性规则。因为这是一个内部模块,要测试外部模块中的代码,需要将其引入到内部模块的作用域中。这里选择使用 glob 全局导入,以便在 tests 模块中使用所有在外部模块定义的内容。
让我们为测试函数增加一个自定义失败信息参数:带占位符的格式字符串,以及 greeting 函数的值:
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`", result
);
}
9-1-2、assert_eq! 和 assert_ne!
这两个宏分别比较两个值是相等还是不相等。当断言失败时他们也会打印出这两个值具体是什么,以便于观察测试 为什么 失败
fn exploration() {
assert_eq!(2 + 2, 4);
}
assert_ne! 宏在传递给它的两个值不相等时通过,而在相等时失败。在代码按预期运行,我们不确定值 会 是什么,不过能确定值绝对 不会 是什么的时候,这个宏最有用处。
9-1-3、#[should_panic]
可以通过对函数增加另一个属性 should_panic 来实现这些。这个属性在函数中的代码 panic 时会通过,而在其中的代码没有 panic 时失败。
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
我们可以给 should_panic 属性增加一个可选的 expected 参数。测试工具会确保错误信息中包含其提供的文本
#[should_panic(expected = "Guess value must be less than or equal to 100")]
9-1-4 Result
不能在使用 Result<T, E> 的测试中使用 #[should_panic] 注解。要断言操作返回 Err 值,不要在 Result<T, E> 值上使用问号运算符。相反,请使用 assert!(value.is_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"))
}
}
}
9-2、运行测试
$ cargo test -- --test-threads=1
这里将测试线程设置为 1,告诉程序不要使用任何并行机制。这也会比并行运行花费更多时间,不过在有共享的状态时,测试就不会潜在的相互干扰了。
$ cargo test -- --show-output
如果你希望也能看到通过的测试中打印的值,可以通过在末尾增加 --show-output 参数来告知 Rust 显示通过测试的输出
cargo test #[test]函数名称
可以向 cargo test 传递任意测试的名称来只运行这个测试,这个也可以过滤 名称中携带这个字段的都会进行测试
cargo test -- --ignored
可以使用 ignore 属性来标记耗时的测试并排除他们
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 需要运行一个小时的代码
}
9-3、测试结构
9-3-1、单元测试
测试模块的 #[cfg(test)] 标注告诉 Rust 只在执行 cargo test 时才编译和运行测试代码,而在运行 cargo build 时不这么做。
你需要使用 #[cfg(test)] 来指定他们不应该被包含进编译结果中
rust 可以测试私有函数
pub fn add_two(a: i32) -> i32 {
internal_adder(a, 2)
}
fn internal_adder(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
assert_eq!(4, internal_adder(2, 2));
}
}
9-3-2、集成测试
为了编写集成测试,需要在项目根目录创建一个 tests 目录,与 src 同级。Cargo 知道如何去寻找这个目录中的集成测试文件。接着可以随意在这个目录中创建任意多的测试文件,Cargo 会将每一个文件当作单独的 crate 来编译。
集成测试对于你需要测试的库来说完全是外部的。同其他使用库的代码一样使用库文件,也就是说它们只能调用一部分库中的公有 API 。集成测试的目的是测试库的多个部分能否一起正常工作。一些单独能正确运行的代码单元集成在一起也可能会出现问题,所以集成测试的覆盖率也是很重要的。
use adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
如果我们可以创建 一个tests/common.rs 文件并创建一个名叫 setup 的函数,我们希望这个函数能被多个测试文件的测试函数调用
pub fn setup() {
// 编写特定库测试所需的代码
}
为了不让 common 出现在测试输出中,我们将创建 tests/common/mod.rs ,而不是创建 tests/common.rs 。这是一种 Rust 的命名规范,这样命名告诉 Rust 不要将 common 看作一个集成测试文件。将 setup 函数代码移动到 tests/common/mod.rs 并删除 tests/common.rs 文件之后,测试输出中将不会出现这一部分。tests 目录中的子目录不会被作为单独的 crate 编译或作为一个测试结果部分出现在测试输出中。
一旦拥有了 tests/common/mod.rs ,就可以将其作为模块以便在任何集成测试文件中使用。这里是一个 tests/integration_test.rs 中调用 setup 函数的 it_adds_two 测试的例子:
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}