在Rust标准库中,存在很多常用的工具类特型,它们能帮助我们写出更具有Rust风格的代码。
ToOwned
这次我们来学一个和Borrow
特型相关的特型,叫ToOwned
类型。看字面意思Borrow
是代表借出,而ToOwned
代表去拥有它。
在Rust中,假定某类型实现了Clone
特型,如果给你一个对它引用,那我们得到它指向内容的备份的最常见方式是调用其clone()
函数。但是如果你想克隆&str
或者&[i32]
时会发生什么呢?你的目的可能是想得到一个String
或者Vec<i32>
。但是根据Clone
特型的定义,你无法得到它们。根据定义,对一个&T
调用clone()
会返回一个T
的值,也就是说会返回str
或者[i32]
。而我们前面学习Sized
特型的时候提到过,str
或者[i32]
是切片类型,无固定大小,是不能保存在变量中或者作为函数结果返回的。
std::borrow::ToOwned
特型提供了一个稍微宽松的方法将一引用转换为可拥有的值。
rust
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
上面的定义中,to_owned
函数返回一个类型为Self::Owned
的新鲜值,但是这个Owned
可不是任意类型,它有个类型约束,也就是实现了Borrow<Self>
. 也就是说A能借出B/&B
(实现了Borrow<B>
),B才能拥有A.
例如,你可以从Vec<T>
中借出一个&[T]
(这里的泛型U 为 [T]
),因此[T]
可以实现ToOwned<Owned=Vec<T>>
,只要T
实现了Clone
特型。这里为什么要对T
限制呢?毕竟你要得到一个备份,如果一个T
不能克隆,那么这个备份是无法实现的,因为需要把切片的元素复制到新的向量中去。相似的,str
实现了ToOwned<Owned=String>
,因此我们可以调用&str
的to_owned
函数得到一个全新的字符串。Path
也实现了ToOwned<Owned=PathBuf>
,我们也可以从Path
引用中得到一个全新的PathBuf
值。
Humble Cow
Borrow
和ToOwned
联动可以实现一个很有意思的类型,Cow
,注意它不是奶牛的意思,而是指 clone on write
我们趁热来学习它。
充分利用 Rust 需要深思熟虑所有权问题,例如某个函数是否应该通过引用或值接收参数。通常你能确定使用其中的一种或者另一种(使用引用还是值),函数的参数类型代表了你的决定。但是存在这样一些场景,你只有在运行时才知道到底是需要借用还是引用,这时,std::borrow::Cow
类型就派上用场了,它的定义如下:
rust
enum Cow<'a, B: ?Sized>
where B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
这里可以看到,Cow是一个枚举,有两个变量,分别代表借用和拥有。其Borrow
变量绑定了一个&B
(这里先忽视生命周期标记),这个B是个泛型,它的约束为B: ToOwned
。它的目标类型我们先假定为U,那么U 必定实现了Borrow<B>
。
它的第二个枚举变量为Owned
,绑定了一个<B as ToOwned>::Owned
的值,也就是U的值,所以Owned
变量可以写成Owned<U>
。其中可以从U借出B,当然,也可以从B拥有U的新值。
第一个枚举变量,是绑定了&B,因此我们可以很方便的得到&B
,第二个变量,是绑定了U,然而U又可以借出B,因此我们仍然可以很容易的得到&B
( 通过U的borrow() 函数)。两个变量都可以方便的得到&B
,因此它也实现了Deref
特型,这样你可以直接在Cow上调用B的相关函数,而不管Cow是借用了B还是拥有了U。
你还可以在Cow
类型的值上调用to_mut
函数得到一个&mut B
。 如果Cow
变量刚好好Borrowed
,则to_mut
函数会先调用&B
的to_owned
方法得到它自己拥有的一个U的Copy,并对原来的变量进行重新赋值,这样就从Borrowed
变量转换成了Owned
变量,然后再从新拥有的U的值中借出一个mut 引用。这里正是clone on write
的含义所在(写时clone).
我们来看一下这个Deref
的实现代码:
rust
#[stable(feature = "rust1", since = "1.0.0")]
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B>
where
B::Owned: Borrow<B>,
{
type Target = B;
fn deref(&self) -> &B {
match *self {
Borrowed(borrowed) => borrowed,
Owned(ref owned) => owned.borrow(),
}
}
}
你代码中我们可以看到,如果Cow
是引用 ,直接将这个引用返回,如果是拥有的U值,则从U值借出,这里有一个细节:
Owned(ref owned) => owned.borrow(),
因为我们的deref函数接收参数为&self
,因此我们无法在函数内部消耗掉Cow
本身,而match
直接匹配时便会消耗这个值,因此为了阻止这种行为,添加了ref owned
,代表这个owned
只是获取一个引用 ,因此这里的owned的类型其实为&U
,所以直接调用其borrow()函数也就得到了一个&B. 注意borrow函数也是接收一个引用而非值作为参数。
to_mut
函数的解释为:
Acquires a mutable reference to the owned form of the data.
Clones the data if it is not already owned.
使用示例为:
rust
use std::borrow::Cow;
let mut cow = Cow::Borrowed("foo");
cow.to_mut().make_ascii_uppercase();
assert_eq!(
cow,
Cow::Owned(String::from("FOO")) as Cow<'_, str>
);
通过上面的示例我们可以看到,就算我们的cow是Borrowed
变量,拥有一个共享的引用,到最后也变成了一个Owned
变量。
我们来看一下实现过程:
rust
#[stable(feature = "rust1", since = "1.0.0")]
pub fn to_mut(&mut self) -> &mut <B as ToOwned>::Owned {
match *self {
Borrowed(borrowed) => {
*self = Owned(borrowed.to_owned());
match *self {
Borrowed(..) => unreachable!(),
Owned(ref mut owned) => owned,
}
}
Owned(ref mut owned) => owned,
}
}
这里 如果是Borrowed,则首先会to_owned得到U的一个新值,然后再将self重新赋值为Owned,然后再重新对self进行match操作,
此时已经是一个Owned,所以直接借出了ref mut,注意因为match操作会消耗值,所以这里的Owned(ref mut owned) => owned,
中加了ref 代表是一个引用,结合上例,我们就得到了一个&mut String。
相似的,Cow
也实现了into_owned
方法将引用转换为一个拥有的值。如果必须,则可以将值的所有权转移给调用者,在这个过程中Cow
本身的值会被消耗掉。
Cow一个常见的用法是返回一个静态的字符串文字值常量或者一个动态的字符串。例如,假定你需要将一个枚举类型转换成一个消息,枚举的大多数变量都可用于固定的字符串,但是有一些变量或者一些额外的信息,因此你可以返回一个Cow<'static str>
。
rust
use std::path::PathBuf;
use std::borrow::Cow;
fn describe(error: &Error) -> Cow<'static, str> {
match *error {
Error::OutOfMemory => "out of memory".into(),
Error::StackOverflow => "stack overflow".into(),
Error::MachineOnFire => "machine on fire".into(),
Error::Unfathomable => "machine bewildered".into(),
Error::FileNotFound(ref path) => {
format!("file not found: {}", path.display()).into()
}
}
}
上面的代码使用了Cow
的Into
特型实现来构造值。这里其实是Cow
的From
实现,然后相对应的&str
就有了Into
实现。这个Match的绝大多数分支都返回一个静态分配的文字串文本用于Cow::Borrowed
绑定,只有最后一个分支返回一个String
用于Owned
变量绑定。
describe
函数的调用者不用管返回的到底Cow的哪个变量,它只用简单的将返回值看成是&str
就行了,例如:
println!("Disaster has struck: {}", describe(&error));
如果你需要一个拥有的值,调用into_owned
函数就可,例如 (describe(&error).into_owned()
就返回一个String
。
使用Cow可以让describle
函数和它的调用者直到在需要时才会分配内存来保存新生成的字符串。