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

199

J'apprends / j'expérimente avec Rust, et dans toute l'élégance que je trouve dans cette langue, il y a une particularité qui me déroute et qui semble totalement déplacée.

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

( Aire de jeux )

Donc, il semble que, plus ou moins:

  • Le compilateur insérera autant d'opérateurs de déréférencement que nécessaire pour appeler une méthode.
  • Le compilateur, lors de la résolution des 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
    • Essaie ensuite d'appeler le type exact de self
    • Ensuite, essaie d'insérer autant d'opérateurs de déréférencement que nécessaire pour une correspondance
  • Les méthodes déclarées en utilisant self(appel par valeur) pour type Tse comportent comme si elles étaient déclarées en utilisant &self(appel par référence) pour type &Tet appelées sur la référence à ce qui se trouve sur le côté gauche de l'opérateur point.
  • Les règles ci-dessus sont d'abord essayées avec un déréférencement intégré brut, et s'il n'y a pas de correspondance, la surcharge avec Dereftrait est utilisée.

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

kFYatek
la source
1
J'ai posté ceci sur le subreddit Rust dans l'espoir d'obtenir de bonnes réponses!
Shepmaster
Pour plus de plaisir, essayez de répéter l'expérience en génériques et comparez les résultats.
user2665887

Réponses:

150

Votre pseudo-code est à peu près correct. Pour cet exemple, supposons que nous ayons un appel de méthode foo.bar()where foo: T. Je vais utiliser la syntaxe pleinement qualifiée (FQS) pour être sans ambiguïté sur le type avec lequel la méthode est appelée, par exemple A::bar(foo)ou A::bar(&***foo). Je vais juste écrire une pile de lettres majuscules aléatoires, chacune étant juste un type / trait arbitraire, sauf qu'il Ts'agit toujours du type de la variable d'origine sur foolaquelle la méthode est appelée.

