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
- Najpierw próbuje wezwać do pojedynczego odwołania
- Metody zadeklarowane przy użyciu
self
(call-by-value) dla typuT
zachowują się tak, jakby zostały zadeklarowane przy użyciu&self
(call-by-referencja) dla typu&T
i 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,
Deref
używane jest przeciążenie cechą.
Jakie są dokładne zasady automatycznego dereferencji? Czy ktoś może podać formalne uzasadnienie takiej decyzji projektowej?
reference
dereference
formal-semantics
rust
kFYatek
źródło
źródło
Odpowiedzi:
Twój pseudo-kod jest prawie poprawny. W tym przykładzie załóżmy, że mieliśmy wywołanie metody
foo.bar()
gdziefoo: 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)
LubA::bar(&***foo)
. Po prostu napiszę stos losowych wielkich liter, każda jest po prostu dowolnym dowolnym typem / cechą, z tym wyjątkiem, żeT
zawsze jest typem oryginalnej zmiennejfoo
, w której wywoływana jest metoda.Rdzeniem algorytmu jest:
U
(to znaczy ustaw,U = T
a następnieU = *T
...)bar
której typ odbiornika (typself
w metodzie) pasujeU
dokładnie, użyj jej ( „metoda według wartości” )&
lub&mut
odbiornika) 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 nie
Self
rodzaj cechy, tj.impl ... for Foo { fn method(&self) {} }
Myśli o&Foo
dopasowaniu metody ifn method2(&mut self)
pomyślałby o&mut Foo
dopasowaniu.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
Deref
implementacja 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ż
&foo
zachowuje silne połączenie zfoo
(jest to adresfoo
samego siebie), ale przyjęcie większej liczby zaczyna go tracić:&&foo
jest to adres jakiejś zmiennej tymczasowej na stosie, który przechowuje&foo
.Przykłady
Załóżmy, że mamy połączenie
foo.refm()
, jeślifoo
ma typ:X
, zaczynamy odU = X
,refm
ma typ odbiornika&...
, więc krok 1 nie pasuje, wykonanie auto-refrenu daje nam&X
, a to pasuje (zSelf = X
), więc wywołanie jestRefM::refm(&foo)
&X
, zaczyna się odU = &X
, co pasuje&self
do pierwszego kroku (zSelf = 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 (zSelf = &&&X
), a wywołanieRefM::refm(*foo)
Z
, nie pasuje do żadnego kroku, więc jest raz wyrenderowanyY
, 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 jestRefM::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 jestCopy
, jeślifoo
ma typ:A
, następnieU = A
dopasowujeself
bezpośrednio, więc połączenie jestM::m(foo)
zSelf = A
&A
, wtedy 1. nie pasuje, i nie 2. (ani&A
nie&&A
implementuje cechy), więc jest dereferencyjneA
, co pasuje, aleM::m(*foo)
wymaga wzięciaA
wartości i stąd wyprowadzkifoo
, stąd błąd.&&A
, 1. nie pasuje, ale autorefing daje&&&A
, co pasuje, więc wywołanie jestM::m(&foo)
zSelf = &&&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ź).
źródło
&&String
->&String
->String
->str
), a następnie odniesie maksymalnie raz (str
->&str
)”.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órerecv
poniżej nazywa się „wyrażeniem odbiorcy”.( 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 toi32
. Wykonujemy następujące kroki:i32
nie można się odnieść, dlatego już wykonaliśmy krok 1. Lista:[i32]
&i32
i&mut i32
. Lista:[i32, &i32, &mut i32]
<i32 as M>::m
który ma typ odbiornikai32
. 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:&&A
można się odnieść do nich&A
, dlatego dodajemy to do listy.&A
można ponownie wyrejestrować, więc dodajemy równieżA
do listy.A
nie można się oderwać, więc przestajemy. Lista:[&&A, &A, A]
T
na liście dodajemy&T
i&mut T
natychmiast poT
. Lista:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
&&A
, więc przechodzimy do następnego typu na liście.<&&&A as M>::m
któ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
X{val:42}.m()
→<X as M>::m
(&X{val:42}).m()
→<&X as M>::m
(&&X{val:42}).m()
→<&&X as M>::m
(&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&X{val:42}).m()
→<&&&X as M>::m
(&&&&&X{val:42}).m()
→<&&&X as M>::m
(*X{val:42}).refm()
→<i32 as RefM>::refm
X{val:42}.refm()
→<X as RefM>::refm
(&X{val:42}).refm()
→<X as RefM>::refm
(&&X{val:42}).refm()
→<&X as RefM>::refm
(&&&X{val:42}).refm()
→<&&X as RefM>::refm
(&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
(&&&&&X{val:42}).refm()
→<&&&X as RefM>::refm
Y{val:42}.refm()
→<i32 as RefM>::refm
Z{val:Y{val:42}}.refm()
→<i32 as RefM>::refm
A.m()
→<A as M>::m
(&A).m()
→<A as M>::m
(&&A).m()
→<&&&A as M>::m
(&&&A).m()
→<&&&A as M>::m
A.refm()
→<A as RefM>::refm
(&A).refm()
→<A as RefM>::refm
(&&A).refm()
→<A as RefM>::refm
(&&&A).refm()
→<&&&A as RefM>::refm
źródło