应该 Rust 实现 From/TryFrom 目标引用或值



>我应该写:

impl<'a> From<&'a Type> for OtherType

还是应该是

impl From<Type> for OtherType

我很难找到答案,也许是由于我的词汇失败。我真的不是特别关心论点的参考性/价值性。

在C++中,我将在值上定义函数 over/方法,并在常量引用上调用它。

是否有从impl Trait<Type>impl<'a> Trait<&'a Type>的自动推导?

Rust 应该实现 From/TryFrom 的目标引用或值吗?

是的,他们应该。

这里没有诡计:实现特征来转换您拥有的任何类型。如果你有一个String,实现它从Strings转换。如果你有一个&str,实现它从&strs转换。如果两者都有,请为两者实现它。

是否有从impl Trait<Type>impl<'a> Trait<&'a Type>的自动派生?

不,而且有充分的理由。例如,考虑以下转换:

struct Filename(String);
impl From<String> for Filename {
fn from(value: String) -> Filename {
Filename(value)
}
}

编译器没有明显正确的方法来实现对String的引用。但是,您可以自己实现它:

impl<'a> From<&'a str> for Filename {
fn from(value: &'a str) -> Filename {
String::into(value.to_owned())
}
}

如果您不能使用传入的分配,那么没有太多理由按值接受参数,因此您不妨接受引用。但是,我想说的是,使用From进行此类转换并不常见——不过,这并非闻所未闻。

在以下情况下,我会说"只实现From<Src>,而不是From<&Src>":

// These types are NOT Copy! (Nor should they be.)
#[derive(Clone, ...)]
struct Src {
v: Vec<u8>,
}
#[derive(Clone, ...)]
struct Dst {
v: Vec<u8>,
}

我的理由:假设某人有&Src,他们想要一个Dst。无需From<&Src>即可轻松完成:

Dst::from(src_ref.clone())

您可能会说,"但是您正在强迫用户克隆!没错,但无论如何您都需要在某个时候克隆:

impl From<&Src> for Dst {
fn from(src: &Src) -> Dst {
Dst {
v: src.v.clone(),
}
}
}

或者

impl From<&Src> for Dst {
fn from(src: &Src) -> Dst {
Dst::from(src.clone())
}
}

无论哪种方式,您都没有避免克隆。

Thm 1:如果你有一个&Src并且你想做一个Dst,你将总是在某个时候克隆。实施From<&Src>并不能使您免于这种必然性。

同时,您几乎复制了代码。回顾你的From<Src>

impl From<Src> for Dst {
fn from(src: Src) -> Dst {
Dst {
v: src.v,
}
}
}

看起来和From<&Src>的第一个 impl 真的很相似,对吧?

如果我们反过来做(即根据From<&Src>实现From<Src>),那么我们将始终克隆(这只是thm 1的推论)。

然而,如果我们只实现From<Src>,调用方可能需要克隆,但他们也可能有机会移交Src的所有权,这将导致只是一个移动而不是克隆。

好的,但是如果调用者有Src,而不是&Src怎么办。这里有两个子案例:

  1. 调用方想要挂在他们的 Src 副本上。
  2. 调用方可以将所有权移交给 Dst::from。

当然,在后一种情况下,他们应该使用我们的From<Src>.

但是案例 1 呢?如果我们坚持我的建议,似乎我们再次强迫调用者克隆:

Dst::from(src.clone())

如果我们能做到就好了

Dst::from(&src)

!但回顾一下thm 1:我们实际上并没有避免克隆。我们只是让From<&Src>为我们做克隆的肮脏工作。可以这么说,我们所做的只是将克隆扫到地毯下。

我认为这涵盖了所有(有趣的)案例。我并没有真正涵盖 Src 和 Dst 是 Copy 的情况,因为这并不真正"有趣"。这"不有趣",因为没有昂贵的克隆可以摆脱。您可能唯一需要做的就是使用取消引用运算符:

