最近在设计Rust库API的时候遇到了一个类似函数重载
带来的恐惧感,特意来分享一下。
背景
为了使得我的API使用起来很灵活,我设计了这么一个struct和trait,函数参数类型标记为impl Trait
,可以传递多种类型的参数,达到一种函数重载
的效果。
代码
函数原型:
fn post_prompt(prompt: impl TryIntoPrompt) {
// ...
}
定义struct和trait:
pub struct Prompt(pub(crate) serde_json::Value);
pub trait TryIntoPrompt {
fn try_into_prompt(self) -> ClientResult<Prompt>;
}
实现trait:
impl TryIntoPrompt for &str {
fn try_into_prompt(self) -> ClientResult<Prompt> {
Ok(Prompt(serde_json::from_str(self)?))
}
}
impl<T: Serialize> TryIntoPrompt for &T {
fn try_into_prompt(self) -> ClientResult<Prompt> {
Ok(Prompt(serde_json::to_value(self)?))
}
}
问题
编译没有报错,感觉设计得很完美,但是cargo test
却失败了,我还以为是啥偶发性原因,重跑了几遍,都是一样的结果。
测试代码:
let prompt = "...".to_string();
post_prompt(&prompt);
原因
经过我的一番研究,我发现跟我幻想中的dispatch发生了冲突。我本来是期望上述代码能分发到impl TryIntoPrompt for &str
,结果却分发到了impl<T: Serialize> TryIntoPrompt for &T
,原来prompt的类型不是&str
而是&String
。
首先serde::Serialize
分别为str
和String
做了实现:
impl Serialize for String;
impl Serialize for str;
按理来说,impl TryIntoPrompt for &str
跟impl<T: Serialize> TryIntoPrompt for &T
应该是冲突的。为啥会编译通过?
因为我试图加上impl TryIntoPrompt for &String
,如无意外的编译报错了。
解惑
问了一下ChatGPT,我才醒悟过来:
为什么
impl TryIntoPrompt for &str
和impl<T: Serialize> TryIntoPrompt for &T
不冲突?1.
impl TryIntoPrompt for &str
它是一个具体实现,直接适用于&str
类型。2.
impl<T: Serialize> TryIntoPrompt for &T
这是一个泛型实现,适用于任何实现了Serialize
的&T
。你可能认为
str
符合T: Serialize
,所以&str
也符合&T: Serialize
,从而产生了冲突。但这里有一个关键点:
T
不能是str
。因为
str
是一个动态大小类型(DST),不能直接用作泛型参数T
,除非用T: ?Sized
这样显式放宽约束。而impl<T: Serialize> TryIntoPrompt for &T
中的T
默认是Sized
的,因此T
不能是str
,导致&T
不能是&str
。如何制造冲突? 如果你允许
T
是?Sized
,那么T
就可以是str
了,这样就会产生冲突。例如:impl<T: Serialize + ?Sized> TryIntoPrompt for &T { fn try_into_prompt(self) -> ClientResult<Prompt> { Ok(Prompt(serde_json::to_value(self)?)) } }
这时候,
T = str
也是合法的,那&T = &str
,就和impl TryIntoPrompt for &str
直接冲突了。
说些什么吧!