Jakie są dokładne zasady autodereferencji Rust?

181

Uczę się / eksperymentuję z Rustem i pomimo całej elegancji, jaką znajduję w tym języku, jest jedna osobliwość, która mnie zaskakuje i wydaje się zupełnie nie na miejscu.

Rdza automatycznie odrzuca wskaźniki podczas wykonywania wywołań metod. Zrobiłem kilka testów, aby ustalić dokładne zachowanie:

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 == @
}

( Plac zabaw )

Wygląda więc na to, że mniej więcej:

  • Kompilator wstawi tyle operatorów dereferencyjnych, ile jest konieczne do wywołania metody.
  • Kompilator podczas rozwiązywania metod zadeklarowanych za pomocą &self(call-by-reference):
    • Najpierw próbuje wezwać do pojedynczego odwołania self
    • Następnie próbuje zadzwonić po dokładny typ self
    • Następnie próbuje wstawić tyle operatorów dereferencji, ile potrzeba do dopasowania
  • Metody zadeklarowane przy użyciu self(call-by-value) dla typu Tzachowują się tak, jakby zostały zadeklarowane przy użyciu &self(call-by-referencja) dla typu &Ti wywołane w odniesieniu do wszystkiego, co znajduje się po lewej stronie operatora kropki.
  • Powyższe reguły są najpierw wypróbowywane przy użyciu surowego wbudowanego dereferencji, a jeśli nie ma zgodności, Derefużywane jest przeciążenie cechą.

Jakie są dokładne zasady automatycznego dereferencji? Czy ktoś może podać formalne uzasadnienie takiej decyzji projektowej?

kFYatek
źródło
Wysłałem to do subreddita Rust w nadziei na uzyskanie dobrych odpowiedzi!
Shepmaster
Dla dodatkowej zabawy spróbuj powtórzyć eksperyment w ogólnych i porównać wyniki.
user2665887

Odpowiedzi:

137

Twój pseudo-kod jest prawie poprawny. W tym przykładzie załóżmy, że mieliśmy wywołanie metody foo.bar()gdzie foo: T. Zamierzam użyć w pełni kwalifikowanej składni (FQS), aby jednoznacznie określić, z jakiego typu wywoływana jest metoda, np . A::bar(foo)Lub A::bar(&***foo). Po prostu napiszę stos losowych wielkich liter, każda jest po prostu dowolnym dowolnym typem / cechą, z tym wyjątkiem, że Tzawsze jest typem oryginalnej zmiennej foo, w której wywoływana jest metoda.

Rdzeniem algorytmu jest:

  • Dla każdego „kroku dereferencji” U (to znaczy ustaw, U = Ta następnie U = *T...)
    1. jeśli istnieje metoda, w barktórej typ odbiornika (typ selfw metodzie) pasuje Udokładnie, użyj jej ( „metoda według wartości” )
    2. w przeciwnym razie dodaj jeden automatyczny ref (wzięcie &lub &mutodbiornika) i, jeśli odbiornik jakiejś metody pasuje &U, użyj go ( „metoda autorefd” )

W szczególności wszystko bierze pod uwagę „typ odbiorcy” metody, a nieSelf rodzaj cechy, tj. impl ... for Foo { fn method(&self) {} }Myśli o &Foodopasowaniu metody i fn method2(&mut self)pomyślałby o &mut Foodopasowaniu.

Jest to błąd, jeśli w wewnętrznych krokach jest wiele metod cechy (tzn. Może istnieć tylko zero lub jedna metoda cechy ważna w każdym z 1. lub 2., ale może być jedna ważna dla każdej: jedna z 1 zostanie wzięty jako pierwszy), a metody nieodłączne mają pierwszeństwo przed cechami. Jest to również błąd, jeśli dojdziemy do końca pętli, nie znajdując nic pasującego. Błędem jest także Derefimplementacja rekurencyjna , która powoduje, że pętla jest nieskończona (osiągną „limit rekurencji”).

Reguły te wydają się robić, co mam na myśli, w większości przypadków, chociaż możliwość napisania jednoznacznego formularza FQS jest bardzo przydatna w niektórych przypadkach na krawędziach i dla rozsądnych komunikatów o błędach dla kodu generowanego przez makro.

Dodano tylko jedno automatyczne odniesienie, ponieważ

  • jeśli nie było żadnych ograniczeń, sytuacja staje się zła / wolna, ponieważ każdy typ może mieć dowolną liczbę referencji
  • biorąc jedno odniesienie &foozachowuje silne połączenie z foo(jest to adres foosamego siebie), ale przyjęcie większej liczby zaczyna go tracić: &&foojest to adres jakiejś zmiennej tymczasowej na stosie, który przechowuje &foo.

Przykłady

