为用户定义的类型实现ToOwned

8
考虑以下示例代码:
#[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提供了整体实现
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)]。(但是,在我的实际情况下,我无法这样做,因为它是一个破坏性的变化,没有意义)

然后是第二个问题,ToOwned 的相关类型 Owned 要求 它实现了 Borrow<Self>

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

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

    ...
}

如果我们按照定义实现DataOwnedBorrow
impl<'a> Borrow<DataRef<'a>> for DataOwned {
    fn borrow(&self) -> &DataRef<'a> {
        DataRef { text: &self.text }
    }
}

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

所以我的问题是:

  • 有没有办法为上面的例子实现ToOwned

  • 考虑到上述问题,ToOwned是否不应该由用户手动实现?因为我无法想象一个真实的例子来反对这一点。

  • (可选回答) 如果允许更改std ToOwned的定义,是否有可能进行改进以使其更好?(允许不稳定和未实现的Rust功能)


borrow::ToOwnedborrow:Cow<T>密切相关,需要T: ToOwned。对于Cow,有PartialEqOrd等的实现,这就是Borrow要求的来源:因为Borrow保证了拥有和借用变体的行为相同,所以Cow可以将其自身进行比较和排序,无论值是借用还是拥有变体。Cow之所以有效,是因为ToOwnedBorrow使得从一个变体无缝转换到另一个变体成为可能。您能详细说明一下为什么要实现ToOwned吗? - user2722968
@user2722968 感谢您指出它与 Cow 有关。我只想让我的类型健壮且用户友好,而 to_owned 是除 into 外的另一个选项。我认为实现 ToOwned 是有意义的,这在某种程度上类似于为 str 和 Path 实现它(尽管我知道这些是不定长的)。 - Sprite
你的最后一个问题是“假设我可以更改std”,还是“如果我想编写自己的特质(trait)”? - Chayim Friedman
@ChayimFriedman 这句话的意思是“假设您可以更改std”。 - Sprite
1个回答

11
你看到的问题是因为ToOwned不应该为一个引用类型实现,而应该为一个引用对象实现。请注意标准库中的实现:链接
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 的目的是允许将数据的引用 - 不是像 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`。(对于一个字符串和一些固定大小的数据,这可能是可行的,但这仍然很具挑战性,因为Rust尚未很好地支持自定义动态大小的类型。)

如果你只是想在一个`DataRef`上调用`.to_owned()`方法,那就不要费心去实现`ToOwned`特质了,只需编写一个内部(非特质)方法即可。

对于一个String包装器来说,做所有这些工作通常都不值得,但如果你想强制执行一些更强的类型/有效性约束条件(例如“所有字符都是ASCII”或“字符串(或字符串片段)是一个格式良好的JSON片段”),并且想要与期望实现`ToOwned`的现有通用代码进行交互,那么这可能是值得的。


String包装器只是一个例子,在我的实际情况中,这个结构包含更多的字段。我会考虑你最后的建议。感谢详细的解释,给你点赞。 - Sprite

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接