为什么不自动使用`From`来强制转换到实现trait的类型?

4

我有两个特点:

trait Foo {}
trait Bar {}

struct FooImpl;
impl Foo for FooImpl {}

struct BarImpl;
impl Bar for BarImpl {}

我希望你能将第三种类型转换成:

struct Baz;

trait IntoBaz {
    fn into(self) -> Baz;
}

由于一致性,我无法为这两个特质定义两个IntoBazimpl,因此我将其中一个进行包装:

struct FooWrapper<F>(F)
where
    F: Sized;

impl<F: Foo + Sized> From<F> for FooWrapper<F> {
    fn from(f: F) -> FooWrapper<F> {
        FooWrapper(f)
    }
}

impl<F: Foo + Sized> IntoBaz for FooWrapper<F> {
    fn into(self) -> Baz {
        Baz
    }
}

而我不包装其他:
impl<B: Bar> IntoBaz for B {
    fn into(self) -> Baz {
        Baz
    }
}

fn do_thing<B: IntoBaz>(b: &B) {}

fn main() {
    do_thing(&BarImpl);
}

到目前为止都很好,但是为什么这行代码不起作用?
fn main() {
    do_thing(&FooImpl);
}

动机

我正在尝试为一个支持fmt::Write的库添加io::Write支持,而不引入破坏性更改。

最简单的方式是定义一些内部的Write特征来涵盖共享的行为,但一致性问题意味着我不能仅仅写入From<io::Write>实例到内部特征中。

我已经尝试了包装io::Write实例,使得强制转换明确,以便编译器优先选择更短的路径并避免不一致性,但它不会使用From实例进行自动强制转换。


错误显示 FooImpl 没有实现 Bar 接口(而这是你的 IntoBaz 实现所必需的)。难道你的本意不是进行如下操作吗:do_thing(&FooWrapper(FooImpl)); - Peter Hall
不行,因为目标是让自动强制转换来完成这个任务 - 我不明白为什么在这种情况下它没有起作用。 - Isaac van Bakel
2
from() or into() don't just get automagically called. You would need to do something like: do_thing(&FooWrapper::from(FooImpl)); - Peter Hall
获取用户定义的自动强制转换的唯一合理方法是在某个包装类型上实现 Deref,这将导致包装器自动衰减为包装的类型。 您可以在 Nomicon 中查看所有强制转换的完整列表:https://doc.rust-lang.org/nomicon/coercions.html。 - Sven Marnach
from() 有时会被语言自动调用 - 我希望避免在调用现场手动使用包装器。对我来说,似乎应该有一种方法可以使用 From 或类似的东西来解决这个协同问题。 - Isaac van Bakel
1
from() 有时会被语言自动调用” — 它可能看起来可以与某些库一起使用,但这只是那些库需要参数为 Into<T> 并显式调用 .into(),就像 do_thing 的主体一样。 - Peter Hall
2个回答

6

请看以下错误信息:

error[E0277]: the trait bound `FooImpl: Bar` is not satisfied
  --> src/main.rs:48:5
   |
48 |     do_thing(&FooImpl);
   |     ^^^^^^^^ the trait `Bar` is not implemented for `FooImpl`
   |
   = note: required because of the requirements on the impl of `IntoBaz` for `FooImpl`
note: required by `do_thing`
  --> src/main.rs:45:1
   |
45 | fn do_thing<B: IntoBaz>(b: &B) {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

它的意思是FooImpl没有实现所需的Bar,这是你的全局实现B到IntoBaz 的要求。与此相关的实现FooWrapper不重要,因为FooImplFooWrapper不同。从FromInto特征提供了一种在类型之间进行转换的方式,但这不能自动完成。尝试添加能够转换为FooWrapper的实现可能会有用,但这样做无法正常工作(并且专业化尚未稳定)。但是,你可以仅对FooImpl定义一个 IntoBaz 实现:
impl IntoBaz for FooImpl {
    fn into(self) -> Baz {
        IntoBaz::into(FooWrapper::from(self))
    }
}

这将使您的代码编译:

fn main() {
    do_thing(&BarImpl);
    do_thing(&FooImpl);
}

我可以为 FooImpl 这样做,但这并不能解决我在动机中解释的一般问题,这需要等价于两个 blanket-impl。我试图通过让类型检查器选择实例(基本上是通过最短的强制转换距离到目标类型)来避免重叠。 - Isaac van Bakel

3

PeterHall的答案完全正确,与所提问的问题相符。在类型层面上,FromInto并没有任何特殊的含义。

然而,您可能把问题问得太过狭隘了。看起来您想要 do_thing(&BarImpl)do_thing(&FooImpl) 编译并做出“正确”的事情。如果只需要这些,那么有一个有点棘手的替代方法可以解决:向 IntoBaz 添加一个类型参数,并使用不同的类型使 impl 不重叠。

trait IntoBaz<T> {
    fn into_baz(self) -> Baz;
}

struct ByFoo;
impl<F: Foo> IntoBaz<ByFoo> for F {
    fn into_baz(self) -> Baz {
        Baz
    }
}

struct ByBar;
impl<B: Bar> IntoBaz<ByBar> for B {
    fn into_baz(self) -> Baz {
        Baz
    }
}

do_thing现在可以针对T进行通用操作:

fn do_thing<T, B: IntoBaz<T>>(_: &B) {}

当您调用它时,如果只有一个有效的T,编译器会自动找到它。
fn main() {
    do_thing(&BarImpl);
    do_thing(&FooImpl);
}

我正在尝试为支持fmt :: Write 的库添加io :: Write支持,但不引入破坏性变化。

不幸的是,这个建议在技术上是一种破坏性变化。如果有一些类型同时实现了io::Writefmt::Write,那么以前使用fmt::Writedo_thing(&implements_both)现在将由于歧义而无法编译。但是任何选择trait不含歧义的地方仍然会像以前一样编译,因此破坏的风险要小得多。

另请参阅:


感谢您提出这个有趣的建议。我尝试了类似的方法,但是使用了 B: Into<I> 后跟自定义的 I: IntoBaz,但类型检查器在类型推断中没有使用孤儿规则,所以不幸的是它没有起作用。当然,这只是为了避免编写 <method_name>_io 而带来了很多麻烦,所以我最终可能会选择编写它。 - Isaac van Bakel

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