Załóżmy, że mamy połączenie foo.refm(), jeśli fooma typ:

  • X, zaczynamy od U = X, refmma typ odbiornika &..., więc krok 1 nie pasuje, wykonanie auto-refrenu daje nam &X, a to pasuje (z Self = X), więc wywołanie jestRefM::refm(&foo)
  • &X, zaczyna się od U = &X, co pasuje &selfdo pierwszego kroku (z Self = X), więc połączenie jestRefM::refm(foo)
  • &&&&&X, nie pasuje to do żadnego kroku (cecha nie jest zaimplementowana dla &&&&X lub &&&&&X), więc raz odejmujemy, aby uzyskać U = &&&&X, co odpowiada 1 (z Self = &&&X), a wywołanieRefM::refm(*foo)
  • Z, nie pasuje do żadnego kroku, więc jest raz wyrenderowany Y , który również nie pasuje, więc jest ponownie wyłuskany, aby uzyskać X, który nie pasuje do 1, ale pasuje po autorefingowaniu, więc wywołanie jest RefM::refm(&**foo).
  • &&A, 1. nie pasuje, podobnie jak 2., ponieważ cecha nie jest zaimplementowana &A&&A , podobnie (dla 1) lub (dla 2), więc jest wyłączona z odniesienia &A, która odpowiada 1., zSelf = A

Załóżmy, że mamy foo.m()i toA nie jest Copy, jeśli fooma typ:

  • A, następnie U = A dopasowuje selfbezpośrednio, więc połączenie jest M::m(foo)zSelf = A
  • &A, wtedy 1. nie pasuje, i nie 2. (ani &Anie &&Aimplementuje cechy), więc jest dereferencyjne A, co pasuje, aleM::m(*foo) wymaga wzięcia Awartości i stąd wyprowadzki foo, stąd błąd.
  • &&A, 1. nie pasuje, ale autorefing daje &&&A, co pasuje, więc wywołanie jestM::m(&foo) z Self = &&&A.

(Ta odpowiedź jest oparta na kodzie i jest dość zbliżona do (nieco nieaktualnego) README . Niko Matsakis, główny autor tej części kompilatora / języka, również przejrzał tę odpowiedź).

huon
źródło
15
Ta odpowiedź wydaje się wyczerpująca i szczegółowa, ale myślę, że brakuje w niej krótkiego i przystępnego podsumowania zasad. Jedno takie lato jest podane w tym komentarzu przez Shepmaster : „To [algorytm deref] wyrenderuje tyle razy, ile to możliwe ( &&String-> &String-> String-> str), a następnie odniesie maksymalnie raz ( str-> &str)”.
Lii,
(Nie wiem, jak dokładne i kompletne jest to wyjaśnienie.)
Lii
1
W jakich przypadkach występuje automatyczne dereferencje? Czy jest używany tylko do wyrażenia odbiorcy dla wywołania metody? Czy masz również dostęp do pól? Zadanie po prawej stronie? Lewa strona? Parametry funkcji? Zwracane wyrażenia wartości?
Lii
1
Uwaga: obecnie nomicon ma notatkę TODO, aby ukraść informacje z tej odpowiedzi i zapisać je w static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB
1
Czy przymus (A) był wypróbowany przed tym lub (B) wypróbowany po tym lub (C) wypróbowany na każdym etapie tego algorytmu lub (D) coś innego?
haslersn
8

Odwołanie do Rust zawiera rozdział o wyrażeniu wywołania metody . Najważniejszą część skopiowałem poniżej. Przypomnienie: mówimy o wyrażeniu recv.m(), które recvponiżej nazywa się „wyrażeniem odbiorcy”.

Pierwszym krokiem jest zbudowanie listy potencjalnych typów odbiorników. Zdobądź je, wielokrotnie odrywając typ wyrażenia odbiorcy, dodając każdy napotkany typ do listy, a następnie w końcu próbując na końcu przymusu bez rozmiaru i dodając typ wyniku, jeśli się powiedzie. Następnie, dla każdego kandydata T, dodaj &Ti &mut Tdo listy natychmiast poT .

Na przykład, jeśli odbiorca ma typ Box<[i32;2]>, wówczas typami kandydującymi będą Box<[i32;2]>:&Box<[i32;2]> , &mut Box<[i32;2]>, [i32; 2](o dereferencing) &[i32; 2], &mut [i32; 2], [i32](o apretur nacisku) &[i32], a na koniec&mut [i32] .

Następnie dla każdego typu kandydata Twyszukaj widoczną metodę z odbiornikiem tego typu w następujących miejscach:

  1. Tmetody nieodłączne (metody wdrożone bezpośrednio na T [¹]).
  2. Każda z metod zapewnianych przez widoczną cechę zaimplementowana przez T. [...]

( Uwaga na temat [¹] : Właściwie to myślę, że to sformułowanie jest nieprawidłowe. Otworzyłem problem . Po prostu zignorujmy to zdanie w nawiasie.)


