web-dev-qa-db-fra.com

Quelles sont les règles exactes de déréférencement automatique de Rust?

J'apprends/expérimente avec Rust, et dans toute l'élégance que je trouve dans cette langue, il y a une particularité qui me déconcerte et semble totalement hors de propos.

Rust déréférence automatiquement les pointeurs lors des appels de méthode. J'ai fait quelques tests pour déterminer le comportement exact:

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 == @
}

Donc, il semble que, plus ou moins:

  • Le compilateur insérera autant d'opérateurs de déréférence que nécessaire pour appeler une méthode.
  • Le compilateur, lors de la résolution de méthodes déclarées à l'aide de &self (appel par référence):
    • Essaie d’abord d’appeler à une seule déréférence de self
    • Puis essaie d’appeler pour le type exact de self
    • Ensuite, essaye d'insérer autant d'opérateurs de déréférence que nécessaire pour une correspondance.
  • Les méthodes déclarées à l'aide de self (appel par valeur) pour le type T se comportent comme si elles avaient été déclarées à l'aide de &self (appel par référence) pour le type &T et appelle la référence à tout ce qui se trouve à gauche de l’opérateur de point.
  • Les règles ci-dessus sont d'abord essayées avec le déréférencement intégré brut, et s'il n'y a pas de correspondance, la surcharge avec Deref trait est utilisée.

Quelles sont les règles exactes de déréférencement automatique? Quelqu'un peut-il donner une justification formelle à une telle décision de conception?

141
kFYatek

Votre pseudo-code est à peu près correct. Pour cet exemple, supposons que nous ayons un appel de méthode foo.bar()foo: T. Je vais utiliser le syntaxe complète (FQS) pour indiquer sans ambiguïté le type de la méthode appelée, par exemple. A::bar(foo) ou A::bar(&***foo). Je vais juste écrire une pile de lettres majuscules aléatoires, chacune correspondant à un type/trait quelconque, sauf que T est toujours le type de la variable d'origine foo de la méthode. sur.

Le noyau de l'algorithme est:

  • Pour chaque "étape de déréférence"U (c'est-à-dire, définissez U = T Puis U = *T, ...)
    1. s'il existe une méthode bar où le type de destinataire (le type de self dans la méthode) correspond exactement à U, utilisez-le ( ne méthode "par valeur") )
    2. sinon, ajoutez une référence automatique (prenez & ou &mut du destinataire) et, si le destinataire de la méthode correspond à &U, utilisez-le ( n " méthode autorefd " )

Notamment, tout considère le "type de destinataire" de la méthode, et non le type Self du trait, c'est-à-dire impl ... for Foo { fn method(&self) {} } pense à &Foo lors de l'appariement de la méthode, et fn method2(&mut self) pense à &mut Foo à l'appariement.

C’est une erreur s’il ya plusieurs méthodes de trait valides dans les étapes internes (c’est-à-dire qu’il ne peut y avoir que zéro ou une méthode de trait valide dans chacune des étapes 1. ou 2., mais qu’il en existe une valide pour chacune: la à partir de 1 sera pris en premier), et les méthodes inhérentes priment sur celles de trait. C'est également une erreur si nous arrivons à la fin de la boucle sans rien trouver qui corresponde. C'est aussi une erreur d'avoir des implémentations Deref récursives, qui rendent la boucle infinie (elles atteindront la "limite de récursivité").

Ces règles semblent faire ce que je veux dire dans la plupart des cas, bien qu'il soit très utile d’écrire le formulaire FQS sans ambiguïté est très utile dans certains cas Edge, ainsi que pour les messages d’erreur sensibles pour le code généré par macro.

Une seule référence automatique est ajoutée car

  • s'il n'y avait pas de lien, les choses vont mal/lentement, car chaque type peut avoir un nombre arbitraire de références prises
  • en prenant une référence &foo conserve une connexion forte à foo (c'est l'adresse de foo lui-même), mais en prenant plus commence à la perdre: &&foo est l'adresse d'une variable temporaire sur la pile qui stocke &foo.

Exemples

Supposons que nous ayons un appel foo.refm(), si foo a le type:

  • X, nous commençons par U = X, refm a le type de destinataire &..., donc l'étape 1 ne correspond pas, prendre un auto-ref nous donne &X, Et cela correspond (avec Self = X), Donc l'appel est RefM::refm(&foo)
  • &X Commence par U = &X, Ce qui correspond à &self À la première étape (avec Self = X), Et l'appel est donc RefM::refm(foo)
  • &&&&&X, Cela ne correspond à aucune étape (le trait n'est pas implémenté pour &&&&X Ou &&&&&X), Donc nous déréférencons une fois pour obtenir U = &&&&X, qui correspond à 1 (avec Self = &&&X) et l'appel est RefM::refm(*foo)
  • Z, ne correspond à aucune étape, il est donc déréférencé une fois pour obtenir Y, ce qui ne correspond pas non plus. Il est donc déréférencé à nouveau pour obtenir X. ne correspond pas à 1, mais après la lecture automatique, l'appel est donc RefM::refm(&**foo).
  • &&A, Le 1. ne correspond pas et 2. non plus, car le trait n'est pas implémenté pour &A (Pour 1) ou &&A (Pour 2), donc est déréférencé à &A, ce qui correspond à 1., avec Self = A

Supposons que nous ayons foo.m(), et que A ne soit pas Copy, si foo a le type:

  • A, alors U = A correspond à self directement, donc l'appel est M::m(foo) avec Self = A
  • &A, Alors 1. ne correspond pas, ni 2. (ni &A Ni &&A Ne mettent en oeuvre le trait), de sorte qu'il est déréférencé à A, qui correspond, mais M::m(*foo) nécessite de prendre A par valeur et donc de sortir de foo, d'où l'erreur.
  • &&A, 1. ne correspond pas, mais la lecture automatique donne &&&A, Ce qui correspond. L'appel est donc M::m(&foo) avec Self = &&&A.

(Cette réponse est basée sur le code , et est raisonnablement proche du fichier README (légèrement dépassé) . Niko Matsakis, l'auteur principal de cette partie du compilateur/langage, jeté un coup d’œil sur cette réponse.)

116
huon