实用特型2
as引用特型Deref与DerefMut
Deref
和 DerefMut
,它们允许自定义类型的实例能够像引用一样被解引用(dereferenced),从而实现智能指针或其他类似的行为。这两个 trait 提供了对内部数据的访问,但有着不同的权限级别。
Deref
特性
-
目的:
Deref
主要用于将一个类型转换为另一个类型,通常是从一个智能指针类型转换为其内部的数据类型。它使得你可以在不显式调用.deref()
方法的情况下,通过*
运算符来访问内部值。 -
方法:
deref(&self) -> &Target
:返回一个指向内部数据的不可变引用。
-
使用场景:当你有一个智能指针或者其他包装类型,并且希望它能够像普通引用那样工作时,你可以实现
Deref
。例如,Box<T>
、Rc<T>
和&RefCell<T>
都实现了Deref
,这使得你可以直接在这些类型上调用其内部类型的成员函数或操作符,而不需要显式地解引用。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
fn main() {
let x = 5;
let b = MyBox(x);
println!("b contains {}", *b); // 自动解引用到 i32
}
DerefMut
特性
-
目的:
DerefMut
扩展了Deref
的功能,提供了对内部数据的可变访问。这对于需要修改内部值的智能指针非常有用。 -
方法:
deref_mut(&mut self) -> &mut Target
:返回一个指向内部数据的可变引用。
-
使用场景:当你不仅需要读取而且还需要修改智能指针内部的数据时,你应该实现
DerefMut
。比如Box<T>
和Rc<RefCell<T>>
实现了DerefMut
,所以你可以获得对内部数据的可变访问。
use std::ops::{Deref, DerefMut};
struct MyBoxMut<T>(T);
impl<T> Deref for MyBoxMut<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for MyBoxMut<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
fn main() {
let mut x = 5;
let mut b = MyBoxMut(x);
*b += 1; // 自动解引用并修改内部值
println!("b now contains {}", *b);
}
区别
-
访问权限:
Deref
只提供对内部数据的不可变访问。DerefMut
提供对内部数据的可变访问,这意味着它可以用来改变内部数据。
-
实现要求:
- 要实现
DerefMut
,你的类型必须先实现Deref
,因为DerefMut
是基于Deref
的扩展。
- 要实现
-
应用场景:
- 如果你需要一个智能指针或者其他包装类型可以透明地当作其内部类型使用,那么你可以只实现
Deref
。 - 如果你还希望这个包装类型能够提供对其内部数据的可变访问,那么你也应该实现
DerefMut
。
- 如果你需要一个智能指针或者其他包装类型可以透明地当作其内部类型使用,那么你可以只实现
-
注意事项:
- 当实现
Deref
或DerefMut
时,确保遵循 Rust 的借用规则和所有权规则,以避免潜在的安全问题或未定义行为。
- 当实现
Deref
和 DerefMut
允许你创建更直观和方便使用的智能指针和其他包装类型
通过实现 std::ops::Deref
特型和 std::ops::DerefMut
特型,可以指定 像* 和 .
这样的解引用运算符在你的类型上的行为。
像 Box 和 Rc 这 样的指针类型就实现了这些特型,因此它们可以像 Rust 的内置指针类型那样用。如果你有一个 Box 型的值 b,那么 *b 引用的就是 b 指向的 Complex(复数)值,而 b.re 引用的是它的实部。如果上下文对引用目标进行了赋值或借用了可变引用,那么 Rust 就会使用 DerefMut(解可变引用)
特型,否则,只要通过 Deref 进行只读访问
就够了。
trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
deref
方法会接受&Self
引用并返回 &Self::Target
引用,而 deref_mut 方法会接受&mut Self
引用并返回 &mut Self::Target
引用。Target 应 该是 Self 包含、拥有或引用的资源:对于 Box,其 Target 类型 是 Complex。
DerefMut
扩展了 Deref:可以解引用并修改某些资源,当然也可以借入对它的共享引用。由于这些方法会返回与 &self 生命周期相同的引用,因此只要返回的引用还存在,self 就会一直处于已借出状态。
Deref 特型和 DerefMut 特型还扮演着另一个角色。由于 deref 会接受&Self
引用并返回 &Self::Target
引用,Rust会利用这一点自动将前 一种类型的引用转换为后一种类型的引用。换句话说,如果只要插入一个 deref 调用
就能解决类型不匹配问题,那 Rust 就会插入它。实现 DerefMut 也可以为可变引用启用相应的转换。这些叫作隐式解引用:一种类型被“转换”成 了另一种类型。
例如:
- 如果你有一个 Rc 型的值 r,并想对其调用
String::find
,就可以简单地写成r.find('?')
,而不用写成(*r).find('?')
:这种方法调用会隐式借入 r,并将 &Rc 转换为 &String,因为Rc<T>
实现了Deref<Target=T>
。 - 可以对 String 值使用
split_at
之类的方法,虽然split_at
是在 str 切片类型上定义的方法,但因为 String 实现了Deref
,所以可以这样写。String 不需要重新实现 str 的所有方法,因为可以将&String
隐式转换为&str
。 - 如果有一个字节向量 v 并且想将它传给需要字节切片
&[u8]
的函数,就可以简单地将 &v 作为参数传递,因为 Vec 实现了Deref
。
Rust 会连续应用多个隐式解引用。你可以将 split_at 直接应用于 Rc,因为 &Rc 解引用成了 &String,后者又解引用成了 &str,而 &str 具有 split_at 方 法。
struct Selector<T> {
elements: Vec<T>, /// 在这个`Selector`中可用的元素
/// usize是`elements`中“当前”(current)元素的索引
/// `Selector`的行为类似于指向当前元素的指针
current: usize
}
use std::ops::{Deref, DerefMut};
impl<T> Deref for Selector<T> {
type Target = T;
fn deref(&self) -> &T {
&self.elements[self.current]
}
}
impl<T> DerefMut for Selector<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.elements[self.current]
}
}
let mut s = Selector {
elements: vec!['x', 'y', 'z'],
current: 2
};
// 因为`Selector`实现了`Deref`,所以可以使用`*`运算符来引用它的当前元素
assert_eq!(*s, 'z');
// 通过隐式解引用直接在`Selector`上使用`char`的方法断言'z'是字母
assert!(s.is_alphabetic());
// 通过对此`Selector`的引用目标赋值,把'z'改成了'w'
*s = 'w';
assert_eq!(s.elements, ['x', 'y', 'w']);
Deref 特型和 DerefMut 特型旨在实现诸如 Box、Rc 和 Arc
之类的智能指针类型
,以及其拥有型版本
会频繁通过引用来使用的类型(比如 Vec 和 String 就是 [T] 和 str 的拥有型版本)。仅仅为了让 Target 类型的方法能 自动通过类型指针使用(就像 C++ 中那样让基类的方法在子类上可见)就为类型实现 Deref 和 DerefMut 是不对的。 隐式解引用有一个容易引起混淆的地方需要注意:Rust 会用它们来解决类型冲 突,但并不会将其用于满足类型变量的限界。例如,下面的代码能正常工作:
let s = Selector { elements: vec!["good", "bad", "ugly"],
current: 2 };
fn show_it(thing: &str) { println!("{}", thing); }
show_it(&s);
调用 show_it(&s)
时,Rust 发现了一个类型为 &Selector<&str>
的实参 (argument)和一个类型为 &str
的形参(parameter),据此找到了这个 Deref 实现,并根据需要将此调用重写成了 show_it(s.deref())
将 show_it 改成泛型函数,Rust 突然就报错
use std::fmt::Display;
fn show_it_generic<T: Display>(thing: T) { println!("{}", thing); }
show_it_generic(&s);
因为Selector<&str> 本身确实没有实现 Display,但它解引用成了 &str,而 &str 实现了 Display。传入一个类型为 &Selector<&str>
的实参并且函数的形参类型为 &T
,因此类型变量 T 必然是 Selector<&str>
。然后,Rust 会检查这是否满足 T: Display 限界,但因为它不会通过隐式解引用来满足类型变量的限界, Selector<&str> 本身确实没有实现 Display,所以这个检查失败了。
解决:
-
show_it_generic(&s as &str);
//显示转换 -
show_it_generic(&*s);
//强制转换&*
默认值特型Default
向量或字符串默认为空、数值默认为 0、 Option 默认为 None,等等。这样的类型都可以实现 std::default::Default 特型:
trait Default {
fn default() -> Self;
}
default 方法只会返回一个 Self 类型的新值。
impl Default for String {
fn default() -> String {
String::new()
}
}
Rust 的所有集合类型(Vec、HashMap、BinaryHeap 等)都实现了 Default,其 default 方法会返回一个空集合
Iterator 特型的 partition 方法会将迭代器生成的值分为两个集合,并使用闭包来决定每个值 的去向:
use std::collections::HashSet;
let squares = [4, 9, 16, 25, 36, 49, 64];
let (powers_of_two, impure): (HashSet<i32>, HashSet<i32>)
= squares.iter().partition(|&n| n & (n-1) == 0);
assert_eq!(powers_of_two.len(), 3);
assert_eq!(impure.len(), 4);
闭包 |&n| n & (n-1) == 0
会使用一些位操作来识别哪些数值是 2 的幂, 并且 partition 会使用它来生成两个 HashSet。不过,partition 显然不 是专属于 HashSet 的,你可以用它来生成想要的任何种类的集合,只要该集合 类型能够实现 Default 以生成一个初始的空集合,并且实现 Extend<char>
以将 T 添加到集合中就可以。String 实现了 Default 和 Extend<char>
,所以可以这样写:
let (upper, lower): (String, String)
= "Great Teacher Onizuka".chars().partition(|&c| c.is_uppercase());
assert_eq!(upper, "GTO");
assert_eq!(lower, "reat eacher nizuka");
Default 的另一个常见用途是为 表示大量参数集合的结构体 生成默认值,其中大部分参数通常不用更改。
如果类型 T 实现了 Default,那么标准库就会自动为 Rc<T>、Arc<T>、 Box<T>、Cell<T>、RefCell<T>、Cow<T>、Mutex<T> 和 RwLock<T>
实 现 Default。例如,类型 Rc<T>
的默认值就是一个指向类型 T 的默认值的 Rc。 如果一个元组类型的所有元素类型都实现了 Default,那么该元组类型也同样 会实现 Default,这个元组的默认值包含每个元素的默认值。 Rust 不会为结构体类型隐式实现 Default,但是如果结构体的所有字段都实现 了 Default,则可以使用 #[derive(Default)]
为此结构体自动实现 Default。
tobe引用特型AsRef 与 AsMut
AsRef
和 AsMut
用于实例到引用的类型转换的 trait,它们允许你将一个类型的实例转换为另一个类型的引用。这两个 trait 提供了安全且无分配成本的方式来访问内部数据,但有着不同的权限级别:一个是只读访问,另一个是可变访问。
AsRef
特性
-
目的:
AsRef
使得你可以将一种类型的实例转换为另一种类型的不可变引用。这在需要临时借用某些数据时非常有用,而不需要进行所有权转移或创建额外的数据副本。 -
方法:
as_ref(&self) -> &T
:返回一个指向目标类型的不可变引用。
-
使用场景:当你有一个类型的实例,并希望在不改变其所有权的情况下将其作为其他类型的不可变引用传递给函数或方法时,可以实现
AsRef
。例如,String
实现了AsRef<str>
,因此你可以将String
的实例作为&str
使用。
use std::fs::File;
use std::path::Path;
fn open_file<P: AsRef<Path>>(path: P) -> std::io::Result<File> {
File::open(path.as_ref())
}
fn main() -> std::io::Result<()> {
let path = "example.txt";
let file = open_file(path)?;
Ok(())
}
在这个例子中,open_file
函数接受实现了 AsRef<Path>
的任何类型,这意味着它可以接受 &str
、String
或 PathBuf
等多种类型作为参数。
AsMut
特性
-
目的:
AsMut
扩展了AsRef
的功能,提供了对目标类型的可变访问。这对于需要修改数据的情况非常有用。 -
方法:
as_mut(&mut self) -> &mut T
:返回一个指向目标类型的可变引用。
-
使用场景:当你不仅需要读取而且还需要修改数据时,应该实现
AsMut
。例如,如果你有一个包装器类型,并且想要提供一种方式来获取和修改内部值,那么你可以实现AsMut
。
struct Wrapper<T>(T);
impl<T> AsRef<T> for Wrapper<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T> AsMut<T> for Wrapper<T> {
fn as_mut(&mut self) -> &mut T {
&mut self.0
}
}
fn main() {
let mut wrapper = Wrapper(vec![1, 2, 3]);
wrapper.as_mut().push(4);
println!("{:?}", wrapper.as_ref());
}
区别
-
访问权限:
AsRef
只提供对目标类型的不可变访问。AsMut
提供对目标类型的可变访问,这意味着它允许修改数据。
-
实现要求:
AsMut
并不要求必须先实现AsRef
,但是两者通常一起实现,因为它们服务于相似的目的——即提供不同级别的访问权限到同一个内部数据。
-
应用场景:
- 如果你需要一个类型能够被当作其他类型的不可变引用使用,你应该实现
AsRef
。 - 如果你还希望这个类型能够提供对其内部数据的可变访问,那么你也应该实现
AsMut
。
- 如果你需要一个类型能够被当作其他类型的不可变引用使用,你应该实现
-
灵活性与泛型编程:
- 这两个 trait 在编写泛型代码时特别有用,尤其是在你需要处理多种可能的输入类型时,但这些类型最终都可以转换成同一种目标类型。
总之,AsRef
和 AsMut
提供了一种方便的方式来进行类型转换,同时保持 Rust 的所有权和借用规则。它们使得你的代码更加灵活和通用,特别是在处理文件路径、字符串等常见类型时。
实现了 AsRef,那么就意味着你可以高效地从引用中借入 &T
。 AsMut 是 AsRef 针对可变引用的对应类型。
trait AsRef<T: ?Sized> {
fn as_ref(&self) -> &T;
}
trait AsMut<T: ?Sized> {
fn as_mut(&mut self) -> &mut T;
}
Vec 实现了 AsRef<[T]>,而 String 实现了 AsRef。还可 以把 String 的内容借入为字节数组,因此 String 也实现了 AsRef<[u8]>。
AsRef 通常用于让函数更灵活地接受其参数类型。例如,
fn open<P: AsRef<Path>>(path: P) -> Result<File>
open 真正想要的是 &Path,即代表文件系统路径的类型。有了这个函数签名, open 就能接受可以从中借入 &Path 的一切,也就是实现了 AsRef 的 一切。这些类型包括 String 和 str、操作系统接口字符串类型 OsString 和 OsStr,当然还有 PathBuf 和 Path。
通用实现:let ref = as_ref(as_ref(String::new("123")))
impl<'a, T, U> AsRef<U> for &'a T
where T: AsRef<U>,T: ?Sized, U: ?Sized
{
fn as_ref(&self) -> &U {
(*self).as_ref()
}
}
对于任意类型 T 和 U,只要满足 T: AsRef,就必然满足 &T: AsRef:只需追踪引用并像以前那样继续处理即可【允许套娃】
特别是,如果满足 str: AsRef
,那么也会满足 &str: AsRef
。从某种意义上 说,这是一种在检查类型变量的 AsRef 限界时获得受限隐式解引用的方法
尽管 AsRef 和 AsMut 非常简单,但为引用转换提供标准的泛型特型可避免更专用的转换特型数量激增。只要能实现 AsRef,就要尽量避免定义自己 的 AsFoo 特型。
当做特型Borrow 与 BorrowMut
- 目的:Borrow 允许不同类型的值在某些情况下被视为相同,特别是当它们表示相同的底层数据时。这有助于泛型代码更加灵活地处理不同类型的数据,只要这些类型可以被认为是等价的。
- 方法:它定义了 borrow() 方法,返回一个不可变引用,该引用指向一个实现了 Borrow 的类型。
- 关联类型:它有一个关联类型 Borrowed,指定了 borrow() 返回的借用类型。
例子:String 实现了 Borrow,意味着可以将一个 String 当作 &str 来使用。
use std::borrow::Borrow;
fn check<T: Borrow<str>>(s: T) {
println!("Checking string slice: {}", s.borrow());
}
let owned_string = String::from("hello");
check(&owned_string); // 使用 &String
check("world"); // 使用 &str
std::borrow::Borrow
特型类似于 AsRef:如果一个类型实现了 Borrow,那么它的 borrow 方法就能高效地从自身借入一个 &T。
但是 Borrow 施加了更多限制:只有当 &T 能通过与它借来的值相同的方式进行哈希和比较时,此类型才应实现 Borrow。(Rust 并不强制执行此限制,它只是 记述了此特型的意图。)这使得 Borrow 在处理哈希表和树中的键或者处理因 为某些原因要进行哈希或比较的值时非常有用。 这在区分对 String 的借用时很重要,比如 String 实现了 AsRef<str>、 AsRef<[u8]> 和 AsRef<Path>
,但这 3 种目标类型通常具有不一样的哈希值。只有 &str
切片才能保证像其等效的 String 一样进行哈希,因此 String 只实现了 Borrow。 Borrow 的定义与 AsRef 的定义基本相同,只是名称变了:
trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}
Borrow 旨在解决具有泛型哈希表和其他关联集合类型的特定情况。假设你有一 个 HashMap<String,i32>
,用于将字符串映射到数 值。这个表的键是 String,每个条目都有一个键。在这个表中查找某个条目的 方法的签名应该是什么呢?
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get(&self, key: K) -> Option<&V> { ... }
}
但在这里,K 是 String,这种签名会强制你将 String 按值传给对 get 的每次调用,这显然是一种浪费
所以:
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get(&self, key: &K) -> Option<&V> { ... }
}
想查找常 量字符串,就必须像下面这样写:
hashtable.get(&"twenty-two".to_string())
在堆上分配一个 String 缓冲区并将文本复制进去,这样才能将其作为 &String 借用出来,传给 get,然后将其丢弃。这显然也是一种浪费
只要求传入任何可以哈希并与我们的键类型进行比较的类型。例如, &str 就完全够用了。
impl<K, V> HashMap<K, V> where K: Eq + Hash
{
fn get<Q: ?Sized>(&self, key: &Q) -> Option<&V>
where K: Borrow<Q>,Q: Eq + Hash
{ ... }
}
只要可以借入一个条目的键充当 &Q
,并且对生成的引用进行哈希和 比较的方式与键本身一致,&Q 显然就是可接受的键类型。由于 String 实现了 Borrow<str> 和 Borrow<String>
,因此最终版本的 get 允许按需传入 &String 型或 &str 型
的 key。
Vec 和 [T; N] 实现了 Borrow<[T]>
。每个类似字符串的类型都能借入其相应的切片类型:String 实现了 Borrow<str>
、PathBuf 实现了 Borrow<Path>
,等等。标准库中所有关联集合类型都使用 Borrow 来决定哪些类型可以传给它们的查找函数。
标准库中包含一个通用实现,因此每个类型 T 都可以从自身借用:T: Borrow<T>
。这确保了在 HashMap<K,V>
中查找条目时 &K 总是可接受的类型。
为便于使用,每个 &mut T 类型
也都实现了 Borrow<T>
,它会像往常一样返回 一个共享引用 &T。这样你就可以给集合的查找函数传入可变引用,而不必重新借入共享引用,以模拟 Rust 通常会从可变引用到共享引用进行的隐式转换。
BorrowMut 特型则类似于针对可变引用的 Borrow:
trait BorrowMut<Borrowed: ?Sized>: Borrow<Borrowed> {
fn borrow_mut(&mut self) -> &mut Borrowed;
}
类型转换特型From 与 Into
std::convert::From
特型和 std::convert::Into
特型表示类型转换, 这种转换会接受一种类型的值并返回另一种类型的值。AsRef 特型和 AsMut 特型用于从一种类型借入另一种类型的引用,而 From 和 Into 会获取其参数的所有权,对其进行转换,然后将转换结果的所有权返回给调用者。 From 和 Into 的定义是对称的:
trait Into<T>: Sized {
fn into(self) -> T;
}
trait From<T>: Sized {
fn from(other: T) -> Self;
}
标准库自动实现了从每种类型到自身的简单转换:每种类型 T 都实现了 From<T>
和 Into<T>
。
-
使用 Into 来让函数在接受参数时更加灵活
use std::net::Ipv4Addr; fn ping<A>(address: A) -> std::io::Result<bool> where A: Into<Ipv4Addr> { let ipv4_address = address.into(); ... } println!("{:?}", ping(Ipv4Addr::new(23, 21, 68, 141))); // 传入一个Ipv4Addr println!("{:?}", ping([66, 146, 219, 98])); // 传入一个[u8; 4] println!("{:?}", ping(0xd076eb94_u32)); let addr1 = Ipv4Addr::from([66, 146, 219, 98]); let addr2 = Ipv4Addr::from(0xd076eb94_u32);
与 AsRef 一样,其效果很像 C++ 中的函数重载
-
From 特型扮演着另一种角色。from 方法会充当泛型构造函数
给定适当的 From 实现,标准库会自动实现相应的 Into 特型。因为转换方法 from 和 into 会接手它们的参数的所有权,所以此转换可以复用 原始值的资源来构造出转换后的值
let text = "Beautiful Soup".to_string();
let bytes: Vec<u8> = text.into();
String 的 Into<Vec<u8>>
的实现只是获取 String 的堆缓冲区,并在不进 行任何更改的情况下将其重新用作所返回向量的元素缓冲区。此转换既不需要分配内存,也不需要复制文本。
From 和 Into 的转换可能会分配内存、 复制或以其他方式处理值的内容。例如,String 实现了 From<&str>,它会 将字符串切片复制到 String 在堆上分配的新缓冲区中
扩展特型TryFrom 与 TryInto
由于转换的行为方式不够清晰,因此 Rust 没有为 i32 实现 From,也没 有实现任何其他可能丢失信息的数值类型之间的转换,而是为 i32 实现了 TryFrom。TryFrom 和 TryInto 是 From 和 Into 的容错版“表亲”, 这种转换同样是双向的,实现了 TryFrom 也就意味着实现了 TryInto。
pub trait TryFrom<T>: Sized {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
pub trait TryInto<T>: Sized {
type Error;
fn try_into(self) -> Result<T, Self::Error>;
}
From 和 Into 可以将类型与简单转换关联起来,而 TryFrom 和 TryInto 通 过 Result 提供的富有表现力的错误处理扩展了 From 和 Into 的简单转换。 这 4 个特型可以一起使用,在同一个 crate 中关联多个类型
引用复原特型ToOwned
转移引用指向的内存 如:&int 返回 新的int
给定一个引用,如果此类型实现了 std::clone::Clone
,则生成其引用目标的拥有型副本的常用方法是调用 clone。但是当你想克隆一个&str 或 & [i32]
时该怎么办呢?你想要的可能是 String 或 Vec,但 Clone 的定义不允许这样做:根据定义,克隆 &T
必须始终返回 T 类型的值,并且 str 和 [u8] 是无固定大小类型,它们甚至都不是函数所能返回的类型。
std::borrow::ToOwned
特型提供了一种稍微宽松的方式来将引用转换为拥有型的值:
trait ToOwned {
type Owned: Borrow<Self>;
fn to_owned(&self) -> Self::Owned;
}
与必须精确返回 Self 类型的 clone 不同,to_owned 可以返回任何能让你从中借入 &Self
的类型:Owned 类型必须实现 Borrow<Self>
。你可以从 Vec<T>
借入 &[T]
,所以只要 T 实现了 Clone
,[T] 就能实现 ToOwned<Owned=Vec<T>>
,这样就可以将切片的元素复制到向量中了。同样,str 实现了 ToOwned<Owned=String>
,Path 实现了 ToOwned<Owned=Pathpuf>
等等。
fn main() {
// 创建一个字符串切片(借用数据)
let borrowed_str: &str = "hello";
// 使用 to_owned() 方法获取 String 类型的所有权
let owned_string: String = borrowed_str.to_owned();
println!("Original borrowed str: {}", borrowed_str);
println!("Owned String: {}", owned_string);
// 对于 Vec<T> 和 &[T] 同样适用
let borrowed_slice: &[i32] = &[1, 2, 3];
let owned_vec: Vec<i32> = borrowed_slice.to_vec(); // 或者使用 .to_owned()
println!("Original slice: {:?}", borrowed_slice);
println!("Owned Vec: {:?}", owned_vec);
}
首先创建了一个字符串切片 borrowed_str,它是一个不可变引用,指向静态存储中的字符串。
然后我们调用了 to_owned() 方法,将 &str 转换成了拥有数据所有权的 String 类型。
对于数组切片 &[i32],我们也做了类似的操作,使用 to_vec() 方法(或 to_owned())来获得一个拥有数据所有权的 Vec。
自定义特性ToOwned:
use std::borrow::ToOwned;
#[derive(Debug, Clone)]
struct MyString(String);
impl ToOwned for str {
type Owned = MyString;
fn to_owned(&self) -> Self::Owned {
MyString(self.to_string())
}
}
fn main() {
let s: &str = "example";
let my_string: MyString = s.to_owned();
println!("{:?}", my_string);
}
Borrow 与 ToOwned 的实际运用:枚举Cow
Cow<T>
是 Rust 标准库中的一个类型,它的全称是 “Clone-On-Write”(写时复制)。Cow<T>
提供了一种灵活的方式在借用数据和拥有数据之间进行选择,从而优化性能并减少不必要的内存分配。它允许你在需要的时候才克隆数据,而在不需要修改的情况下可以继续借用现有的数据。
Cow<T>
的特点
-
三种状态:
Cow<T>
可以处于三种不同的状态之一:- 借用的不可变数据 (
Borrowed
):表示当前Cow<T>
正在借用外部的数据。 - 借用的可变数据 (
BorrowedMut
):Rust 没有直接提供这种状态;Cow<T>
中的借用总是不可变的。 - 拥有的数据 (
Owned
):表示Cow<T>
拥有了自己的数据副本。
- 借用的不可变数据 (
-
智能转换:当需要修改数据时,
Cow<T>
会自动将借用的数据转换为拥有的数据,并执行必要的克隆操作。这避免了在不需要修改时进行不必要的克隆。 -
泛型参数:
T
必须实现ToOwned
trait,这意味着T
必须能够从其借用形式转换为拥有形式。例如,str
和[T]
都实现了ToOwned
,因此你可以使用Cow<str>
和Cow<[T]>
。
使用场景
Cow<T>
特别适用于以下情况:
- 当你有一个 API,它可以接受借用的数据或拥有的数据作为输入,并且你希望在内部尽可能地借用数据,但又能够在必要时克隆数据。
- 当你需要对数据进行潜在的修改,但不确定是否真的会发生修改,这时可以先借用数据,只有在确实需要修改时才克隆。
示例
下面是一个简单的例子,展示了如何使用 Cow<str>
来处理字符串数据:
use std::borrow::Cow;
fn process_text(text: Cow<str>) -> String {
// 如果 text 是 Borrowed 并且我们需要修改它,那么这里会触发 clone
if text.contains("world") {
let mut owned = text.to_owned(); // 显式转换为拥有形式,发生了克隆
owned.push_str("!");
owned
} else {
// 否则,我们可以直接返回文本(无论是借用还是拥有)
text.into_owned()
}
}
fn main() {
let s1 = "hello".to_string();
let cow1: Cow<str> = Cow::Borrowed(&s1);
println!("{}", process_text(cow1)); // 不会触发 clone
let s2 = "hello world".to_string();
let cow2: Cow<str> = Cow::Owned(s2);
println!("{}", process_text(cow2)); // 触发 clone 因为进行了修改
}
在这个例子中,process_text
函数接收一个 Cow<str>
参数,它可以是借用的 &str
或者拥有的 String
。函数内部根据条件决定是否需要修改数据。如果需要修改,则会触发克隆操作;否则,它会尽量保持借用的形式,从而节省资源。
Cow<T>
两个特别重要的方法是 into_owned()
和 to_mut()
。这两个方法允许你灵活地处理 Cow<T>
内部的数据,确保你能够根据需要获取拥有所有权的数据或可变引用。
1. into_owned()
-
作用:这个方法将
Cow<T>
转换为拥有所有权的T
类型。如果Cow<T>
当前已经处于Owned
状态,则直接返回内部的数据;如果它处于Borrowed
状态,则会克隆数据并返回拥有的副本。 -
签名:
pub fn into_owned(self) -> T
-
使用场景:当你确定需要拥有数据的所有权时,可以调用
into_owned()
。这在你需要长期保存数据、传递给其他所有者或者需要修改数据但不想保留原始借用的情况下非常有用。
示例
use std::borrow::Cow;
fn main() {
let borrowed_str = "hello";
let cow: Cow<str> = Cow::Borrowed(borrowed_str);
// 将 Cow<str> 转换为拥有所有权的 String
let owned_string: String = cow.into_owned();
println!("Owned string: {}", owned_string);
}
在这个例子中,cow.into_owned()
会创建一个新的 String
实例,即使原始数据是借用的。
2. to_mut()
-
作用:
to_mut()
方法确保你可以获得对Cow<T>
内部数据的可变引用。如果Cow<T>
已经处于Owned
状态,则直接返回一个可变引用;如果它处于Borrowed
状态,则会先克隆数据转换为Owned
状态,然后再返回可变引用。 -
签名:
pub fn to_mut(&mut self) -> &mut T
-
使用场景:当你需要修改
Cow<T>
内部的数据,并且不确定当前是否已经拥有数据的所有权时,可以调用to_mut()
。这在你需要确保能够在不违反借用规则的前提下修改数据时非常有用。
示例
use std::borrow::Cow;
fn main() {
let mut borrowed_str = "hello".to_string();
let mut cow: Cow<str> = Cow::Borrowed(&borrowed_str);
// 修改 Cow<str> 中的数据
cow.to_mut().make_ascii_uppercase(); // 这里会触发 clone
println!("Modified string: {}", cow);
}
在这个例子中,调用 cow.to_mut()
会导致 Cow<str>
从 Borrowed
状态转换为 Owned
状态,从而允许我们修改字符串的内容。
总结
- 使用
into_owned()
可以确保你总是得到拥有所有权的数据副本,无论原来的数据是借用的还是拥有的。 - 使用
to_mut()
则可以在需要时安全地获得对数据的可变访问权限,同时避免不必要的克隆,除非确实需要修改数据。
这两个方法使得 Cow<T>
成为了一个强大而灵活的工具,适用于需要高效管理数据所有权和借用关系的场景。
Cow<T>
提供了一个优雅的解决方案,用于在借用数据和拥有数据之间做出选择,同时确保只有在真正需要的时候才会发生数据的克隆。这对于提高性能、减少内存分配非常有用,尤其是在编写高性能的库或框架时。
函数应该通过引用还是值接受参数。通常可以任选一种方式,让参数的类型反映你的决定。但在某些情况下,在程序开始运行之前你无法决定是该借用还是该拥有, std::borrow::Cow
类型(用于“写入时克隆”,clone on write 的缩写)提供了一种兼顾两者的方式。
enum Cow<'a, B: ?Sized> where B: ToOwned
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
Cow<B>
要么借入对 B 的共享引用,要么拥有可供借入此类引用的值。由于 Cow 实现了 Deref
,因此你可以像对 B 的共享引用一样调用它的方法:
- 如果它是 Owned,就会借入对拥有值的共享引用;
- 如果它是 Borrowed,就会转让自己持有的引用。
还可以通过调用 Cow 的to_mut
方法来获取对 Cow 值的可变引用,这个方法会 返回 &mut B。如果 Cow 恰好是 Cow::Borrowed,那么 to_mut 只需调用引用的 to_owned
方法来获取其引用目标的副本,将 Cow 更改为 Cow::Owned, 并借入对新创建的这个拥有型值的可变引用即可【复制一份再抛出一个可变引用】。这就是此类型名称所指的“写入时克隆”行为。
Cow 还有一个into_owned
方法,该方法会在必要时提升对所拥有值的引用并返回此引用,这会将所有权转移给调用者并在此过程中消耗掉 Cow。
Cow 的一个常见用途是返回静态分配的字符串常量或由计算得来的字符串。假设你需要将错误枚举转换为错误消息。大多数变体可以用固定字符串来处理,但有些也需要在消息中包含附加数据。你可以返回一个 Cow<'static, str>:
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
实现来构造出值。此 match 语句的大多数分支会 返回 Cow::Borrowed
来引用静态分配的字符串。但是当我们得到一个 FileNotFound 变体时,会使用 format! 来构建包含给定文件名的消息。 match 语句的这个分支会生成一个Cow::Owned
值。
如果 describe 的调用者不打算更改值,就可以直接把此 Cow 看作 &str
: println!("Disaster has struck: {}", describe(&error));
如果调用者确实需要一个拥有型的值,那么也能很容易地生成一个: let mut log: Vec = Vec::new(); ... log.push(describe(&error).into_owned());
使用 Cow,describe 及其调用者可以把分配的时机推迟到有必要的时候。