Dst::from(*src_ref);

但这不是性能问题。这样做的唯一"费用"是再输入一个*字符,这真的不会杀死你。

我的建议的主要"问题"是人们会看着你的克隆并想,"我知道如何摆脱它!我就加impl From<&Src>!但事实证明,这是一件愚蠢的差事。

扩展@allyourcode写的非常出色的答案,特别是涵盖From<&SrcType>不会进行隐式复制或克隆的情况:如果目标是构建一个引用位于SrcType中的字段的DstType(即DstType提供SrcType)的视图,例如,使用以下新类型:

struct EmailAddr(String);    // e.g. user@example.com
struct Domain<'a>(&'a str);  // a view onwards from the @ symbol of the above

要构建一个从EmailAddr到视图Domain<'a>impl,可以做这样的事情(诚然,这是一个相当人为的例子,为了简单起见,对电子邮件地址的结构做出了错误的假设):

impl<'a> From<&'a EmailAddr> for Domain<'a> {
fn from(value: &'a EmailAddr) -> Self {
Self(&value.0[value.0.find('@').unwrap_or(0)..])
}
}

然后可以执行以下操作来演示上述工作原理(游乐场链接):

let email = EmailAddr("user@example.com".to_string());
let domain = Domain::from(&email);

为了进一步扩展上面的例子,如果有一个函数会消耗这样的Domain

fn consume_domain<'a>(domain: Domain<'a>) {
println!("domain {:?} consumed", domain);
}

上述函数可以通过使用带有EmailAddr引用的.into()生成Domain来调用,如下所示:

consume_domain((&email).into());

请注意值(&email)两边的括号 - 这是为了确保这是值调用.into()的引用,否则它被解释为将整个EmailAddr转换为目标类型,然后获取该类型的引用。 是的,这可能感觉很笨拙,但对于消除这两种不同解释的歧义是必要的。

对于更实际的示例,可能有一个 newtype,它封装了简单地址簿的名称和电子邮件地址的 2 元组向量。 为了便于进行大量查找,一个好方法是通过引用(指向)位于Vec<(String, String)>中的数据来创建该数据HashMap。 将矢量中的所有Strings克隆到映射中始终是可能的,但这样做将不仅仅是重复的内存使用(或者在提供反向查找映射的情况下更糟;实际上最好不要克隆潜在的长字符串)。 例如:

struct ListOfUsers(Vec<(String, String)>);  // (User, Email)
struct UserToEmail<'a>(HashMap<&'a str, &'a str>);
struct EmailToUser<'a>(HashMap<&'a str, &'a str>);
impl<'a> From<&'a ListOfUsers> for UserToEmail<'a> {
fn from(value: &'a ListOfUsers) -> Self {
Self(value.0.iter()
.map(|(u, e)| (u.as_ref(), e.as_ref()))
.collect::<HashMap<&'_ str, &'_ str>>()
)
}
}
impl<'a> From<&'a ListOfUsers> for EmailToUser<'a> {
// same fn from as above but with user/email swapped
}

从概念上讲,HashMap<&'a str, &'a str>用作Vec<(String, String)>的视图。使用From<&...>创建视图只是巩固了这样一个事实,即制作视图确实涉及某种形式的非视图相关转换(仅仅是因为需要分配保留视图的具体内容,例如HashMap)。

示例用法可能如下所示(在游乐场链接中完成的示例):

let users = ListOfUsers(vec![
("John".into(), "john@example.com".into()),
("Mary".into(), "mary@example.com".into()),
("Takao".into(), "takao@example.com".into()),
("Darcy".into(), "darcy@example.com".into()),
]);
let user2email = UserToEmail::from(&users);
println!("{}", *user2email.0.get("Darcy").unwrap());  // outputs darcy@example.com
let email2user = EmailToUser::from(&users);
println!("{}", *email2user.0.get("mary@example.com").unwrap());  // outputs Mary

最新更新