Przejrzyjmy szczegółowo kilka przykładów z twojego kodu! W przykładach możemy zignorować część dotyczącą „przymusu bez rozmiarów” i „metod nieodłącznych”.

(*X{val:42}).m(): typ wyrażenia odbiorcy to i32 . Wykonujemy następujące kroki:

  • Tworzenie listy potencjalnych typów odbiorników:
    • i32 nie można się odnieść, dlatego już wykonaliśmy krok 1. Lista: [i32]
    • Następnie dodajemy &i32i &mut i32. Lista:[i32, &i32, &mut i32]
  • Wyszukiwanie metod dla każdego potencjalnego odbiorcy:
    • Znajdujemy, <i32 as M>::mktóry ma typ odbiornika i32. Więc już skończyliśmy.


Jak dotąd tak łatwo. Teraz wybrać trudniejszą przykład: (&&A).m(). Typ wyrażenia odbiorcy to &&A. Wykonujemy następujące kroki:

  • Tworzenie listy potencjalnych typów odbiorników:
    • &&Amożna się odnieść do nich &A, dlatego dodajemy to do listy. &Amożna ponownie wyrejestrować, więc dodajemy również Ado listy. Anie można się oderwać, więc przestajemy. Lista:[&&A, &A, A]
    • Następnie dla każdego typu Tna liście dodajemy &Ti &mut Tnatychmiast po T. Lista:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Wyszukiwanie metod dla każdego potencjalnego odbiorcy:
    • Nie ma metody z typem odbiornika &&A , więc przechodzimy do następnego typu na liście.
    • Znajdujemy metodę, <&&&A as M>::mktóra rzeczywiście ma typ odbiornika &&&A. Skończyliśmy.

Oto lista kandydatów na wszystkie twoje przykłady. Typ, który jest objęty, ⟪x⟫to ten, który „wygrał”, tj. Pierwszy typ, dla którego można znaleźć metodę dopasowania. Pamiętaj również, że pierwszym typem na liście jest zawsze typ wyrażenia odbiorcy. Na koniec sformatowałem listę w trzech wierszach, ale to tylko formatowanie: ta lista jest płaska.

  • (*X{val:42}).m()<i32 as M>::m
    [i32, &i32, &mut i32]
  • X{val:42}.m()<X as M>::m
    [⟪X⟫, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).m()<&X as M>::m
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).m()<&&X as M>::m
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).m()<&&&X as M>::m
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).m()<&&&X as M>::m
    [&&&&&X, &&&&&&X, &mut &&&&&X, 
     &&&&X, &&&&&X, &mut &&&&X,&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • (*X{val:42}).refm()<i32 as RefM>::refm
    [i32,&i32, &mut i32]
  • X{val:42}.refm()<X as RefM>::refm
    [X,&X⟫, &mut X, 
     i32, &i32, &mut i32]
  • (&X{val:42}).refm()<X as RefM>::refm
    [&X⟫, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&X{val:42}).refm()<&X as RefM>::refm
    [&&X⟫, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&X{val:42}).refm()<&&X as RefM>::refm
    [&&&X⟫, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]
  • (&&&&&X{val:42}).refm()<&&&X as RefM>::refm
    [&&&&&X, &&&&&&X, &mut &&&&&X,&&&&X⟫, &&&&&X, &mut &&&&X, 
     &&&X, &&&&X, &mut &&&X, 
     &&X, &&&X, &mut &&X, 
     &X, &&X, &mut &X, 
     X, &X, &mut X, 
     i32, &i32, &mut i32]


  • Y{val:42}.refm()<i32 as RefM>::refm
    [Y, &Y, &mut Y,
     i32,&i32, &mut i32]
  • Z{val:Y{val:42}}.refm()<i32 as RefM>::refm
    [Z, &Z, &mut Z,
     Y, &Y, &mut Y,
     i32,&i32, &mut i32]


  • A.m()<A as M>::m
    [⟪A⟫, &A, &mut A]
  • (&A).m()<A as M>::m
    [&A, &&A, &mut &A,
     ⟪A⟫, &A, &mut A]
  • (&&A).m()<&&&A as M>::m
    [&&A,&&&A⟫, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).m()<&&&A as M>::m
    [&&&A⟫, &&&&A, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
  • A.refm()<A as RefM>::refm
    [A,&A⟫, &mut A]
  • (&A).refm()<A as RefM>::refm
    [&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&A).refm()<A as RefM>::refm
    [&&A, &&&A, &mut &&A,&A⟫, &&A, &mut &A,
     A, &A, &mut A]
  • (&&&A).refm()<&&&A as RefM>::refm
    [&&&A,&&&&A⟫, &mut &&&A,
     &&A, &&&A, &mut &&A,
     &A, &&A, &mut &A,
     A, &A, &mut A]
Lukas Kalbertodt
źródło