>我应该写:
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
,实现它从String
s转换。如果你有一个&str
,实现它从&str
s转换。如果两者都有,请为两者实现它。
是否有从
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
怎么办。这里有两个子案例:
- 调用方想要挂在他们的 Src 副本上。
- 调用方可以将所有权移交给 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