Quais são as regras exatas de desreferência automática da Rust?

Estou aprendendo / experimentando Rust, e com toda a elegância que encontro nessa linguagem, há uma peculiaridade que me deixa perplexa e parece totalmente fora de lugar.

Rust automaticamente faz referência a pointers ao fazer chamadas de método. Fiz alguns testes para determinar o comportamento exato:

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

Então, parece que, mais ou menos:

  • O compilador irá inserir quantos operadores de remoção de referência forem necessários para invocar um método.
  • O compilador, ao resolver os methods declarados usando &self (call-by-reference):
    • Primeiras tentativas de pedir uma única desreferencia do self
    • Em seguida, tenta ligar para o tipo exato de self
    • Em seguida, tenta inserir tantos operadores de cancelamento de referência quanto necessário para uma correspondência
  • Os methods declarados usando self (call-by-value) para o tipo T se comportam como se fossem declarados usando &self (chamada por referência) para type &T e chamados na referência para o que estiver no lado esquerdo do operador dot.
  • As regras acima são primeiramente tentadas com desreferenciação bruta e, se não houver correspondência, a sobrecarga com o traço Deref é usada.

Quais são as regras exatas de desreferência automática? Alguém pode dar alguma justificativa formal para tal decisão de design?

Seu pseudo-código é bem correto. Para este exemplo, suponha que tenhamos um método chamado foo.bar() onde foo: T Vou usar a syntax totalmente qualificada (FQS) para não ser ambígua sobre com que tipo o método está sendo chamado, por exemplo, A::bar(foo) ou A::bar(&***foo) . Eu só vou escrever uma pilha de letras maiúsculas aleatórias, cada uma é apenas um tipo / traço arbitrário, exceto T é sempre o tipo da variável original foo que o método é chamado.

O núcleo do algoritmo é:

  • Para cada “etapa de desreferenciamento” U (isto é, ajuste U = T e depois U = *T , …)
    1. se houver uma bar método em que o tipo de receptor (o tipo de self no método) corresponde exatamente a U , use-o ( um “método de valor” )
    2. caso contrário, adicione um auto-ref (take & or &mut do receptor) e, se algum receptor do método corresponder a &U , use-o ( um “método autorefd” )

Notavelmente, tudo considera o “tipo receptor” do método, não o tipo Self da característica, ie impl ... for Foo { fn method(&self) {} } pensa sobre &Foo ao combinar o método, e fn method2(&mut self) pensaria em &mut Foo quando combinando.

É um erro se houver sempre vários methods de traços válidos nas etapas internas (ou seja, pode haver apenas methods de zero ou um traço válidos em cada um dos 1. ou 2., mas pode haver um válido para cada um: o único de 1 serão tomadas primeiro), e os methods inerentes têm precedência sobre os traços. Também é um erro se chegarmos ao final do loop sem encontrar nada que corresponda. Também é um erro ter implementações Deref recursivas, o que torna o loop infinito (elas atingem o “limite de recursion”).

Essas regras parecem fazer o que eu quero dizer na maioria das circunstâncias, embora ter a capacidade de escrever o formulário FQS não ambíguo seja muito útil em alguns casos de borda e para mensagens de erro sensatas para código gerado por macro.

Somente uma referência automática é adicionada porque

  • se não houver limite, as coisas ficam ruins / lentas, já que todo tipo pode ter um número arbitrário de referências
  • tomando uma referência &foo mantém uma conexão forte com foo (é o endereço do próprio foo ), mas tendo mais começa a perdê-lo: &&foo é o endereço de alguma variável temporária na pilha que armazena &foo .

Exemplos

Suponha que tenhamos uma chamada foo.refm() , se foo tiver tipo:

  • X , então começamos com U = X , refm tem tipo de receptor &... , então o passo 1 não corresponde, fazendo com que um auto-ref nos forneça &X , e isso corresponde (com Self = X ), então a chamada é RefM::refm(&foo)
  • &X , começa com U = &X , que corresponde a &self no primeiro passo (com Self = X ), e assim a chamada é RefM::refm(foo)
  • &&&&&X , isso não corresponde a nenhuma etapa (o atributo não está implementado para &&&&X ou &&&&&X ), portanto, &&&&&X referência uma vez para obter U = &&&&X , que corresponde a 1 (com Self = &&&X ) e a chamada é RefM::refm(*foo)
  • Z , não corresponde a nenhuma das etapas, portanto, é desreferenciada uma vez, para obter Y , que também não corresponde, então é desreferenciada novamente, para obter X , que não corresponde a 1, mas corresponde após o autorefing. é RefM::refm(&**foo) .
  • &&A , o 1. não corresponde e nem 2. uma vez que o traço não está implementado para &A (para 1) ou &&A (para 2), então é desreferenciado para &A , que corresponde a 1., com Self = A

Suponha que temos foo.m() , e que A não é Copy , se foo tiver tipo:

  • A , então U = A corresponde diretamente ao self então a chamada é M::m(foo) com Self = A
  • &A , então 1. não combina, e nem 2. (nem &A nem &&A implementa o traço), portanto, é desreferenciado para A , que corresponde, mas M::m(*foo) requer que se tome A por valor e, portanto, saindo de foo , daí o erro.
  • &&A , 1. não corresponde, mas o autorefing dá &&&A , que corresponde, então a chamada é M::m(&foo) com Self = &&&A

(Esta resposta é baseada no código e está razoavelmente próxima do (ligeiramente desatualizado) README . Niko Matsakis, o principal autor desta parte do compilador / linguagem, também deu uma olhada nessa resposta.)

    Intereting Posts