为什么'?'运算符使用From而不是Into?

3
假设有一个外部模块(由第三方库提供):
pub mod external_module {
    #[derive(Debug)]
    pub struct ExternalError;

    pub trait SomeTrait {
        fn do_stuff(&self) -> Result<i32, ExternalError>;
    }
}

我需要在我的代码中实现SomeTrait。我的代码可能会遇到一些内部错误,因此我定义了自己的错误类型并使其可转换成ExternalError

struct MyError;

impl Into<ExternalError> for MyError {
    fn into(self) -> external_module::ExternalError {
        ExternalError {}
    }
}

请注意,我无法实现 from<MyError> for ExternalError,因为 ExternalError 是在我的命名空间之外定义的(实际上甚至在我的板条箱之外)。
现在我要实现 SomeTrait:
fn do_my_things() -> Result<(), MyError> {
    Ok(())
}

struct MyStruct;

impl SomeTrait for MyStruct {
    fn do_stuff(&self) -> Result<i32, ExternalError> {
        do_my_things()?;
        Ok(200)
    }
}

点击此处查看完整代码示例,但是这段代码无法编译:

error[E0277]: `?` couldn't convert the error to `external_module::ExternalError`
  --> src/main.rs:28:23
   |
28 |         do_my_things()?;
   |                       ^ the trait `std::convert::From<MyError>` is not implemented for `external_module::ExternalError`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = note: required by `std::convert::From::from`

为了使其编译,我可以明确地使用into():
    do_my_things().map_err(|e| e.into())?;

Playground。这里有两个问题:

  1. 为什么“?”操作符使用From而不是Into?后者似乎更合理,因为它会更加宽容:From<A> for B意味着Into<B> for A,但反之不成立。
  2. 在我特定的情况下,处理错误转换的最佳方法是什么?编写.map_err(|e| e.into())看起来非常冗长(有时我甚至不得不在闭包中显式指定类型,因为编译器无法推断它们!)

一些背景信息:当我使用tonic库时遇到了这种情况。该库从.proto文件生成trait,然后希望您实现它们(请参见基本的helloworld示例)。如果失败,则需要返回Err(tonic::Status),它基本上包含GRPC错误代码和错误消息。我不希望我的内部错误类型与GRPC错误代码有任何关系,我只想在我的错误转换为tonic::Status时添加它们。


有趣的是,RFC确实使用了Into,但是还有一个关于使用正确特质的部分,包括FromInto或其他内容。我找不到任何关于这个问题解决的参考资料,但是最初的实现最终使用了From - mcarton
1个回答

4
is_ok!宏最初是没有隐式错误转换的实现。该宏被重命名try!,当随后实现错误转换时,他们选择使用FromError特质,这个特质后来被更通用的转换特质From<E>取代,这发生在大约6个月后实现

正如 @mcarton 所提到的,“?”操作符的RFC 使用了 Into trait 进行错误转换。它还说:“后缀 ? 操作符可应用于 Result 值,并等同于当前的 try!() 宏。”然而,正如 stbuehler 在 这条评论 中指出的那样:

遗憾的是,会有一些退化情况。如果没有针对错误类型实现(非平凡的)From<...> 实例,则编译器可以在某些情况下推断类型,在使用 Into 时无法推断类型(当前创建的 From 实例集受限,完整的 Into 实例集在编译创建时未知)。

因此,我对此的解释是,在FromInto特征存在之前,try!宏添加了错误转换支持,因为?运算符应该与try!宏具有相同的行为,而且更改为Into将破坏类型推断,因此使用From实现了?运算符。

话虽如此,只要MyType是你的crate中的类型,你就可以实现From<MyType> for ForeignType

struct MyError;

impl From<MyError> for ExternalError {
    fn from(err: MyError) -> external_module::ExternalError {
        ExternalError {}
    }
}

在游乐场中的示例


2
哇,我以为 Rust 不允许在外部类型上实现外部 trait。From 是这个规则的“特例”还是我漏掉了什么? - kreo
@kreo 我其实不确定它为什么能够工作,我认为一般规则仍然是不允许这样做的(例如 impl<T> From<MyType<T>> for ForeignType<T> 将不起作用),但有时似乎仍然被允许。不过这似乎并不特别适用于 From,因为你也可以使用 AsRef 等相同的方法。 - Frxstrem

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