为什么我需要为值和引用都实现`From`?方法不应该自动解引用或借用吗?

huangapple go评论80阅读模式
英文:

Why do I need to implement `From` for both a value and a reference? Shouldn't methods be automatically dereferenced or borrowed?

问题

Rust方法调用的行为在The Rust Reference中有描述。它声明“在查找方法调用时,接收者可能会自动解引用或借用以调用方法”。行为在本章的后面更详细地解释了。

在示例中,您可以看到into_charSuit移动,而to_char借用了Suit

  • into_char,它被值和引用调用,引用会自动解引用。

  • to_char,它也被值和引用调用,值会自动借用。

FromSuit的值和引用都实现了。因此,当被值调用时,会调用Into<Suit>::into(),当被引用调用时,会调用Into<&Suit>::into()

但是,我难道不只需要实现其中一个trait吗?当我注释掉其中一个实现时,Rust编译器不会编译...

这个过程似乎是这样的...首先生成一个“候选接收者类型”的列表。这是通过“重复解引用”获得的,并在最后“尝试非大小的强制转换”。此外,对于每个“候选类型T”,在T之后立即添加&T和&mut T。然后,对于这些“候选接收者类型”中的每一个,查找“直接在T上实现的方法”和“由T实现的可见trait提供的方法”。

考虑只为char实现了From<Suit>。然后应该为char实现Into<Suit>

  • 当调用let c: char = value.into();时,“候选接收者类型”应该至少包含Suit&Suit&mut Suit。然后,Into<T>::into()很容易为列表中的第一个项解析。因此,调用Into<Suit>::into()

  • 但是,当调用let f: char = reference.into();时,“候选接收者类型”也应至少包含&Suit&&Suit&mut &Suit*&Suit = Suit&Suit(再次)和&mut Suit。然后,Into<T>::into()无法找到&Suit&&Suit&mut &Suit的实现,但随后找到了*&Suit = Suit的实现。因此,调用Into<Suit>::into()

我的逻辑正确吗?如果我注释掉其中一个From实现,为什么这不起作用?

Rust Playground

#[derive(Clone, Copy)]
pub enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

pub use Suit::*;

impl Suit {
    #[inline(never)]
    pub fn into_char(self) -> char {
        match self {
            Club => 'C',
            Diamond => 'D',
            Heart => 'H',
            Spade => 'S',
        }
    }
    
    #[inline(never)]
    pub fn to_char(&self) -> char {
        match self {
            Club => 'C',
            Diamond => 'D',
            Heart => 'H',
            Spade => 'S',
        }
    }
}

impl std::convert::From<Suit> for char {
    fn from(suit: Suit) -> Self {
        suit.into_char()
    }
}

impl std::convert::From<&Suit> for char {
    fn from(suit: &Suit) -> Self {
        suit.to_char()
    }
}

fn main() {
    let value = Club;
    let reference = &value;
    
    let a: char = value.into_char();
    let b: char = value.to_char();
    let c: char = value.into();
    println!("{}, {}, {}", a, b, c);
    
    let d: char = reference.into_char();
    let e: char = reference.to_char();
    let f: char = reference.into();
    println!("{}, {}, {}", d, e, f);
}

编辑:正如下面讨论的那样,我创建了一个更好的问题重现(https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2b46d12ea13f660bc87ea09e90315daa),它有助于缩小造成此行为的原因。

