最近在设计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直接冲突了。


说些什么吧!