Le cœur de l'algorithme est:

  • Pour chaque "étape de déréférencement" U (c'est-à-dire définir U = Tet ensuite U = *T, ...)
    1. s'il existe une méthode baroù le type de récepteur (le type de selfdans la méthode) correspond Uexactement, utilisez-la ( une "méthode par valeur" )
    2. sinon, ajoutez une auto-ref (prise &ou &mutdu receveur), et, si le receveur d'une méthode correspond &U, utilisez-la ( une "méthode autorefd" )

Notamment, tout considère le "type de récepteur" de la méthode, pas le Selftype du trait, c'est-à-dire qu'il impl ... for Foo { fn method(&self) {} }réfléchit &Foolors de l'appariement de la méthode, et auquel fn method2(&mut self)il réfléchirait &mut Foolors de l'appariement.

C'est une erreur s'il y a jamais 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 méthodes 1. ou 2., mais il peut y en avoir une valide pour chacune: celle à partir de 1 sera prise en premier), et les méthodes inhérentes ont la priorité sur les caractéristiques. C'est aussi une erreur si nous arrivons à la fin de la boucle sans rien trouver qui corresponde. C'est aussi une erreur d'avoir des Derefimplémentations 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 circonstances, bien que la capacité d'écrire le formulaire FQS sans ambiguïté soit très utile dans certains cas extrêmes, et 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 limite, les choses deviennent mauvaises / lentes, car chaque type peut avoir un nombre arbitraire de références prises
  • prendre une référence &fooconserve une forte connexion avec foo(c'est l'adresse d' fooelle - même), mais en prendre plus commence à la perdre: &&fooc'est l'adresse d'une variable temporaire sur la pile qui stocke &foo.

Exemples

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

  • X, alors nous commençons par U = X, refma le type de récepteur &..., donc l'étape 1 ne correspond pas, prendre une auto-ref nous donne &X, et cela correspond (avec Self = X), donc l'appel estRefM::refm(&foo)
  • &X, commence par U = &X, qui correspond &selfà la première étape (avec Self = X), et donc l'appel estRefM::refm(foo)
  • &&&&&X, cela ne correspond à aucune étape (le trait n'est pas implémenté pour &&&&Xou &&&&&X), donc nous déréférencer une fois pour obtenir U = &&&&X, qui correspond à 1 (avec Self = &&&X) et l'appel estRefM::refm(*foo)
  • Z, ne correspond à aucune des étapes, il est donc déréférencé une fois, pour obtenir Y, ce qui ne correspond pas non plus, donc il est à nouveau déréférencé, pour obtenir X, qui ne correspond pas à 1, mais correspond après l'autorefing, donc l'appel est RefM::refm(&**foo).
  • &&A, le 1. ne correspond pas et ne le fait pas non plus 2. puisque le trait n'est pas implémenté pour &A(pour 1) ou &&A(pour 2), il est donc déréférencé &A, ce qui correspond à 1., avecSelf = A

Supposons que nous ayons foo.m(), et ce An'est pas le Copycas, si fooa le type:

  • A, puis U = Acorrespond selfdirectement pour que l'appel soit M::m(foo)avecSelf = A
  • &A, alors 1. ne correspond pas, ni 2. (ni &An'implémente &&Ale trait), donc il est déréférencé à A, ce qui correspond, mais M::m(*foo)nécessite de prendre Apar valeur et donc de sortir foo, d'où l'erreur.
  • &&A, 1. ne correspond pas, mais l'autorefing donne &&&A, ce qui correspond, donc l'appel est M::m(&foo)avec Self = &&&A.

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

huon
la source
17
Cette réponse semble exhaustive et détaillée mais je pense qu'il manque un résumé court et accessible des règles. Un tel résumé est donné dans ce commentaire de Shepmaster : "Il [l'algorithme de déréf] sera déréfié autant de fois que possible ( &&String-> &String-> String-> str) puis référencé au maximum une fois ( str-> &str)".
Lii
(Je ne sais pas à quel point cette explication est exacte et complète moi-même.)
Lii
2
Dans quels cas le déréférencement automatique se produit-il? Est-il utilisé uniquement pour l'expression de récepteur pour l'appel de méthode? Pour les accès sur le terrain également? Affectation à droite? Côté gauche? Paramètres de fonction? Renvoyer des expressions de valeur?
Lii
2
Remarque: Actuellement, le nomicon a une note TODO pour voler des informations de cette réponse et l'écrire dans static.rust-lang.org/doc/master/nomicon/dot-operator.html
SamB
1
La coercition (A) a-t-elle été essayée avant ceci ou (B) essayé après ceci ou (C) essayé à chaque étape de cet algorithme ou (D) autre chose?
haslersn
9

La référence Rust contient un chapitre sur l'expression d'appel de méthode . J'ai copié la partie la plus importante ci-dessous. Rappel: nous parlons d'une expression recv.m(), où recvest appelée "expression du récepteur" ci-dessous.

La première étape consiste à créer une liste de types de récepteurs candidats. Obtenez-les en déréférençant à plusieurs reprises le type de l'expression du récepteur, en ajoutant chaque type rencontré à la liste, puis en essayant finalement une coercition non dimensionnée à la fin et en ajoutant le type de résultat si cela réussit. Ensuite, pour chaque candidat T, ajoutez &Tet &mut Tà la liste immédiatement après T.

Par exemple, si le récepteur est de type Box<[i32;2]>, alors les types de candidats seront Box<[i32;2]>, &Box<[i32;2]>, &mut Box<[i32;2]>, [i32; 2](par déréférencement), &[i32; 2], &mut [i32; 2], [i32](par la contrainte non encollé), &[i32]et enfin &mut [i32].

Ensuite, pour chaque type de candidat T, recherchez une méthode visible avec un récepteur de ce type aux endroits suivants:

  1. Tles méthodes inhérentes à (méthodes implémentées directement sur T[¹]).
  2. L'une des méthodes fournies par un trait visible implémenté par T. [...]

( Remarque à propos de [¹] : je pense en fait que cette formulation est erronée. J'ai ouvert un problème . Ignorons simplement cette phrase entre parenthèses.)


Passons en revue quelques exemples de votre code en détail! Pour vos exemples, nous pouvons ignorer la partie sur la "coercition non dimensionnée" et les "méthodes inhérentes".

(*X{val:42}).m(): le type de l'expression du récepteur est i32. Nous effectuons ces étapes:

  • Création de la liste des types de récepteurs candidats:
    • i32 ne peut pas être déréférencé, nous en avons donc déjà terminé avec l'étape 1. Liste: [i32]
    • Ensuite, nous ajoutons &i32et&mut i32 . Liste:[i32, &i32, &mut i32]
  • Recherche de méthodes pour chaque type de récepteur candidat:
    • Nous trouvons <i32 as M>::mqui a le type de récepteur i32. Nous avons donc déjà terminé.


Jusqu'ici si facile. Prenons maintenant un exemple plus difficile:(&&A).m() . Le type de l'expression du récepteur est &&A. Nous effectuons ces étapes:

  • Création de la liste des types de récepteurs candidats:
    • &&Apeut être déréférencé &A, donc nous ajoutons cela à la liste. &Apeut être à nouveau déréférencé, nous ajoutons donc également Aà la liste. Ane peut pas être déréférencé, alors nous nous arrêtons. Liste:[&&A, &A, A]
    • Ensuite, pour chaque type Tde la liste, nous ajoutons &Tet &mut Timmédiatement après T. Liste:[&&A, &&&A, &mut &&A, &A, &&A, &mut &A, A, &A, &mut A]
  • Recherche de méthodes pour chaque type de récepteur candidat:
    • Il n'y a pas de méthode avec le type de récepteur &&A , nous passons donc au type suivant dans la liste.
    • On retrouve la méthode <&&&A as M>::mqui a en effet le type de récepteur &&&A. Nous avons donc terminé.

Voici les listes de destinataires candidats pour tous vos exemples. Le type inclus dans ⟪x⟫est celui qui a "gagné", c'est-à-dire le premier type pour lequel une méthode d'ajustement a pu être trouvée. Souvenez-vous également que le premier type de la liste est toujours le type de l'expression du récepteur. Enfin, j'ai formaté la liste en lignes de trois, mais ce n'est que du formatage: cette liste est une liste plate.

  • (*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]
    
Lukas Kalbertodt
la source