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

182

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

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

kFYatek
источник
1
Я перепостил это в Rred subreddit в надежде получить хорошие ответы!
Шепмастер
Для дополнительного удовольствия попробуйте повторить эксперимент в дженериках и сравните результаты.
user2665887

Ответы:

137

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

Суть алгоритма:

  • Для каждого «шага разыменования» U (то есть установить, U = Tа затем U = *T, ...)
    1. если есть метод, в barкотором тип получателя (тип selfв методе) Uточно совпадает , используйте его ( метод «по значению» )
    2. в противном случае добавьте один auto-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. не совпадает, но дает авторефинг &&&A, который совпадает, поэтому вызов M::m(&foo)с Self = &&&A.

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

Юон
источник
15
Этот ответ кажется исчерпывающим и подробным, но я думаю, что ему не хватает краткого и доступного описания правил. Одна из таких сумм приводится в этом комментарии Шепмастером : «Он [алгоритм разыменования] будет разыменовываться как можно больше раз ( &&String-> &String-> String-> str), а затем будет ссылаться на максимум один раз ( str-> &str)».
Лии
(Я не знаю , насколько точны и полное объяснение , что это я.)
ЛИИ
1
В каких случаях происходит автоматическая разыменование? Он используется только для выражения получателя для вызова метода? Для доступа к полю также? Назначение правой стороны? Левые стороны? Параметры функции? Возвращаемое значение выражения?
Лий
1
Примечание. В настоящее время номикон имеет примечание TODO, чтобы украсть информацию из этого ответа и записать ее в static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB,
1
Испытывается ли принуждение (A) до этого или (B) после этого или (C) на каждом этапе этого алгоритма или (D) что-то еще?
haslersn
8

Ссылка Rust содержит главу о выражении вызова метода . Я скопировал самую важную часть ниже. Напоминание: мы говорим о выражении recv.m(), recvкоторое ниже называется «выражение получателя».

Первым шагом является создание списка возможных типов получателей. Получите их, неоднократно разыменовывая тип получающего выражения, добавляя каждый встреченный тип в список, затем, в конце концов, пытаясь выполнить принудительное приведение в конце и добавляя тип результата, если это успешно. Затем для каждого кандидата Tдобавьте &Tи &mut Tв список сразу после T.

Например, если приемник имеет тип Box<[i32;2]>, то типы кандидатов будут Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](разыменовывая) &[i32; 2], &mut [i32; 2], [i32](по некалиброванному принуждению), &[i32]и , наконец &mut [i32].

Затем для каждого типа кандидата Tищите видимый метод с получателем этого типа в следующих местах:

  1. TПрисущие методы (методы, реализованные непосредственно на T[¹]).
  2. Любой из методов, предоставляемых видимой чертой, реализованной T. [...]

( Примечание по поводу [¹] : я на самом деле думаю, что это выражение неверно. Я открыл проблему . Давайте просто проигнорируем это предложение в скобках.)


Давайте подробно рассмотрим несколько примеров из вашего кода! Для ваших примеров мы можем игнорировать часть о «нерегулируемом принуждении» и «внутренних методах».

(*X{val:42}).m(): Тип Выражение ресивера i32. Мы выполняем эти шаги:

  • Создание списка возможных типов получателей:
    • i32 не может быть разыменовано, поэтому мы уже закончили с шагом 1. Список: [i32]
    • Далее добавляем &i32и &mut i32. Список:[i32, &i32, &mut i32]
  • Поиск методов для каждого типа получателя-кандидата:
    • Мы находим, <i32 as M>::mкоторый имеет тип получателя i32. Итак, мы уже сделали.


Пока все просто. Теперь давайте выберем более сложный пример: (&&A).m(). Выражение типа ресивера &&A. Мы выполняем эти шаги:

  • Создание списка возможных типов получателей:
    • &&Aможет быть разыменовано &A, поэтому мы добавим это в список. &Aможет быть снова разыменовано, поэтому мы также добавляем Aв список. Aне может быть разыменовано, поэтому мы остановимся. Список:[&&A, &A, A]
    • Далее для каждого типа Tв списке мы добавляем &Tи &mut Tсразу после T. Список:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Поиск методов для каждого типа получателя-кандидата:
    • Нет метода с типом получателя &&A, поэтому мы переходим к следующему типу в списке.
    • Мы находим метод, <&&&A as M>::mкоторый действительно имеет тип получателя &&&A. Итак, мы закончили.

Вот списки получателей кандидатов для всех ваших примеров. Тип, который заключен в ⟪x⟫это тот, который «победил», то есть первый тип, для которого можно найти подходящий метод. Также помните, что первый тип в списке всегда является типом выражения получателя. Наконец, я отформатировал список в три строки, но это только форматирование: этот список является плоским списком.

  • (*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]
Лукас Калбертодт
источник