10、闭包与迭代器
10-1、闭包
10-1-1、使用闭包存储结果
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
println!("today,do{}pushups!", res.value(4));
println!("Next, do {} situps!", res.value(3));
闭包不要求像 fn 函数那样在参数和返回值上注明类型。函数中需要类型标注是因为他们是暴露给用户的显式接口的一部分。严格的定义这些接口对于保证所有人都认同函数使用和返回值的类型来说是很重要的。但是闭包并不用于这样暴露在外的接口:他们储存在变量中并被使用,不用命名他们或暴露给库的用户调用。
闭包通常很短,并只关联于小范围的上下文而非任意情境。
如果尝试调用闭包两次,第一次使用 String 类型作为参数而第二次使用 u32,则会得到一个错误:
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
10-1-2、cacher单例模式
impl<T> Cacher<T>
where T: Fn(u32) -> u32
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
},
}
}
}
10-1-3、闭包与环境
闭包从环境中捕获一个值,闭包会在闭包体中储存这个值以供使用
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));
}
以下不能编译,因为作用域的生命周期中没有x
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
闭包可以通过三种方式捕获其环境,他们直接对应函数的三种获取参数的方式:获取所有权,可变借用和不可变借用。这三种捕获值的方式被编码为如下三个 Fn trait:
FnOnce消费从周围作用域捕获的变量,闭包周围的作用域被称为其 环境,environment。为了消费捕获到的变量,闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的Once部分代表了闭包不能多次获取相同变量的所有权的事实,所以它只能被调用一次。FnMut获取可变的借用值所以可以改变其环境Fn从其环境获取不可变的借用值
Rust 根据其如何使用环境中变量来推断我们希望如何引用环境。由于所有闭包都可以被调用至少一次,所以所有闭包都实现了 FnOnce 。那些并没有移动被捕获变量的所有权到闭包内的闭包也实现了 FnMut ,而不需要对被捕获的变量进行可变访问的闭包则也实现了 Fn
10-2、迭代器
10-2-1、Iterator trait 和 next 方法
trait 是类型规范 impl trait for struct 可以理解成extends trait的类型
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// 此处省略了方法的默认实现
}
next 是 Iterator 实现者被要求定义的唯一方法。next 一次返回迭代器中的一个项,封装在 Some 中,当迭代器结束时,它返回 None。
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
这些调用 next 方法的方法被称为 消费适配器(consuming adaptors),因为调用他们会消费迭代器。一个消费适配器的例子是 sum 方法。这个方法获取迭代器的所有权并反复调用 next 来遍历迭代器,因而会消费迭代器
10-2-2、迭代器方法
Iterator trait 中定义了另一类方法,被称为 迭代器适配器(iterator adaptors),他们允许我们将当前迭代器变为不同类型的迭代器。可以链式调用多个迭代器适配器。不过因为所有的迭代器都是惰性的,必须调用一个消费适配器方法以便获取迭代器适配器调用的结果。
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
collect() 可以将任何可迭代的东西变成一个相关的集合。 这是在各种上下文中使用的标准库中功能更强大的方法之一。
10-2-3、闭包获取环境
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter()
.filter(|s| s.size == shoe_size)
.collect()
}
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe { size: 10, style: String::from("sneaker") },
Shoe { size: 13, style: String::from("sandal") },
Shoe { size: 10, style: String::from("boot") },
];
let in_my_size = shoes_in_my_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe { size: 10, style: String::from("sneaker") },
Shoe { size: 10, style: String::from("boot") },
]
);
}
如果是函数的话,需要规定其生命周期,shoe_size的生命周期和筛选的生命周期不一致
10-2-4、自定义迭代器
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
11、智能指针
11-1、Box
box 允许你将一个值放在堆上而不是栈上。留在栈上的则是指向堆数据的指针。
使用场景
- 当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候
- 当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候
- 当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候
11-1-1、Box存储
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
其值是一个指向被分配在堆上的值 5 的 Box。这个程序会打印出 b = 5;
11-1-2、Box递归类型
enum List{
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
}
因为 Box<T> 是一个指针,我们总是知道它需要多少空间:指针的大小并不会根据其指向的数据量而改变。
现在实现这个概念的方式更像是一个项挨着另一项,而不是一项包含另一项。
11-2、Deref智能指针
实现 Deref trait 允许我们重载 解引用运算符
11-2-1、解引用
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较
11-2-2、deref的trait
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
没有 Deref trait 的话,编译器只会把 & 引用类型解引用。deref 方法向编译器提供了一种能力:能够获取任何实现了 Deref trait 的类型的值,并且可以通过调用这个类型的 deref 方法来获取一个解引用方法已知的 & 引用。
11-2-3、解引用转换规则
类似于使用 Deref trait 重载不可变引用的 * 运算符,Rust 提供了 DerefMut trait 用于重载可变引用的 * 运算符。
Rust 在发现类型和 trait 的实现满足以下三种情况时会进行解引用强制转换:
- 当
T: Deref<Target=U>:从&T到&U。 - 当
T: DerefMut<Target=U>:从&mut T到&mut U。 - 当
T: Deref<Target=U>:从&mut T到&U。
前两种情况除了可变性之外是相同的:第一种情况表明如果有一个 &T,而 T 实现了返回 U 类型的 Deref,则可以直接得到 &U。第二种情况表明对于可变引用也有着相同的行为。
第三种情况有些微妙:Rust 也会将可变引用强转为不可变引用,但是反之是 不可能 的,因为不可变引用永远也不能强转为可变引用。因为根据借用规则,如果有一个可变引用,其必须是这些数据的唯一引用(否则程序将无法编译)。将一个可变引用转换为不可变引用永远也不会打破借用规则。将不可变引用转换为可变引用则需要数据只能有一个不可变引用,而借用规则无法保证这一点。因此,Rust 无法假设将不可变引用转换为可变引用是可能的。
11-2-4、解引用转换
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
&因为切片需要显示书写
myBox实现了Deref的trait,这个里面对Box进行解引用,解引用得到String,可以对string进行操作,如果没有这个智能指针,则接出来的是MyBox
11-3、Drop
我们在智能指针上下文中讨论 Drop 是因为其功能几乎总是用于实现智能指针。例如,Box<T> 自定义了 Drop 用来释放 box 所指向的堆空间。
11-3-1、std::mem::drop
use std::mem::drop
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
11-4、Rc
Rc<T> 的类型。其名称为 引用计数
注意 Rc<T> 只能用于单线程场景;
11-4-1、Rc共享数据
每次调用 Rc::clone,Rc<List> 中数据的引用计数都会增加,直到有零个引用之前其数据都不会被清理。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
在程序中每个引用计数变化的点,会打印出引用计数,其值可以通过调用 Rc::strong_count 函数获得。
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
Rc<T> 允许在程序的多个部分之间只读地共享数据,Drop trait 的实现当 Rc<T> 值离开作用域时自动减少引用计数。
11-5、RefCell
11-5-1、RefCell和内部可变模式
RefCell<T> 代表其数据的唯一的所有权,
类似于 Rc<T>,RefCell<T> 只能用于单线程场景。如果尝试在多线程上下文中使用RefCell<T>,会得到一个编译错误。第 16 章会介绍如何在多线程程序中使用 RefCell<T> 的功能。
如下为选择 Box<T>,Rc<T> 或 RefCell<T> 的理由:
Rc<T>允许相同数据有多个所有者;Box<T>和RefCell<T>有单一所有者。Box<T>允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T>允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>自身是不可变的情况下修改其内部的值。
11-5-2、refcell用于进行可变借用
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
// sent_messages: Vec<String>,//2、因为&self并不是可变
//3、进行refcell借用
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
//1、self只是借用无法进行可变 self.sent_messages.push(String::from(message));
//4、可以使用borrow_mut进行修改
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
borrow 方法返回 Ref<T> 类型的智能指针,borrow_mut 方法返回 RefMut<T> 类型的智能指针。这两个类型都实现了 Deref。
11-5-3、*Rc和RefCell可以拥有多个可变数据所有者
Rc是不允许可变的,它可以让数据有多个指针,可以进行共享,,理解为一个电视机多个人看
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
//此处 对所有使用value的值进行处理 对所有的value值+10,a是value的值
*value.borrow_mut() += 10;
println!("a after = {:?}", a);15,nil
println!("b after = {:?}", b);6,15
println!("c after = {:?}", c);10,15
}
RefCell用于此处是用来进行可变处理的,
11-6、循环引用
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
//将其Cons中的第二位RefCell的值就是a的RefCell::new(Rc::new(Nil)) 但b里面又有a所以循环引用了
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
避免循环引用
11-6、weak
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
//调用 Rc::downgrade 时会得到 Weak<T> 类型的智能指针
//强引用代表如何共享 Rc<T> 实例的所有权,但弱引用并不属于所有权关系。他们不会造成引用循环,因为任何弱引用的循环会在其相关的强引用计数为 0 时被打断。
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
结果
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
Rc<Node> 的强引用计数减少为 0,所以其 Node 被丢弃。来自 leaf.parent 的弱引用计数 1 与 Node 是否被丢弃无关,所以并没有产生任何内存泄漏!
12、并发
12-1、线程
- 竞争状态(Race conditions),多个线程以不一致的顺序访问数据或资源
- 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
- 只会发生在特定情况且难以稳定重现和修复的 bug
12-1-1、spawn新线程
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
thread::sleep 调用强制线程停止执行一小段时间,这会允许其他不同的线程运行。这些线程可能会轮流运行,不过并不保证如此:这依赖操作系统如何调度线程
12-1-2、join等待所有线程结束
线程是异步操作,阻塞(Blocking) 线程意味着阻止该线程执行工作或退出
use std::thread;
use std::time::Duration;
fn main() {
println!("hello world1");
let handle = thread::spawn(|| {
println!("hello world2");
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
println!("hello world3");
handle.join().unwrap();
println!("hello world4");
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
hello world1
hello world3
hello world2
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hello world4
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
12-1-3、move转移所有权
通过move捕获环境中的所有权并且实现转移,不能在主线程中使用drop清理代码,所有权转移到spawn中了
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
12-2、通讯
12-2-1、mpsc::channel
mpsc::channel 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,tx 和 rx 通常作为 发送者(transmitter)和 接收者(receiver)的缩写
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
send 函数获取其参数的所有权并移动这个值归接收者所有
12-2-2、发送多个值
use std::thread;
use std::sync::mpsc;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
}
将 rx 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束
12-2-3、通过克隆创建多个发送者
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {}", received);
}
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
这一次,在创建新线程之前,我们对通道的发送端调用了 clone 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向通道的接收端发送不同的消息。
12-3、共享状态并发
12-3-1、互斥器
互斥器(mutex)是 mutual exclusion 的缩写,也就是说,任意时刻,其只允许一个线程访问某些数据。为了访问互斥器中的数据,线程首先需要通过获取互斥器的 锁(lock)来表明其希望访问数据。锁是一个作为互斥器一部分的数据结构,它记录谁有数据的排他访问权。因此,我们描述互斥器为通过锁系统 保护(guarding)其数据。
- 在使用数据之前尝试获取锁。
- 处理完被互斥器所保护的数据之后,必须解锁数据,这样其他线程才能够获取锁。
12-3-2、Mutex的api
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
//离开此作用域时,lock锁自动释放
}
println!("m = {:?}", m);
}
Mutex<T> 是一个智能指针。更准确的说,lock 调用 返回 一个叫做 MutexGuard 的智能指针。这个智能指针实现了 Deref 来指向其内部数据;其也提供了一个 Drop 实现当 MutexGuard 离开作用域时自动释放锁,这正发生于示例 16-12 内部作用域的结尾。为此,我们不会冒忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。
12-3-3、共享Mutex
所幸 Arc<T> 正是 这么一个类似 Rc<T> 并可以安全的用于并发环境的类型。字母 “a” 代表 原子性(atomic),所以这是一个原子引用计数(atomically reference counted)类型。原子性是另一类这里还未涉及到的并发原语:请查看标准库中 std::sync::atomic 的文档来获取更多细节。其中的要点就是:原子性类型工作起来类似原始类型,不过可以安全的在线程间共享。
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
//问题,多个线程中move会多次获取counter的所有权我们该怎么解决
//方案一 使用Rc获取多个所有权,但每次使用需要使用Rc.clone()对其进行计数,无法实现,此方案不可行
//方案二 使用Arc进行原子引用计数
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
12-3-4、RefCell/Rc与Mutex/Arc的相似性
使用 RefCell<T> 可以改变 Rc<T> 中的内容那样,同样的可以使用 Mutex<T> 来改变 Arc<T> 中的内容
Mutex<T> 提供了内部可变性
这时两个 Rc<T> 值相互引用,造成内存泄漏。同理,Mutex<T> 也有造成 死锁(deadlock) 的风险。这发生于当一个操作需要锁住两个资源而两个线程各持一个锁,这会造成它们永远相互等待。如果你对这个主题感兴趣,尝试编写一个带有死锁的 Rust 程序,接着研究任何其他语言中使用互斥器的死锁规避策略并尝试在 Rust 中实现他们。标准库中 Mutex<T> 和 MutexGuard 的 API 文档会提供有用的信息。
12-4、使用Sync与Send的Trait
Send标记 trait 表明类型的所有权可以在线程间传递Sync标记 trait 表明一个实现了Sync的类型可以安全的在多个线程中拥有其值的引用。- 通常并不需要手动实现
Send和Synctrait,因为由Send和Sync的类型组成的类型,自动就是Send和Sync的。手动实现是不安全的
13、Rust的面向对象
13-1、特点
另一个通常与面向对象编程相关的方面是 封装(encapsulation)的思想:对象的实现细节不能被使用对象的代码获取到。所以唯一与对象交互的方式是通过对象提供的公有 API;使用对象的代码无法深入到对象内部并直接改变数据或者行为。封装使得改变和重构对象的内部时无需改变使用对象的代码。
继承(Inheritance)是一个很多编程语言都提供的机制,一个对象可以定义为继承另一个对象的定义,这使其可以获得父对象的数据和行为,而无需重新定义。
Rust 代码可以使用默认 trait 方法实现来进行共享
表现为子类型可以用于父类型被使用的地方。这也被称为 多态(polymorphism),这意味着如果多种对象共享特定的属性,则可以相互替代使用。
13-2、trait
13-2-1、trait的实现
我们通过指定某种指针来创建 trait 对象,例如 & 引用或 Box<T> 智能指针,还有 dyn keyword
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
//泛型类型参数一次只能替代一个具体类型,而 trait 对象则允许在运行时替代多种具体类型。
//使用bound
//我在库里调用他只会认出一种类型参数传进去 需要多种类型传入的情况无法使用
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
use gui::{Screen, Button};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
13-2-2、trait要求对象安全
- 返回值类型不为
Self - 方法没有任何泛型类型参数
pub struct Screen {
pub components: Vec<Box<dyn Clone>>,
}
例如,如果尝试实现示例 17-4 中的 Screen 结构体来存放实现了 Clone trait 而不是 Draw trait 的类型
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
--> src/lib.rs:2:5
|
2 | pub components: Vec<Box<dyn Clone>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone`
cannot be made into an object
|
= note: the trait cannot require that `Self : Sized`
14、模式和匹配
14-1、模式
14-1-1、match
match 表达式必须是 穷尽(exhaustive)的,意为 match 表达式所有可能的值都必须被考虑到。
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
14-1-2、if let条件表达式
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {}, as the background", color);
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
// 打印此代码
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
if let 表达式的缺点在于其穷尽性没有为编译器所检查,而 match 表达式则检查了。如果去掉最后的 else 块而遗漏处理一些情况,编译器也不会警告这类可能的逻辑错误。
14-1-3、while let 条件循环
一个与 if let 结构类似的是 while let 条件循环,它允许只要模式匹配就一直进行 while 循环
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
这个例子会打印出 3、2 接着是 1。pop 方法取出 vector 的最后一个元素并返回 Some(value)。如果 vector 是空的,它返回 None。while 循环只要 pop 返回 Some 就会一直运行其块中的代码。一旦其返回 None,while 循环停止。我们可以使用 while let 来弹出栈中的每一个元素。
14-1-4、for循环
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
这里使用 enumerate 方法适配一个迭代器来产生一个值和其在迭代器中的索引,他们位于一个元组中。第一个 enumerate 调用会产生元组 (0, 'a')。当这个值匹配模式 (index, value),index 将会是 0 而 value 将会是 'a',并打印出第一行输出。
14-1-5、let语句
let PATTERN = EXPRESSION;
let本身就是一种模式匹配,进行解构的同时更为明显
let (x, y, z) = (1, 2, 3);
这里将一个元组与模式匹配。Rust 会比较值 (1, 2, 3) 与模式 (x, y, z) 并发现此值匹配这个模式。在这个例子中,将会把 1 绑定到 x,2 绑定到 y 并将 3 绑定到 z。你可以将这个元组模式看作是将三个独立的变量模式结合在一起。
14-1-6、函数参数
函数参数也是一个模式匹配
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
这会打印出 Current location: (3, 5)。值 &(3, 5) 会匹配模式 &(x, y),如此 x 得到了值 3,而 y得到了值 5。
14-2、refutability可反驳性
irrefutable(不可反驳的):一个例子就是 let x = 5; 语句中的 x,因为 x 可以匹配任何值所以不可能会失败。
refutable(可反驳的):能匹配任何传递的可能值的模式被称为是 不可反驳的(irrefutable)。一个这样的例子便是 if let Some(x) = a_value 表达式中的 Some(x);如果变量 a_value 中的值是 None 而不是 Some,那么 Some(x) 模式不能匹配。
函数参数、 let 语句和 for 循环只能接受不可反驳的模式,因为通过不匹配的值程序无法进行有意义的工作。if let 和 while let 表达式被限制为只能接受可反驳的模式,因为根据定义他们意在处理可能的失败:条件表达式的功能就是根据成功或失败执行不同的操作。
let Some(x) = some_option_value;
//修正
if let Some(x) = some_option_value {
println!("{}", x);
}
若 some_option_value 的值是 None,则不会成功匹配模式 Some(x),表明这个模式是可反驳的。然而 let 语句只能接受不可反驳模式,因为代码不能通过 None 值进行有效的操作。Rust 会在编译时抱怨我们尝试在要求不可反驳模式的地方使用可反驳模式:
error[E0005]: refutable pattern in local binding: `None` not covered
-->
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
我们给了代码一个得以继续的出路!这段代码完全有效,尽管这意味着我们不能在避免产生警告的情况下使用无可辩驳的模式。如果为 if let 提供了一个总是会匹配的模式,比如示例 18-10 中的 x,编译器会给出一个警告。
if let x = 5 {
println!("{}", x);
};
warning: irrefutable if-let pattern
--> <anon>:2:5
|
2 | / if let x = 5 {
3 | | println!("{}", x);
4 | | };
| |_^
|
= note: #[warn(irrefutable_let_patterns)] on by default
基于此,match 匹配分支必须使用可反驳模式,除了最后一个分支需要使用能匹配任何剩余值的不可反驳模式。Rust 允许我们在只有一个匹配分支的 match 中使用不可反驳模式,不过这么做不是特别有用,并可以被更简单的 let 语句替代。
14-3、模式语法
14-3-1、匹配命名变量
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {:?}", y),//5
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {:?}", x, y);//Some(5),10
}
让我们看看当 match 语句运行的时候发生了什么。第一个匹配分支的模式并不匹配 x 中定义的值,所以代码继续执行。
第二个匹配分支中的模式引入了一个新变量 y,它会匹配任何 Some 中的值。因为我们在 match 表达式的新作用域中,这是一个新变量,而不是开头声明为值 10 的那个 y。这个新的 y 绑定会匹配任何 Some 中的值,在这里是 x 中的值。因此这个 y 绑定了 x 中 Some 内部的值。这个值是 5,所以这个分支的表达式将会执行并打印出 Matched, y = 5。
如果 x 的值是 None 而不是 Some(5),头两个分支的模式不会匹配,所以会匹配下划线。这个分支的模式中没有引入变量 x,所以此时表达式中的 x 会是外部没有被覆盖的 x。在这个假想的例子中,match 将会打印 Default case, x = None。
一旦 match 表达式执行完毕,其作用域也就结束了,同理内部 y 的作用域也结束了。最后的 println! 会打印 at the end: x = Some(5), y = 10。
14-3-2、多个模式
在 match 表达式中,可以使用 | 语法匹配多个模式,它代表 或(or)的意思。例如,如下代码将 x 的值与匹配分支相比较,第一个分支有 或 选项,意味着如果 x 的值匹配此分支的任一个值,它就会运行:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
14-3-3、通过..=匹配值的范围
如果 x 是 1、2、3、4 或 5,第一个分支就会匹配。这相比使用 | 运算符表达相同的意思更为方便;相比 1..=5,使用 | 则不得不指定 1 | 2 | 3 | 4 | 5。相反指定范围就简短的多,特别是在希望匹配比如从 1 到 1000 的数字的时候!
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
如下是一个使用 char 类型值范围的例子:
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
14-3-4、解构并分解值
也可以使用模式来解构结构体、枚举、元组和引用,以便使用这些值的不同部分。让我们来分别看一看。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
这段代码创建了变量 a 和 b 来匹配结构体 p 中的 x 和 y 字段。
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {}", x),
Point { x: 0, y } => println!("On the y axis at {}", y),
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
}
}
//实际上是模式进行匹配,匹配成功的才会拿到y,拿不到x,x他不是为止值解构
第一个分支通过指定字段 y 匹配字面量 0 来匹配任何位于 x 轴上的点。此模式仍然创建了变量 x 以便在分支的代码中使用。
类似的,第二个分支通过指定字段 x 匹配字面量 0 来匹配任何位于 y 轴上的点,并为字段 y 创建了变量 y。第三个分支没有指定任何字面量,所以其会匹配任何其他的 Point 并为 x 和 y 两个字段创建变量。
在这个例子中,值 p 因为其 x 包含 0 而匹配第二个分支,因此会打印出 On the y axis at 7。
14-3-5、解构枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.")
}
Message::Move { x, y } => {
println!(
"Move in the x direction {} and in the y direction {}",
x,
y
);
}
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => {
println!(
"Change the color to red {}, green {}, and blue {}",
r,
g,
b
)
}
}
}
14-3-6、解构嵌套的结构体和枚举
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!(
"Change the color to red {}, green {}, and blue {}",
r,
g,
b
)
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!(
"Change the color to hue {}, saturation {}, and value {}",
h,
s,
v
)
}
_ => ()
}
}
14-3-7、解构结构体和元组
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
14-3-8、忽略
有时忽略模式中的一些值是有用的,比如 match 中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 _ 模式(我们已经见过了),在另一个模式中使用 _ 模式,使用一个以下划线开始的名称,或者使用 .. 忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做。
函数
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
嵌套
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {:?}", setting_value);
这段代码会打印出 Can't overwrite an existing customized value 接着是 setting is Some(5)
测试 setting_value 和 new_setting_value 都为 Some 成员的情况,如果有值不为Some才会调用下面的赋值
元组
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}
忽略未使用变量
fn main() {
let _x = 5;
let y = 10;
let s = Some(String::from("Hello!"));
//if let Some(_s) = s 使用_s会报错
if let Some(_) = s {
println!("found a string");
}
println!("{:?}", s);
}
因为 s 的值仍然会移动进 _s,并阻止我们再次使用 s。然而只使用下划线本身,并不会绑定值。示例 18-22 能够无错编译,因为 s 没有被移动进 _
用..忽略剩余值
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
只操作 x 坐标并忽略 y 和 z 字段的值
元组..忽略
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {}, {}", first, last);
},
}
}
.. 必须是无歧义的。如果期望匹配和忽略的值是不明确的,Rust 会报错
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second)
},
}
}
14-3-9、匹配守卫
匹配守卫(match guard)是一个指定于 match 分支模式之后的额外 if 条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
现在这会打印出 Default case, x = Some(5)。现在第二个匹配分支中的模式不会引入一个覆盖外部 y 的新变量 y,这意味着可以在匹配守卫中使用外部的 y。相比指定会覆盖外部 y 的模式 Some(y),这里指定为 Some(n)。此新建的变量 n 并没有覆盖任何值,因为 match 外部没有变量 n。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {}", n),
_ => println!("Default case, x = {:?}", x),
}
println!("at the end: x = {:?}, y = {}", x, y);
}
也可以在匹配守卫中使用 或 运算符 | 来指定多个模式,同时匹配守卫的条件会作用于所有的模式
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
//优先级
(4 | 5 | 6) if y => ...
14-3-10、@绑定
at 运算符(@)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式。
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable)
},
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}
使用 @ 可以在一个模式中同时测试和保存变量值。
15、高级特征
15-1、不安全的rust
Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rust(unsafe Rust)。它与常规 Rust 代码无异,但是会提供额外的超能力。
15-1-1、unsafe
有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力。
- 解引用裸指针
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 实现不安全 trait
- 访问
union的字段
这五类操作必须位于标记为 unsafe 的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe 块内。保持 unsafe 块尽可能小
15-1-2、解引用指针
裸指针与引用和智能指针的区别在于:
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
同时创建不可变和可变裸指针
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
可以在安全代码中 创建 裸指针,只是不能在不安全块之外 解引用 裸指针
创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。需要在unsafe中调用
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
15-1-3、不安全函数或方法
split_at_mut
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid))
}
}
slice::from_raw_parts_mut 函数是不安全的因为它获取一个裸指针,并必须确信这个指针是有效的。裸指针上的 add 方法也是不安全的,因为其必须确信此地址偏移量也是有效的指针。因此必须将 slice::from_raw_parts_mut 和 add 放入 unsafe 块中以便能调用它们。通过观察代码,和增加 mid 必然小于等于 len 的断言
15-1-4、使用exterm调用外部代码
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern,有助于创建和使用 外部函数接口(Foreign Function Interface, FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
在 extern "C" 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。"C" ABI 是最常见的,并遵循 C 编程语言的 ABI。
使用 extern 来创建一个允许其他语言调用 Rust 函数的接口。不同于 extern 块,就在 fn 关键字之前增加 extern 关键字并指定所用到的 ABI。还需增加 #[no_mangle] 标注来告诉 Rust 编译器不要 mangle 此函数的名称。
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
15-1-5、访问或修改可变静态变量
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静态变量一般以大写形式书写;
常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。
常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 不安全 的
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER);
}
}
15-1-6、不安全trait
unsafe 的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe,
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe 表明。
15-1-7、访问联合体中的字段
仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互
15-2、高级trait
15-2-1、关联类型在trait定义中指定占位符类型
关联类型(associated types)是一个将类型占位符与 trait 相关联的方式,这样 trait 的方法签名中就可以使用这些占位符类型。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
和泛型的区别
泛型需要在每个实现中标注类型;
当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 Counter 的 next 方法时,必须提供类型标注来表明希望使用 Iterator 的哪一个实现。
15-2-2、运算符重载
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
//Point 结构体上实现 Add trait 来重载 + 运算符,这样就可以将两个 Point 实例相加了
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 });
}
Add trait 有一个叫做 Output 的关联类型,它用来决定 add 方法的返回值类型
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
尖括号中的 RHS=Self:这个语法叫做 默认类型参数(default type parameters)。RHS 是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add 方法中的 rhs 参数。如果实现 Add trait 时不指定 RHS 的具体类型,RHS 的类型将是默认的 Self 类型,也就是在其上实现 Add 的类型
Add trait 时希望自定义 RHS 类型而不是使用默认类型的例子
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
为了使 Millimeters 和 Meters 能够相加,我们指定 impl Add<Meters> 来设定 RHS 类型参数的值而不是使用默认的 Self。
默认参数类型主要用于如下两个方面:
- 扩展类型而不破坏现有代码。
- 在大部分用户都不需要的特定情况进行自定义。
不能从外部读取类型,这不是动态语言,是静态语言
15-2-3、调用名字相同的方法
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
当调用 Human 实例的 fly 时,编译器默认调用直接实现在类型上的方法,如示例 19-17 所示。
fn main() {
let person = Human;
person.fly();
}
为了能够调用 Pilot trait 或 Wizard trait 的 fly 方法,我们需要使用更明显的语法以便能指定我们指的是哪个 fly 方法。这个语法展示在示例 19-18 中:
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();//也可以选择写成 Human::fly(&person),这等同于示例 19-18 中的 person.fly()
}
15-2-4、完全限定语法
关联函数是 trait 的一部分,但没有 self 参数。当同一作用域的两个类型实现了同一 trait,Rust 就不能计算出我们期望的是哪一个类型,除非使用 完全限定语法(fully qualified syntax)
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Animal trait,它有关联函数 baby_name,结构体 Dog 实现了 Animal,同时有关联函数 baby_name 直接定义于 Dog 之上,如果要调用trait重新赋值在Dog上的方法,因为他不是实现对象,所以没有self
通常,完全限定语法定义为:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
15-2-5、trait中实现另一个trait
实质:在一个trait上添加另一个trait能使之默认实现,可以理解成继承,不过是添加在父trait上
有时我们可能会需要某个 trait 使用另一个 trait 的功能。在这种情况下,需要能够依赖相关的 trait 也被实现。这个所需的 trait 是我们实现的 trait 的 父(超) trait(supertrait)。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
因为指定了 OutlinePrint 需要 Display trait,则可以在 outline_print 中使用 to_string, 其会为任何实现 Display 的类型自动实现。如果我们尝试使用 to_string 而不添加冒号(:)并在 trait 名称后面指定 Display trait,则会得到一个错误说在当前作用域中没有找到用于 &Self 类型的方法 to_string。
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main(){
let point = Point { x: 3, y: 4 };
point.outline_print();
}
//那么在 Point 上实现 OutlinePrint trait 将能成功编译,并可以在 Point 实例上调用 outline_print 来显示位于星号框中的点的值。
在 Point 上实现 OutlinePrint trait 将能成功编译,并可以在 Point 实例上调用 outline_print 来显示位于星号框中的点的值
15-2-6、newType
*如何不用宏来实现打印
孤儿规则(orphan rule),它说明只要 trait 或类型对于当前 crate 是本地的话就可以在此类型上实现该 trait。一个绕开这个限制的方法是使用 newtype 模式(newtype pattern)
例如,如果想要在 Vec<T> 上实现 Display,而孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。可以创建一个包含 Vec<T> 实例的 Wrapper 结构体,
use std::fmt;
//()元组
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w);
}
在 Vec<T> 上实现 Display,而孤儿规则阻止我们直接这么做,因为 Display trait 和 Vec<T> 都定义于我们的 crate 之外。可以创建一个包含 Vec<T> 实例的 Wrapper 结构体,接着可以如示例 19-31 那样在 Wrapper 上实现 Display 并使用 Vec<T> 的值:
Display 的实现使用 self.0 来访问其内部的 Vec<T>,因为 Wrapper 是元组结构体而 Vec<T> 是结构体总位于索引 0 的项。接着就可以使用 Wrapper 中 Display 的功能了。
15-3、高级类型
15-3-1、类型别名
Rust 还提供了声明 类型别名(type alias)的能力,使用 type 关键字来给予现有类型另一个名字
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
类型别名的主要用途是减少重复。例如,可能会有这样很长的类型:
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
}
类型别名也经常与 Result<T, E> 结合使用来减少重复。考虑一下标准库中的 std::io 模块。
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
15-3-2、never type
- 用于无返回值
- 用于修正match,用于panic!
- loop
Rust 有一个叫做 ! 的特殊类型。在类型理论术语中,它被称为 empty type,因为它没有值。我们更倾向于称之为 never type。这个名字描述了它的作用:在函数从不返回的时候充当返回值。例如:
fn bar() -> ! {
// --snip--
}
match
忽略了代码中的一些细节,match 的分支必须返回相同的类型
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
他们需要同一个返回值,前者是 u32 值,而后者是 ! 值。因为 ! 并没有一个值,Rust 决定 guess 的类型是 u32。所以在 Err 的情况,事实上并未对 guess 赋值。
panic
never type 的另一个用途是 panic!。还记得 Option<T> 上的 unwrap 函数吗?它产生一个值或 panic。这里是它的定义:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
loop
print!("forever ");
loop {
print!("and ever ");
}
15-3-3、动态大小和size trait
因为 Rust 需要知道例如应该为特定类型的值分配多少空间这样的信息其类型系统的一个特定的角落可能令人迷惑:这就是 动态大小类型(dynamically sized types)的概念。这有时被称为 “DST” 或 “unsized types”,这些类型允许我们处理只有在运行时才知道大小的类型。
str 是一个 DST;直到运行时我们都不知道字符串有多长
Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存
let s1: str = "Hello there!";
let s2: str = "How's it going?";
//如果允许编写这样的代码,也就意味着这两个 str 需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建一个存放动态大小类型的变量的原因。
s1 和 s2 的类型是 &str 而不是 str所以就不会报错
所以虽然 &T 是一个储存了 T 所在的内存位置的单个值,&str 则是 两个 值:str 的地址和其长度。这样,&str 就有了一个在编译时可以知道的大小:它是 usize 长度的两倍。也就是说,我们总是知道 &str 的大小,
动态大小类型的黄金规则:必须将动态大小类型的值置于某种指针之后。
为了处理 DST,Rust 有一个特定的 trait 来确定一个类型的大小是否在编译时可知:这就是 Sized trait。这个 trait 自动为编译器在编译时就知道其大小的类型实现。另外,Rust 隐式的为每一个泛型函数增加了 Sized bound。也就是说,对于如下泛型函数定义:
fn generic<T>(t: T) {
// --snip--
}
实际上被当作如下处理:
fn generic<T: Sized>(t: T) {
// --snip--
}
以使用如下特殊语法来放宽这个限制:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
15-4、高级函数和闭包
15-4-1、函数指针
函数的类型是 fn (使用小写的 “f” )以免与 Fn 闭包 trait 相混淆。fn 被称为 函数指针(function pointer)。指定参数为函数指针的语法类似于闭包
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer);
}
不同于闭包,fn 是一个类型而不是一个 trait,所以直接指定 fn 作为参数而不是声明一个带有 Fn 作为 trait bound 的泛型参数。
可以使用内联定义的闭包又可以使用命名函数
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> = list_of_numbers
.iter()
.map(|i| i.to_string())// .map(ToString::to_string)//
.collect();
元组结构体和元组结构体枚举成员的实现细节
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> =
(0u32..20)
.map(Status::Value)
.collect();
15-4-2、返回闭包
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
//报错
不允许使用函数指针 fn 作为返回值类型。Rust 并不知道需要多少空间来储存闭包
我们可以使用trait对象
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
15-5、宏
从根本上来说,宏是一种为写其他代码而写代码的方式,即所谓的 元编程(metaprogramming);
宏可以在编译器翻译代码前展开,例如,宏可以在一个给定类型上实现 trait 。而函数则不行,因为函数是在运行时被调用,同时 trait 需要在编译时实现
宏(Macro)指的是 Rust 中一系列的功能:使用 macro_rules! 的 声明(Declarative)宏,和三;种 过程(Procedural)宏:
- 自定义
#[derive]宏在结构体和枚举上指定通过derive属性添加的代码 - 类属性(Attribute-like)宏定义可用于任意项的自定义属性
- 类函数宏看起来像函数不过作用于作为参数传递的 token
15-5-1、macro_rules!的声明宏用于通用元编程
Rust 最常用的宏形式是 声明宏(declarative macros)。它们有时也被称为 “macros by example”、“macro_rules! 宏” 或者就是 “macros”。其核心概念是,声明宏允许我们编写一些类似 Rust match 表达式的代码;
可以使用 macro_rules! 来定义宏。让我们通过查看 vec! 宏定义来探索如何使用 macro_rules! 结构。第 8 章讲述了如何使用 vec! 宏来生成一个给定值的 vector。例如,下面的宏用三个整数创建一个 vector:
let v: Vec<u32> = vec![1, 2, 3];
无法使用函数做相同的事情,因为我们无法预先知道参数值的数量和类型
vec!的简化定义
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
#[macro_export] 标注说明,只要将定义了宏的 crate 引入作用域,宏就应当是可用的。如果没有该标注,这个宏就不能被引入作用域。
接着使用 macro_rules! 和宏名称开始宏定义,且所定义的宏并 不带 感叹号。名字后跟大括号表示宏定义体,在该例中宏名称是 vec 。
vec! 宏的结构和 match 表达式的结构类似。此处有一个单边模式 ( $( $x:expr ),* ) ,后跟 => 以及和模式相关的代码块。如果模式匹配,该相关代码块将被执行。假设这是这个宏中唯一的模式,则只有这一种有效匹配,其他任何匹配都是错误的。更复杂的宏会有多个单边模式。
首先,一对括号包含了整个模式。接下来是美元符号( $ ),后跟一对括号,捕获了符合括号内模式的值以用于替换后的代码。$() 内则是 $x:expr ,其匹配 Rust 的任意表达式,并将该表达式记作 $x。
$() 之后的逗号说明一个可有可无的逗号分隔符可以出现在 $() 所匹配的代码之后。紧随逗号之后的 * 说明该模式匹配零个或更多个 * 之前的任何模式。
当以 vec![1, 2, 3]; 调用宏时,$x 模式与三个表达式 1、2 和 3 进行了三次匹配。
调用该宏时,替换该宏调用所生成的代码会是下面这样:
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
15-5-2、从属性生产过程宏
第二种形式的宏被称为 过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收 Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技术原因,将来我们希望能够消除这些限制。使用这些宏需采用类似示例 19-29 所示的代码形式,其中 some_attribute 是一个使用特定宏的占位符。
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
定义过程宏的函数以一个 TokenStream 作为输入并产生一个 TokenStream 作为输出。该 TokenStream 类型由包含在 Rust 中的 proc_macro crate 定义,并表示令牌序列。这是宏的核心:宏操作的源代码构成了输入 TokenStream,宏产生的代码是输出 TokenStream。该函数还附加了一个属性,该属性指定我们正在创建过程宏的类型。我们可以在同一个 crate 中拥有多种过程宏。
15-5-3、编写自定义derive宏
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
运行该代码将会打印 Hello, Macro! My name is Pancakes! 第一步是像下面这样新建一个库 crate:
cargo new hello_macro --lib
文件名: src/lib.rs
pub trait HelloMacro {
fn hello_macro();
}
下一步是定义过程式宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取消。构造 crate 和其中宏的惯例如下:对于一个 foo 的包来说,一个自定义的派生过程宏的包被称为 foo_derive 。在 hello_macro 项目中新建名为 hello_macro_derive 的包。
cargo new hello_macro_derive --lib
由于两个 crate 紧密相关,因此在 hello_macro 包的目录下创建过程式宏的 crate。如果改变在 hello_macro 中定义的 trait ,同时也必须改变在 hello_macro_derive 中实现的过程式宏。这两个包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可以只用 hello_macro 包而将 hello_macro_derive 作为一个依赖,并重新导出过程式宏的代码。但现在我们组织项目的方式使编程人员在无需 derive 功能时也能够单独使用 hello_macro。
我们需要声明 hello_macro_derive crate 是过程宏(proc-macro) crate。正如稍后将看到的那样,我们还需要 syn 和 quote crate 中的功能,所以需要将其加到依赖中。将下面的代码加入到 hello_macro_derive 的 Cargo.toml 文件中。
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
hello_macro_derive/src/lib.rs
extern crate proc_macro;
use crate::proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 将 Rust 代码解析为语法树以便进行操作
let ast = syn::parse(input).unwrap();
// 构建 trait 实现
impl_hello_macro(&ast)
}
现在,我们已经引入了三个新的 crate:proc_macro 、 syn 和 quote 。Rust 自带 proc_macro crate,因此无需将其加到 Cargo.toml 文件的依赖中。proc_macro crate 是编译器用来读取和操作我们 Rust 代码的 API。
syn crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。quote 则将 syn 解析的数据结构转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的解析器并不是一件简单的工作。
当用户在一个类型上指定 #[derive(HelloMacro)] 时,hello_macro_derive 函数将会被调用。原因在于我们已经使用 proc_macro_derive 及其指定名称对 hello_macro_derive 函数进行了标注:HelloMacro,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
TokenStream 的 input 转换为一个我们可以解释和操作的数据结构。这正是 syn 派上用场的地方。syn 中的 parse_derive_input 函数获取一个 TokenStream 并返回一个表示解析出 Rust 代码的 DeriveInput 结构体。示例 19-32 展示了从字符串 struct Pancakes; 中解析出来的 DeriveInput 结构体的相关部分:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 ident( identifier,表示名字)为 Pancakes。
此时,尚未定义 impl_hello_macro 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注意其输出也是 TokenStream。所返回的 TokenStream 会被加到我们的 crate 用户所写的代码中,因此,当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。
当调用 syn::parse 函数失败时,我们用 unwrap 来使 hello_macro_derive 函数 panic。在错误时 panic 对过程宏来说是必须的,因为 proc_macro_derive 函数必须返回 TokenStream 而不是 Result,以此来符合过程宏的 API。这里选择用 unwrap 来简化了这个例子;在生产代码中,则应该通过 panic! 或 expect 来提供关于发生何种错误的更加明确的错误信息。
现在我们有了将标注的 Rust 代码从 TokenStream 转换为 DeriveInput 实例的代码,让我们来创建在注明类型上实现 HelloMacro trait 的代码,如示例 19-33 所示。
hello_macro_derive/src/lib.rs
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
我们得到一个包含以 ast.ident 作为注明类型名字(标识符)的 Ident 结构体实例
quote! 宏让我们可以编写希望返回的 Rust 代码。quote! 宏执行的直接结果并不是编译器所期望的并需要转换为 TokenStream。为此需要调用 into 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 TokenStream 类型值。
这个宏也提供了一些非常酷的模板机制;我们可以写 #name ,然后 quote! 会以名为 name 的变量值来替换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 quote crate 的文档 来获取详尽的介绍。
我们期望我们的过程式宏能够为通过 #name 获取到的用户注明类型生成 HelloMacro trait 的实现。该 trait 的实现有一个函数 hello_macro ,其函数体包括了我们期望提供的功能:打印 Hello, Macro! My name is 和标注的类型名。
添加依赖
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
15-5-4、类属性宏
类属性宏与自定义派生宏相似,不同于为 derive 属性生成代码,它们允许你创建新的属性。它们也更为灵活;derive 只能用于结构体和枚举;属性还可以用于其它的项,比如函数。作为一个使用类属性宏的例子,可以创建一个名为 route 的属性用于标注 web 应用程序框架(web application framework)的函数:
#[route(GET, "/")]
fn index() {
#[route] 属性将由框架本身定义为一个过程宏。其宏定义的函数签名看起来像这样:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
这里有两个 TokenStream 类型的参数;第一个用于属性内容本身,也就是 GET, "/" 部分。第二个是属性所标记的项:在本例中,是 fn index() {} 和剩下的函数体。
除此之外,类属性宏与自定义派生宏工作方式一致:创建 proc-macro crate 类型的 crate 并实现希望生成代码的函数!
15-5-6、类函数宏
类函数宏定义看起来像函数调用的宏。类似于 macro_rules!,它们比函数更灵活;例如,可以接受未知数量的参数。然而 macro_rules! 宏只能使用之前 “使用 macro_rules! 的声明宏用于通用元编程” 介绍的类匹配的语法定义。类函数宏获取 TokenStream 参数,其定义使用 Rust 代码操纵 TokenStream,就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的 sql! 宏:
let sql = sql!(SELECT * FROM posts WHERE id=1);
这个宏会解析其中的 SQL 语句并检查其是否是句法正确的,这是比 macro_rules! 可以做到的更为复杂的处理。sql! 宏应该被定义为如此:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {