为用户定义的类型实现 ToOwned

Implement ToOwned for user-defined types

考虑以下示例代码:

#[derive(Clone)]
struct DataRef<'a> {
    text: &'a str,
}

#[derive(Clone)]
struct DataOwned {
    text: String,
}

我将以这种方式为 DataRef 实施 ToOwned

impl ToOwned for DataRef<'_> {
    type Owned = DataOwned;

    fn to_owned(&self) -> DataOwned {
        DataOwned {
            text: self.text.to_owned(),
        }
    }
}

从字面上看,这很有道理吧?但是也有一些问题。


第一个问题是,因为 ToOwned 提供了 blanket implementation:

impl<T> ToOwned for T where T: Clone { ... }

以上代码会出现编译错误:

error[E0119]: conflicting implementations of trait `std::borrow::ToOwned` for type `DataRef<'_>`
  --> src/main.rs:13:1
   |
13 | impl ToOwned for DataRef<'_> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `alloc`:
           - impl<T> ToOwned for T
             where T: Clone;

好吧,我们可以做出一些妥协。让我们从 DataRef 中删除 #[derive(Clone)]。 (然而,在我的真实案例中我不能这样做,因为这是一个破坏性的变化并且没有意义)


然后是第二个问题,ToOwnedrequires that it implements Borrow<Self>的关联类型Owned

pub trait ToOwned {
    type Owned: Borrow<Self>;

    fn to_owned(&self) -> Self::Owned;

    ...
}

如果我们按照定义为 DataOwned 实现 Borrow

impl<'a> Borrow<DataRef<'a>> for DataOwned {
    fn borrow(&self) -> &DataRef<'a> {
        DataRef { text: &self.text }
    }
}

这显然是不可能的,因为我们不能在某处存储一个DataRef


所以我的问题是:

您看到的问题是因为 ToOwned 不应该被实施 引用类型 而是 referent。 注意 the standard library implementations 的样子:

impl ToOwned for str
impl ToOwned for CStr
impl ToOwned for OsStr
impl ToOwned for Path
impl<T> ToOwned for [T]

这些都是 !Sized, !Clone 类型,它们总是出现在某些通用指针类型(例如 &strBox<str>&Path)或专用指针类型(String包含一个strPathBuf包含一个PathVec<T>包含一个[T])。 ToOwned 的目的是允许从 reference 转换为数据——不是你所谓的 FooRef 而是实际的 & —— 到专门的拥有指针类型,这样转换是 可逆和一致的 (这就是 Borrow<Self> 的意义所在)。

如果您想获得 BorrowToOwned 的好处,您需要定义一个不是引用但引用可以指向的类型,如下所示:

use std::borrow::Borrow;
#[repr(transparent)]
struct Data {
    text: str,
}

#[derive(Clone)]
struct DataOwned {
    text: String,
}

impl Borrow<Data> for DataOwned {
    fn borrow<'s>(&'s self) -> &'s Data {
        // Use unsafe code to change type of referent.
        // Safety: `Data` is a `repr(transparent)` wrapper around `str`.
        let ptr = &*self.text as *const str as *const Data;
        unsafe { &*ptr }
    }
}

impl ToOwned for Data {
    type Owned = DataOwned;
    fn to_owned(&self) -> DataOwned {
        DataOwned { text: String::from(&self.text) }
    }
}

但是,请注意此策略 适用于单个连续数据块(例如 str 中的 UTF-8 字节)。无法以适用于包含 两个 字符串的 DataOwned 的方式实现 Borrow + ToOwned。 (它可以对一个字符串和一些 fixed-sized 数据完成,但这仍然具有挑战性,因为自定义 dynamically-sized 类型还不是很 well-supported Rust。)

通常不值得为一个 String 包装器做所有这些,但如果你想对内容强制执行一些更强的 type/validity 不变性(例如“所有字符都是ASCII”或“字符串(或字符串切片)是 well-formed JSON 片段”), 您希望能够与现有的通用代码进行交互期望 ToOwned 得到实施。

如果您只想能够在 DataRef 上调用 .to_owned() 方法,请不要理会 ToOwned 特征;只需编写一个固有的(non-trait)方法。