1. ホーム

[解決済み】Rustの正確な自動再参照のルールは?

2022-04-19 08:48:57

質問

Rustを学習・実験していますが、この言語に見られるすべてのエレガンスの中で、私を当惑させ、全く場違いだと思われる特殊性が1つあります。

Rustはメソッド呼び出しの際、自動的にポインタの参照を解除します。正確な挙動を確認するために、いくつかテストをしてみました。

struct X { val: i32 }
impl std::ops::Deref for X {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

trait M { fn m(self); }
impl M for i32   { fn m(self) { println!("i32::m()");  } }
impl M for X     { fn m(self) { println!("X::m()");    } }
impl M for &X    { fn m(self) { println!("&X::m()");   } }
impl M for &&X   { fn m(self) { println!("&&X::m()");  } }
impl M for &&&X  { fn m(self) { println!("&&&X::m()"); } }

trait RefM { fn refm(&self); }
impl RefM for i32  { fn refm(&self) { println!("i32::refm()");  } }
impl RefM for X    { fn refm(&self) { println!("X::refm()");    } }
impl RefM for &X   { fn refm(&self) { println!("&X::refm()");   } }
impl RefM for &&X  { fn refm(&self) { println!("&&X::refm()");  } }
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }


struct Y { val: i32 }
impl std::ops::Deref for Y {
    type Target = i32;
    fn deref(&self) -> &i32 { &self.val }
}

struct Z { val: Y }
impl std::ops::Deref for Z {
    type Target = Y;
    fn deref(&self) -> &Y { &self.val }
}


#[derive(Clone, Copy)]
struct A;

impl M for    A { fn m(self) { println!("A::m()");    } }
impl M for &&&A { fn m(self) { println!("&&&A::m()"); } }

impl RefM for    A { fn refm(&self) { println!("A::refm()");    } }
impl RefM for &&&A { fn refm(&self) { println!("&&&A::refm()"); } }


fn main() {
    // I'll use @ to denote left side of the dot operator
    (*X{val:42}).m();        // i32::m()    , Self == @
    X{val:42}.m();           // X::m()      , Self == @
    (&X{val:42}).m();        // &X::m()     , Self == @
    (&&X{val:42}).m();       // &&X::m()    , Self == @
    (&&&X{val:42}).m();      // &&&X:m()    , Self == @
    (&&&&X{val:42}).m();     // &&&X::m()   , Self == *@
    (&&&&&X{val:42}).m();    // &&&X::m()   , Self == **@
    println!("-------------------------");

    (*X{val:42}).refm();     // i32::refm() , Self == @
    X{val:42}.refm();        // X::refm()   , Self == @
    (&X{val:42}).refm();     // X::refm()   , Self == *@
    (&&X{val:42}).refm();    // &X::refm()  , Self == *@
    (&&&X{val:42}).refm();   // &&X::refm() , Self == *@
    (&&&&X{val:42}).refm();  // &&&X::refm(), Self == *@
    (&&&&&X{val:42}).refm(); // &&&X::refm(), Self == **@
    println!("-------------------------");

    Y{val:42}.refm();        // i32::refm() , Self == *@
    Z{val:Y{val:42}}.refm(); // i32::refm() , Self == **@
    println!("-------------------------");

    A.m();                   // A::m()      , Self == @
    // without the Copy trait, (&A).m() would be a compilation error:
    // cannot move out of borrowed content
    (&A).m();                // A::m()      , Self == *@
    (&&A).m();               // &&&A::m()   , Self == &@
    (&&&A).m();              // &&&A::m()   , Self == @
    A.refm();                // A::refm()   , Self == @
    (&A).refm();             // A::refm()   , Self == *@
    (&&A).refm();            // A::refm()   , Self == **@
    (&&&A).refm();           // &&&A::refm(), Self == @
}

( 遊び場 )

ということは、多かれ少なかれ、あるようです。

  • コンパイラは、メソッドを呼び出すのに必要な数の参照解除演算子を挿入します。
  • を使用して宣言されたメソッドを解決するとき、コンパイラは &self (参照渡し)。
    • の単一の参照解除を呼び出そうとします。 self
    • の正確な型を呼ぼうとする。 self
    • 次に、マッチングに必要な数の参照外演算子を挿入しようとします。
  • を使用して宣言されたメソッドは self (コール・バイ・バリュー) タイプの T を使用して宣言されたかのように振る舞います。 &self (参照渡し)で、型 &T で、ドット演算子の左側にあるものへの参照で呼び出されます。
  • 上記のルールは、まず生のビルトインデリフェレンシングで試され、マッチしない場合、オーバーロードで Deref の形質が使われます。

正確な自動再参照のルールは?また、そのような設計上の決定に対して、正式な根拠を示すことができる人はいますか?

解決方法は?

あなたの擬似コードはかなり正しいです。この例では、メソッド呼び出しがあったとします。 foo.bar() ここで foo: T . を使うことにします。 完全修飾構文 (FQS) を使用すると、メソッドがどのような型で呼び出されているのかが明確になります。 A::bar(foo) または A::bar(&***foo) . ランダムに大文字を書き連ねるだけで、それぞれは任意の型/特性ですが、ただし T は常に元の変数の型 foo で、そのメソッドが呼び出される。

アルゴリズムの核となるのは

  • 参照ステップ" U (を設定します(つまり U = T を指定し、次に U = *T , ...)
    1. メソッドがある場合 bar ここで、レシーバの型( self はメソッド内の U を使用します。 a "値による方法"。 )
    2. そうでない場合は、自動参照を1つ追加します。 & または &mut と一致する場合、そのメソッドのレシーバは &U であれば、それを使う ( autorefd method" )

注目すべきは、すべてがメソッドの "receiver type" を考慮していることです。 ではなく Self の型、すなわち、特質が impl ... for Foo { fn method(&self) {} } を考える &Foo メソッドとマッチングするときに fn method2(&mut self) を考えるだろう。 &mut Foo をマッチングさせる。

内側のステップで有効な形質メソッドが複数ある場合(つまり、1.または2.のそれぞれで有効な形質メソッドはゼロか1つだけで、それぞれで有効なものがある可能性があります:1からのものが最初に取られます)、エラーとなり、固有のメソッドは形質メソッドよりも優先されます。また、ループの最後まで行って、マッチするものが見つからなかった場合もエラーとなります。また、再帰的な Deref の実装では、ループが無限大になってしまいます("recursion limit"にヒットしてしまいます)。

曖昧さのないFQS形式を書く能力は、いくつかのエッジケースや、マクロで生成されたコードの賢明なエラーメッセージのために非常に便利ですが、これらのルールは、ほとんどの状況で何を意味するかというと、そうです。

自動参照は1つだけ追加されます。

  • すべての型は任意の数の参照を持つことができるので、束縛がなかった場合、物事は悪く/遅くなる
  • 1つのリファレンスを取る &foo との強い結びつきが保たれています。 foo (のアドレスである)。 foo を含む)が、それ以上取ると失われ始める。 &&foo を格納するスタック上の一時的な変数のアドレスです。 &foo .

使用例

という呼び出しがあったとします。 foo.refm() もし foo は型を持つ。

  • X で始まる。 U = X , refm は受信機型 &... ということで、ステップ1がマッチしないので、自動参照をとると、次のようになります。 &X で、これは一致します。 Self = X ) であるため、呼び出しは RefM::refm(&foo)
  • &X で始まる。 U = &X と一致します。 &self は、最初のステップで( Self = X ) であるため、呼び出しは RefM::refm(foo)
  • &&&&&X に対して実装されていない)。 &&&&X または &&&&&X を取得するために、一度参照を解除しています。 U = &&&&X にマッチし、1( Self = &&&X ) であり、呼び出しは RefM::refm(*foo)
  • Z はどちらのステップにもマッチしないので、一度だけ参照され、次のようになります。 Y これもマッチしないので、もう一度参照され、次のようになります。 X これは1にはマッチしませんが、オートリフィングの結果マッチするので、呼び出しは次のようになります。 RefM::refm(&**foo) .
  • &&A には実装されていないので、1.は一致せず、2.も一致しません。 &A (1について)または &&A (2の場合)であるため、デリファレンスは &A で、1.にマッチします。 Self = A

があるとします。 foo.m() で、その ACopy もし foo は型を持つ。

  • A であれば U = A マッチ self を直接呼び出すので、呼び出しは M::m(foo)Self = A
  • &A の場合、1.はマッチしませんし、2.もマッチしません(どちらも &A また &&A を実装している)ため、デリファレンスは A というのは一致するのですが M::m(*foo) を取る必要があります。 A を値で指定し、その結果 foo そのため、エラーが発生します。
  • &&A は一致しませんが、オートリファイリングにより &&&A であり、これは一致するので、呼び出しは M::m(&foo)Self = &&&A .

(この回答は コード であり、かつ は、(少し古いですが)READMEにそれなりに近いです。 . この部分のメイン作者であるNiko Matsakis氏もこの回答に目を通しました)。