昨天我们一起学习了智能指针 Box。今天再学习2个智能指针 Cow和。
这是用于提供写时克隆(Clone-on-Write)的一个智能指针,和虚拟内存管理的写时复制很像。
代码定义如下:
pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized {
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
包含一个只读借用,如果调用方需要所有权做修改操作,他就会clone借用的数据。
这里发现2个新的trait:
这2个trait的代码定义:
pub trait ToOwned {
type Owned: Borrow<Self>;
#[must_use = "cloning is often expensive and is not expected to have side effects"]
fn to_owned(&self) -> Self::Owned;
fn clone_into(&self, target: &mut Self::Owned) { ... }
}
pub trait Borrow<Borrowed> where Borrowed: ?Sized {
fn borrow(&self) -> &Borrowed;
}
可以看到Owned是关联类型,需要使用者定义,和之前接触的关联类型不同的是,这里Owned不能是任意类型,需要满足Borrow<T> trait。
看一下str 对 ToOwned trait的实现:
impl ToOwned for str {
type Owned = String;
#[inline]
fn to_owned(&self) -> String {
unsafe { String::from_utf8_unchecked(self.as_bytes().to_owned()) }
}
fn clone_into(&self, target: &mut String) {
let mut b = mem::take(target).into_bytes();
self.as_bytes().clone_into(&mut b);
*target = unsafe { String::from_utf8_unchecked(b) }
}
}
这里Owned被定义为String,而String必须定义Borrow<T>。ToOwned要求是Borrow<Self>,这里实现ToOwned的主体是str,Borrow<Self>就是Borrow<str>, 也就是说String 要实现 Borrow,看代码也确实实现了。
impl Borrow<str> for String {
#[inline]
fn borrow(&self) -> &str {
&self[..]
}
}
通过这张图,我们可以更好地搞清楚 Cow 和 ToOwned / Borrow之间的关系。
那么为何 Borrow 要定义成一个泛型 trait 呢?
use std::borrow::Borrow;
fn main() {
let s = "hello world!".to_owned();
// 这里必须声明类型,因为 String 有多个 Borrow<T> 实现
// 借用为 &String
let r1: &String = s.borrow();
// 借用为 &str
let r2: &str = s.borrow();
println!("r1: {:p}, r2: {:p}", r1, r2);
}
从这个例子可以看到 String可以Borrow出来&String,也可以Borrow出来&str。因为它有多个Borrow<T>的实现。
Cow是智能指针,就必须实现Deref trait:
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
type Target = B;
fn deref(&self) -> &B {
match *self {
Borrowed(borrowed) => borrowed,
Owned(ref owned) => owned.borrow(),
}
}
}
根据 self 是 Borrowed 还是 Owned,我们分别取其内容,生成引用:
这样,虽然 Cow 是一个 enum,但是通过 Deref 的实现,我们可以获得统一的体验。这种根据 enum 的不同状态来进行统一分发的方法是第三种分发手段,之前讲过可以使用泛型参数做静态分发和使用 trait object 做动态分发。
Cow可以在需要的时候 才进行内存分配和拷贝。如果Cow<'a, B> 中的 Owned 数据类型是一个需要在堆上分配内存的类型,如 String、Vec等,还能减少堆内存分配的次数。
我们知道堆内存的分配/释放效率远不及栈内存,减少不必要的堆内存分配是提升软件效率的关键手段,Cow<'a, B>就可以达到这个效果,说体验还非常舒服。(但是我到目前为止,舒服是没有感觉到,别扭到感觉到不少)。
作者为了证明Cow的带来的舒服,还举了一个解析url参数和jsonDecode的例子:说大多数做法是把key->value 都是新的字符串。请求大起来就会有很多堆内存的分配。(话说难道,不应该预先分配内存,做池化处理的吗?)
然后作者说明了一下Cow的过程:
解析url:
use std::borrow::Cow;
use url::Url;
fn main() {
let url = Url::parse("https://tyr.com/rust?page=1024&sort=desc&extra=hello%20world").unwrap();
let mut pairs = url.query_pairs();
assert_eq!(pairs.count(), 3);
let (mut k, v) = pairs.next().unwrap();
// 因为 k, v 都是 Cow<str> 他们用起来感觉和 &str 或者 String 一样
// 此刻,他们都是 Borrowed
println!("key: {}, v: {}", k, v);
// 当修改发生时,k 变成 Owned
k.to_mut().push_str("_lala");
print_pairs((k, v));
print_pairs(pairs.next().unwrap());
// 在处理 extra=hello%20world 时,value 被处理成 "hello world"
// 所以这里 value 是 Owned
print_pairs(pairs.next().unwrap());
}
fn print_pairs(pair: (Cow<str>, Cow<str>)) {
println!("key: {}, value: {}", show_cow(pair.0), show_cow(pair.1));
}
fn show_cow(cow: Cow<str>) -> String {
match cow {
Cow::Borrowed(v) => format!("Borrowed {}", v),
Cow::Owned(v) => format!("Owned {}", v),
}
}
解析json的例子:
use serde::Deserialize;
use std::borrow::Cow;
#[derive(Debug, Deserialize)]
struct User<'input> {
#[serde(borrow)]
name: Cow<'input, str>,
age: u8,
}
fn main() {
let input = r#"{ "name": "Tyr", "age": 18 }"#;
let user: User = serde_json::from_str(input).unwrap();
match user.name {
Cow::Borrowed(x) => println!("borrowed {}", x),
Cow::Owned(x) => println!("owned {}", x),
}
}
MutexGuard<T> 是另外一类很有意思的智能指针:它不但通过 Deref 提供良好的用户体验,还通过 Drop trait 来确保,使用到的内存以外的资源在退出时进行释放。
MutexGuard这个结构是在调用 Mutex::lock 时生成的:
pub fn lock(&self) -> LockResult<MutexGuard<'_, T>> {
unsafe {
self.inner.raw_lock();
MutexGuard::new(self)
}
}
// 这里用 must_use,当你得到了却不使用 MutexGuard 时会报警
#[must_use = "if unused the Mutex will immediately unlock"]
pub struct MutexGuard<'a, T: ?Sized + 'a> {
lock: &'a Mutex<T>,
poison: poison::Guard,
}
impl<T: ?Sized> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<T: ?Sized> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<T: ?Sized> Drop for MutexGuard<'_, T> {
#[inline]
fn drop(&mut self) {
unsafe {
self.lock.poison.done(&self.poison);
self.lock.inner.raw_unlock();
}
}
}
MutexGuard在结束时,会做unlock操作,Rust 有所有权机制,可以确保只要 MutexGuard 离开作用域,锁就会被释放。(好像在是Golang里 自动加了 defer unlock的功能。这样是不是还能避免多级锁 不小心,带来的死锁问题?)
例子:
use lazy_static::lazy_static;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
// lazy_static 宏可以生成复杂的 static 对象
lazy_static! {
// 一般情况下 Mutex 和 Arc 一起在多线程环境下提供对共享内存的使用
// 如果你把 Mutex 声明成 static,其生命周期是静态的,不需要 Arc
static ref METRICS: Mutex<HashMap<Cow<'static, str>, usize>> =
Mutex::new(HashMap::new());
}
fn main() {
// 用 Arc 来提供并发环境下的共享所有权(使用引用计数)
let metrics: Arc<Mutex<HashMap<Cow<'static, str>, usize>>> =
Arc::new(Mutex::new(HashMap::new()));
for _ in 0..32 {
let m = metrics.clone();
thread::spawn(move || {
let mut g = m.lock().unwrap();
// 此时只有拿到 MutexGuard 的线程可以访问 HashMap
let data = &mut *g;
// Cow 实现了很多数据结构的 From trait,
// 所以我们可以用 "hello".into() 生成 Cow
let entry = data.entry("hello".into()).or_insert(0);
*entry += 1;
// MutexGuard 被 Drop,锁被释放
});
}
thread::sleep(Duration::from_millis(100));
println!("metrics: {:?}", metrics.lock().unwrap());
}
MutexGuard 不允许 Send,只允许 Sync,也就是说,你可以把 MutexGuard 的引用传给另一个线程使用,但你无法把 MutexGuard 整个移动到另一个线程。
类似 MutexGuard 的智能指针有很多用途。比如要创建一个连接池,你可以在 Drop trait 中,回收 checkout 出来的连接,将其再放回连接池。(开源项目: r2d2)
这2天我们介绍了三个重要的智能指针,它们有各自独特的实现方式和使用场景。
明天我们继续学习集合容器。
如果你觉得有点收获,欢迎点个关注, 也欢迎分享给你身边的朋友。