编辑:我已经为Rust编译器创建了一个GitHub问题(https://github.com/rust-lang/rust/issues/112951)。

英文:

The behaviour of a Rust method call is described in The Rust Reference. It states that "when looking up a method call, the receiver may be automatically dereferenced or borrowed in order to call a method." The behaviour is explained in more detail further into the chapter.

In the example, you can see that into_char moves Suit, while to_char borrows Suit.

  • into_char, it is called by a value and a reference, and the reference is automatically dereferenced.

  • to_char, it is also called by a value and a reference, and the value is automatically borrowed.

From is implemented for both a value and a reference of Suit. Therefore, when called by a value Into&lt;Suit&gt;::into() is called, and when called by a reference Into&lt;&amp;Suit&gt;::into() is called.

However, shouldn't I only need to implement one of these traits? When I comment out either implementation the Rust compiler does not compile...

The process appears to be as follows... First generate a list of "candidate receiver types". This is obtained by "repeatedly dereferencing", and finally "attempting an unsized coercion at the end". In addition, "for each candidate T, add &T and &mut T to the list immediately after T." Then, for each of these "candidate receiver types" look for "methods implemented directly on T" and "methods provided by a visible trait implemented by T".

Consider that just From&lt;Suit&gt; is implemented for char. Then Into&lt;Suit&gt; should be implemented for char.

  • When let c: char = value.into(); is called, the "candidate receiver types" should contain at least Suit &amp;Suit and &amp;mut Suit. Then, Into&lt;T&gt;::into() is easily resolved for the first item in the list. Hence, Into&lt;Suit&gt;::into() is called.

  • But, when let f: char = reference.into(); is called, the "candidate receiver types" should also contain at least &amp;Suit, &amp;&amp;Suit, &amp;mut &amp;Suit, *&amp;Suit = Suit, &amp;Suit (again) and &amp;mut Suit. Then Into&lt;T&gt;::into() cannot find an implemented for &amp;Suit, &amp;&amp;Suit and &amp;mut &amp;Suit, but does then find an implementation for *&amp;Suit = Suit. Hence, Into&lt;Suit&gt;::into() is called.

Is my logic correct? Why doesn't this work if I comment-out one of the From implementations?

Rust Playground

#[derive(Clone, Copy)]
pub enum Suit {
    Club,
    Diamond,
    Heart,
    Spade,
}

pub use Suit::*;

impl Suit {
    #[inline(never)]
    pub fn into_char(self) -&gt; char {
        match self {
            Club =&gt; &#39;C&#39;,
            Diamond =&gt; &#39;D&#39;,
            Heart =&gt; &#39;H&#39;,
            Spade =&gt; &#39;S&#39;,
        }
    }
    
    #[inline(never)]
    pub fn to_char(&amp;self) -&gt; char {
        match self {
            Club =&gt; &#39;C&#39;,
            Diamond =&gt; &#39;D&#39;,
            Heart =&gt; &#39;H&#39;,
            Spade =&gt; &#39;S&#39;,
        }
    }
}

impl std::convert::From&lt;Suit&gt; for char {
    fn from(suit: Suit) -&gt; Self {
        suit.into_char()
    }
}

impl std::convert::From&lt;&amp;Suit&gt; for char {
    fn from(suit: &amp;Suit) -&gt; Self {
        suit.to_char()
    }
}

fn main() {
    let value = Club;
    let reference = &amp;value;
    
    let a: char = value.into_char();
    let b: char = value.to_char();
    let c: char = value.into();
    println!(&quot;{}, {}, {}&quot;, a, b, c);
    
    let d: char = reference.into_char();
    let e: char = reference.to_char();
    let f: char = reference.into();
    println!(&quot;{}, {}, {}&quot;, d, e, f);
}

EDIT: As discussed below, I created a better reproduction of the problem, which does help narrow down what causes this behaviour.

EDIT: I have created a GitHub issue for the Rust compiler.

答案1

得分: 2

这是因为在匹配特性时,编译器不考虑后续的类型推断。因此,虽然我们知道into()应该生成B,但编译器还不知道这一点。所以它只看到我们要一个Into的实现 Suit as Into<_>,对于某个类型_。因为它不知道类型_是什么,这与编译器看到的 impl<U, T: From<U>> Into<T> for U 匹配,编译器看到它为 Into<_> for _,因为它无法验证 where T: From<U>,因为_可以是任何类型。因此,编译器选择了这个实现(因为它没有使用自动引用,所以优先级较高),不再搜索impl Into<_> for &Suit

稍后,当编译器再次考虑这个决策时,已经知道我们期望一个char,它意识到这个实现不再匹配 - 但它不会回头搜索自动引用类型的实现;它已经完成了。所以我们最终没有匹配的实现。

英文:

This happens because when matching traits the compiler does not take later inference types into account. So, while we know that into() is expected to produce B, the compiler doesn't know that yet. So all it sees is that we want an implementation of Into Suit as Into&lt;_&gt;, for some type _. Because it doesn't know what type _ is, this matches impl&lt;U, T: From&lt;U&gt;&gt; Into&lt;T&gt; for U, seen by the compiler as Into&lt;_&gt; for _, as it is unable to verify the where T: From&lt;U&gt; because _ can be any type. So the compiler picks this implementation (which has higher priority because it does not use autoref) and does not search further for impl Into&lt;_&gt; for &amp;Suit.

Later, when the compiler comes back to this decision, armed with the knowledge that we expect a char, it realizes that this impl no longer match - but it doesn't go back to search impls for autoref types; it is done with that. So we end up with no matching impl.

huangapple
  • 本文由 发表于 2023年6月15日 09:49:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76478569.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定