Каковы точные правила автоматической разыменования в Rust?
Я изучаю / экспериментирую с 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
(по значению) для типаT
вести себя так, как будто они были объявлены с использованием&self
(вызов по ссылке) для типа&T
и вызывается ссылка на все, что находится слева от оператора точки. - Приведенные выше правила сначала пробуются с необработанной встроенной разыменовкой, и если нет совпадения, перегрузка с
Deref
черта используется.
Каковы точные правила автоматической разыменования? Кто-нибудь может дать какое-либо формальное обоснование для такого решения проекта?
4 ответа
Ваш псевдокод в значительной степени правильный. Для этого примера предположим, что у нас был вызов метода foo.bar()
где foo: T
, Я собираюсь использовать полный синтаксис (FQS), чтобы однозначно определить, с каким типом вызывается метод, например A::bar(foo)
или же A::bar(&***foo)
, Я просто собираюсь написать кучу случайных заглавных букв, каждая из которых представляет собой произвольный тип / черту, кроме T
всегда тип исходной переменной foo
что метод вызывается.
Суть алгоритма:
- За каждый "шаг разыменования"
U
(то есть установитьU = T
а потомU = *T
...)- если есть метод
bar
где тип получателя (типself
в методе) совпадаетU
точно, используйте это ( "методом стоимости") - в противном случае добавьте один авто-реф (взять
&
или же&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 не совпадает, принимая авто-реф дает нам&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. Нико Мацакис, основной автор этой части компилятора / языка, также посмотрел на этот ответ.)
В справочнике 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
, найдите видимый метод с приемником этого типа в следующих местах:
T
присущие методы (методы, реализованные непосредственно наT
[¹]).- Любой из методов, предоставляемых видимым признаком, реализованным
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]
Меня эта проблема беспокоила долгое время, особенно по этой части:
(*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 == **@
пока я не нашел способ запомнить эти странные правила. Я не уверен, что это правильно, но в большинстве случаев этот метод эффективен.
Ключ заключается в том, что при поиске функции, которую следует использовать, НЕ используйте тип, вызывающий "оператор точки", чтобы определить, какой "impl" использовать, а найдите функцию в соответствии с сигнатурой функции , а затем определите тип " self »с сигнатурой функции .
Я преобразовываю код определения функции следующим образом:
trait RefM { fn refm(&self); }
impl RefM for i32 { fn refm(&self) { println!("i32::refm()"); } }
// converted to: fn refm(&i32 ) { println!("i32::refm()"); }
// => type of 'self' : i32
// => type of parameter: &i32
impl RefM for X { fn refm(&self) { println!("X::refm()"); } }
// converted to: fn refm(&X ) { println!("X::refm()"); }
// => type of 'self' : X
// => type of parameter: &X
impl RefM for &X { fn refm(&self) { println!("&X::refm()"); } }
// converted to: fn refm(&&X ) { println!("&X::refm()"); }
// => type of 'self' : &X
// => type of parameter: &&X
impl RefM for &&X { fn refm(&self) { println!("&&X::refm()"); } }
// converted to: fn refm(&&&X ) { println!("&&X::refm()"); }
// => type of 'self' : &&X
// => type of parameter: &&&X
impl RefM for &&&X { fn refm(&self) { println!("&&&X::refm()"); } }
// converted to: fn refm(&&&&X) { println!("&&&X::refm()"); }
// => type of 'self' : &&&X
// => type of parameter: &&&&X
Поэтому, когда вы пишете код:
(&X{val:42}).refm();
функция
fn refm(&X ) { println!("X::refm()");
будет вызываться, потому что тип параметра
&X
.
И если соответствующая сигнатура функции не найдена, выполняется автоматическая ссылка или какое-то автоматическое удаление.
Методы, объявленные с использованием self (вызов по значению) для типа T, ведут себя так, как если бы они были объявлены с использованием &self (вызов по ссылке) для типа &T и вызываются по ссылке на то, что находится слева от оператора точки.
Они не ведут себя точно так же. При использовании self происходит перемещение (если только структура не копируется)
let example = X { val: 42};
example.m (); // is the same as M::m (example);
// Not possible: value used here after move
// example.m ();
let example = X { val: 42};
example.refm ();
example.refm ();