Я изучаю / экспериментирую с 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
признаком.
Каковы точные правила автоматической разыменования? Кто-нибудь может дать какое-либо формальное обоснование для такого решения дизайна?
reference
dereference
formal-semantics
rust
kFYatek
источник
источник
Ответы:
Ваш псевдокод в значительной степени правильный. Для этого примера предположим, что у нас был вызов метода
foo.bar()
wherefoo: 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 не совпадает, беря 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 . Нико Мацакис, основной автор этой части компилятора / языка, также посмотрел на этот ответ.)
источник
&&String
->&String
->String
->str
), а затем будет ссылаться на максимум один раз (str
->&str
)».Ссылка Rust содержит главу о выражении вызова метода . Я скопировал самую важную часть ниже. Напоминание: мы говорим о выражении
recv.m()
,recv
которое ниже называется «выражение получателя».( Примечание по поводу [¹] : я на самом деле думаю, что это выражение неверно. Я открыл проблему . Давайте просто проигнорируем это предложение в скобках.)
Давайте подробно рассмотрим несколько примеров из вашего кода! Для ваших примеров мы можем игнорировать часть о «нерегулируемом принуждении» и «внутренних методах».
(*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
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
источник