【2025 Rust学习 --- 12 实用工具特型02】

news2025/1/10 8:50:11

实用特型2

as引用特型Deref与DerefMut

DerefDerefMut,它们允许自定义类型的实例能够像引用一样被解引用(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);
}

区别

  1. 访问权限

    • Deref 只提供对内部数据的不可变访问。
    • DerefMut 提供对内部数据的可变访问,这意味着它可以用来改变内部数据。
  2. 实现要求

    • 要实现 DerefMut,你的类型必须先实现 Deref,因为 DerefMut 是基于 Deref 的扩展。
  3. 应用场景

    • 如果你需要一个智能指针或者其他包装类型可以透明地当作其内部类型使用,那么你可以只实现 Deref
    • 如果你还希望这个包装类型能够提供对其内部数据的可变访问,那么你也应该实现 DerefMut
  4. 注意事项

    • 当实现 DerefDerefMut 时,确保遵循 Rust 的借用规则和所有权规则,以避免潜在的安全问题或未定义行为。

DerefDerefMut 允许你创建更直观和方便使用的智能指针和其他包装类型

通过实现 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

AsRefAsMut 用于实例到引用的类型转换的 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> 的任何类型,这意味着它可以接受 &strStringPathBuf 等多种类型作为参数。

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());
}

区别

  1. 访问权限

    • AsRef 只提供对目标类型的不可变访问。
    • AsMut 提供对目标类型的可变访问,这意味着它允许修改数据。
  2. 实现要求

    • AsMut 并不要求必须先实现 AsRef,但是两者通常一起实现,因为它们服务于相似的目的——即提供不同级别的访问权限到同一个内部数据。
  3. 应用场景

    • 如果你需要一个类型能够被当作其他类型的不可变引用使用,你应该实现 AsRef
    • 如果你还希望这个类型能够提供对其内部数据的可变访问,那么你也应该实现 AsMut
  4. 灵活性与泛型编程

    • 这两个 trait 在编写泛型代码时特别有用,尤其是在你需要处理多种可能的输入类型时,但这些类型最终都可以转换成同一种目标类型。

总之,AsRefAsMut 提供了一种方便的方式来进行类型转换,同时保持 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 看作 &strprintln!("Disaster has struck: {}", describe(&error));

如果调用者确实需要一个拥有型的值,那么也能很容易地生成一个: let mut log: Vec = Vec::new(); ... log.push(describe(&error).into_owned());使用 Cow,describe 及其调用者可以把分配的时机推迟到有必要的时候。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2274248.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

HTML5实现好看的端午节网页源码

HTML5实现好看的端午节网页源码 前言一、设计来源1.1 网站首页界面1.2 登录注册界面1.3 端午节由来界面1.4 端午节习俗界面1.5 端午节文化界面1.6 端午节美食界面1.7 端午节故事界面1.8 端午节民谣界面1.9 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 H…

git merge与rebase区别以及实际应用

在 Git 中&#xff0c;merge 和 rebase 是两种将分支的更改合并到一起的常用方法。虽然它们都可以实现类似的目标&#xff0c;但它们的工作方式和效果有所不同。 1. Git Merge 定义&#xff1a;git merge 是将两个分支的历史合并在一起的一种操作。当你执行 git merge 时&…

Matlab APP Designer

我想给聚类的代码加一个图形化界面&#xff0c;需要输入一些数据和一些参数并输出聚类后的图像和一些评价指标的值。 gpt说 可以用 app designer 界面元素设计 在 设计视图 中直接拖动即可 如图1&#xff0c;我拖进去一个 按钮 &#xff0c;图2 红色部分 出现一行 Button 图…

PyCharm 引用其他路径下的文件报错 ModuleNotFound 或报红

PyCharm 中引用其他路径下的文件提示 ModuleNotFound&#xff0c;将被引用目录添加到系统路径&#xff1a; # # 获取当前目录 dir_path os.path.dirname(os.path.realpath(__file__)) # # 获取上级目录 parent_dir_path os.path.abspath(os.path.join(dir_path, os.pardir))…

【HarmonyOS NEXT】鸿蒙应用点9图的处理(draw9patch)

【HarmonyOS NEXT】鸿蒙应用点9图的处理&#xff08;draw9patch&#xff09; 一、前言&#xff1a; 首先在鸿蒙中是不支持安卓 .9图的图片直接使用。只有类似拉伸的处理方案&#xff0c;鸿蒙提供的Image组件有与点九图相同功能的API设置。 可以通过设置resizable属性来设置R…

SOLID原则学习,开闭原则

文章目录 1. 定义2. 开闭原则的详细解释3. 实现开闭原则的方法4. 总结 1. 定义 开闭原则&#xff08;Open-Closed Principle&#xff0c;OCP&#xff09;是面向对象设计中的五大原则&#xff08;SOLID&#xff09;之一&#xff0c;由Bertrand Meyer提出。开闭原则的核心思想是…

【Vue3中使用crypto-js】crypto-js加密解密用法

目录 1、安装crypto2、创建crypto.js文件3、在main.js主文件中进行引用4、页面中进行使用5、实现效果展示6、加密模式解析以及iv参数使用 1、安装crypto npm install crypto-js 如果是在Typescript版本需要再安装 npm install --save types/crypto-js2、创建crypto.js文件 注…

