Что такое правила автоматического разыменования Rust?

Я изучаю/экспериментирую с 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<'a>         M for &'a X         { fn m(self) { println!("&X::m()"); } }
impl<'a, 'b>     M for &'a &'b X     { fn m(self) { println!("&&X::m()"); } }
impl<'a, 'b, 'c> M for &'a &'b &'c 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<'a>         RefM for &'a X         { fn refm(&self) { println!("&X::refm()"); } }
impl<'a, 'b>     RefM for &'a &'b X     { fn refm(&self) { println!("&&X::refm()"); } }
impl<'a, 'b, 'c> RefM for &'a &'b &'c 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 }
}

struct A;
impl std::marker::Copy for A {}
impl             M for             A { fn m(self) { println!("A::m()"); } }
impl<'a, 'b, 'c> M for &'a &'b &'c A { fn m(self) { println!("&&&A::m()"); } }
impl             RefM for             A { fn refm(&self) { println!("A::refm()"); } }
impl<'a, 'b, 'c> RefM for &'a &'b &'c 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::refm() , 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 == **@

    (*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 == **@

    Y{val:42}.refm();        // i32::refm() , self == *@
    Z{val:Y{val:42}}.refm(); // i32::refm() , self == **@

    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 (call-by-value) для типа T, ведут себя так, как если бы они были объявлены с использованием &self (call-by-reference) для типа &T и вызвали ссылку на что-либо находится в левой части оператора точки.
  • Приведенные выше правила сначала проверяются с использованием сырой встроенной разыменования, и если нет совпадения, используется перегрузка с тэгом Deref.

Каковы точные правила автоматического разыменования? Может ли кто-нибудь дать какое-либо формальное обоснование для такого проектного решения?

Ответ 1

Ваш псевдокод в значительной степени верен. Для этого примера предположим, что у нас был вызов метода foo.bar() где foo: T. Я использую полный синтаксис (FQS), чтобы быть однозначным в отношении того, с каким типом метода вызывается, например. A::bar(foo) или A::bar(&***foo). Я просто собираюсь написать кучу случайных заглавных букв, каждый из которых является лишь некоторым произвольным типом/признаком, за исключением того, что T всегда является типом исходной переменной foo, вызываемой методом.

Ядро алгоритма:

  • Для каждого "шаг разворота" U (то есть установите U = T, а затем U = *T,...)
    • если существует метод bar, где тип приемника (тип self в методе) точно соответствует U, используйте его (a "по методу значений" )
    • в противном случае добавьте один авто-ref (возьмите & или &mut получателя), и если какой-либо приемник метода соответствует &U, используйте его ( "метод autorefd" )

Примечательно, что все рассматривает "тип приемника" метода, а не тип self признака, т.е. impl ... for Foo { fn method(&self) {} } думает о &Foo при сопоставлении метода, а fn method2(&mut self) будет думать о &mut Foo при сопоставлении.

Это ошибка, если на внутренних этапах есть допустимые методы с несколькими признаками (то есть, могут быть только нулевые или одни методы признака, действительные в каждом из 1. или 2., но может быть один действительный для каждого: первый из 1 будет сделан первым), а присущие ему методы имеют приоритет над чертами. Это также ошибка, если мы дойдем до конца цикла, не найдя ничего, что соответствует. Это также ошибка для рекурсивных реализаций Deref, которые делают цикл бесконечным (они попадут в "предел рекурсии" ).

Эти правила, по-видимому, делают-что-я-значение в большинстве случаев, хотя наличие возможности писать однозначную форму FQS очень полезно в некоторых случаях кросс-кода и для разумных сообщений об ошибках для макрогенерированного кода.

Добавлена ​​только одна автоматическая ссылка, потому что

  • Если не было никакой привязки, все становится плохо/медленным, так как каждый тип может иметь произвольное количество принятых ссылок
  • взятие одной ссылки &Foo сохраняет сильное соединение с foo (это адрес самого foo), но при этом больше начинает его потерять: &&foo - это адрес некоторой временной переменной в стеке который хранит &Foo.

Примеры

Предположим, что у нас есть вызов foo.refm(), если foo имеет тип:

  • X, тогда мы начинаем с U = X, refm имеет тип приемника &..., поэтому шаг 1 не соответствует, взяв auto-ref, дает нам &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() и что A не Copy, если foo имеет тип:

  • A, тогда U = A соответствует self напрямую, поэтому вызов M::m(foo) с Self = A
  • &A, то 1. не соответствует, и не делает 2. (ни &A, ни &&A не реализует этот признак), поэтому он разыменован на A, который соответствует, но M::m(*foo) требует принятия A по значению и, следовательно, выхода из foo, следовательно, ошибки.
  • &&A, 1. не соответствует, но autorefing дает &&&A, который соответствует, поэтому вызов M::m(&foo) с Self = &&&A.

(Этот ответ основан на код, а достаточно близко к (слегка устаревшему) README. Нико Мацакис, главный автор этой части компилятора/языка, также просмотрел этот ответ.)