跨界融合:人工智能与区块链如何重新定义数据安全?

引言&#xff1a;数据安全的挑战与现状 在信息化驱动的数字化时代&#xff0c;数据已成为企业和个人最重要的资产之一。然而&#xff0c;随着网络技术的逐步优化和数据量的爆发式增长&#xff0c;数据安全问题也愈变突出。 数据安全现状&#xff1a;– 数据泄露驱动相关事件驱…

简单易用的PDF工具箱

软件介绍 PDF24 Creator是一款简单易用的PDF工具箱&#xff0c;而且完全免费&#xff0c;没有任何功能限制。既可以访问官网在线使用各种PDF工具&#xff0c;也可以下载软件离线使用各种PDF工具。 软件功能 1、PDF转换 支持将多种文件格式&#xff08;Word、PowerPoint、Exc…

低秩信息收集_0109

系列博客目录 文章目录 系列博客目录LoRA: Low-Rank Adaptation of Large Language Models传统模型适配的局限性&#xff1a;尽管研究界致力于通过添加适配器层或优化输入层激活来提高模型适配效率&#xff0c;这些方法在大型模型和延迟敏感的环境中存在局限。适配器层尽管参数…

C语言与ASCII码应用之简单加密

加密是什么&#xff1f;什么是加密通话&#xff1f;用人话说就是一句有含义的话&#xff0c;经过一定的特殊规则把里面的每个字按照这个规则进行改变&#xff0c;但是这个规则只有你和你想让知道这条信息的人知道 今天我们来用ASCII码编写一个简单加密与解密的程序&#xff0c…

国产3D CAD将逐步取代国外软件

在工业软件的关键领域&#xff0c;计算机辅助设计&#xff08;CAD&#xff09;软件对于制造业的重要性不言而喻。近年来&#xff0c;国产 CAD 的发展态势迅猛&#xff0c;展现出巨大的潜力与机遇&#xff0c;正逐步改变着 CAD 市场长期由国外软件主导的格局。 国产CAD发展现状 …

【Linux网络编程】第二十二弹---深入理解 I/O 多路转接之 epoll:系统调用、工作原理、代码演示及应用场景

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【Linux网络编程】 目录 1、I/O 多路转接之 epoll 1.1、epoll 初识 1.2、epoll 的相关系统调用 1.2.1、epoll_create 1.2.2、epol…

品牌账号矩阵如何打造?来抄作业

在讲究全域营销的当下&#xff0c;目前企业都在各自搭建品牌矩阵号&#xff0c;以提升自己在不同渠道上的影响力。虽然不同平台之间有诸多细节值得深究&#xff0c;但也不妨碍我们先了解如何搭建品牌矩阵。接下来&#xff0c;就让我们一同来了解下该如何搭建。 一、一个主账号 …

备考蓝桥杯:数据结构概念浅谈

目录 1数据结构的概念 什么是数据结构: 为什么要有数据结构 2.数据结构的三个组成要素 1.逻辑结构 2.存储结构 3.数据运算 3。算法好坏的度量&#xff08;时间复杂度和空间复杂度&#xff09; 时间复杂度计算 最优和平均和最差时间复杂度 计算时间复杂度例子 空间复…

scala代码打包配置(maven)

目录 mavenpom.xml打包配置项&#xff08;非完整版&#xff0c;仅含打包的内容< build>&#xff09;pom.xml完整示例&#xff08;需要修改参数&#xff09;效果说明 maven 最主要的方式还是maven进行打包&#xff0c;也好进行配置项的管理 以下为pom文件&#xff08;不要…

用于 EV 牵引电机的先进冷却技术

电动汽车牵引电机的冷却挑战 热管理的重要性 有效的热管理在电动汽车 &#xff08;EV&#xff09; 设计中至关重要&#xff0c;尤其是在牵引电机方面。这些电机将电能转化为机械运动&#xff0c;对车辆的整体性能和效率至关重要。 管理它们的热量至关重要&#xff0c;不仅可以…

课题推荐——基于GPS的无人机自主着陆系统设计

关于“基于GPS的无人机自主着陆系统设计”的详细展开&#xff0c;包括项目背景、具体内容、实施步骤和创新点。如需帮助&#xff0c;或有导航、定位滤波相关的代码定制需求&#xff0c;请点击文末卡片联系作者 文章目录 项目背景具体内容实施步骤相关例程MATLAB例程python例程 …

毕业项目推荐:基于yolov8/yolov5/yolo11的动物检测识别系统(python+卷积神经网络)

文章目录 概要一、整体资源介绍技术要点功能展示&#xff1a;功能1 支持单张图片识别功能2 支持遍历文件夹识别功能3 支持识别视频文件功能4 支持摄像头识别功能5 支持结果文件导出&#xff08;xls格式&#xff09;功能6 支持切换检测到的目标查看 二、数据集三、算法介绍1. YO…

OpenLDAP 进阶指南:复制、引用与别名配置详解

文章目录 1.复制和引用概述2.LDAP 引用3.LDAP 复制4.OpenLDAP 复制4.1 OpenLDAP syncrepl方式复制(从2.3版本开始)4.2 Replication refreshAndPersist (provider Push)4.3 OpenLDAP syncrepl (N-Way) 多主复制 前言 本章提供了关于配置LDAP系统进行复制(replication)、引用(ref…