Pourquoi ne puis-je pas stocker une valeur et une référence à cette valeur dans la même structure?

222

J'ai une valeur et je veux stocker cette valeur et une référence à quelque chose à l'intérieur de cette valeur dans mon propre type:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

Parfois, j'ai une valeur et je veux stocker cette valeur et une référence à cette valeur dans la même structure:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

Parfois, je ne prends même pas une référence de la valeur et j'obtiens la même erreur:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

Dans chacun de ces cas, j'obtiens une erreur indiquant que l'une des valeurs "ne vit pas assez longtemps". Que signifie cette erreur?

Shepmaster
la source
1
Pour ce dernier exemple, une définition de Parentet Childpourrait aider ...
Matthieu M.
1
@MatthieuM. J'en ai débattu, mais j'ai décidé de ne pas le faire sur la base des deux questions liées. Aucune de ces questions n'a examiné la définition de la structure ou de la méthode en question, j'ai donc pensé qu'il serait préférable d'imiter que les gens puissent plus facilement associer cette question à leur propre situation. Notez que je fais montrer la signature de méthode dans la réponse.
Shepmaster

Réponses:

245

Regardons une implémentation simple de ceci :

struct Parent {
    count: u32,
}

struct Child<'a> {
    parent: &'a Parent,
}

struct Combined<'a> {
    parent: Parent,
    child: Child<'a>,
}

impl<'a> Combined<'a> {
    fn new() -> Self {
        let parent = Parent { count: 42 };
        let child = Child { parent: &parent };

        Combined { parent, child }
    }
}

fn main() {}

Cela échouera avec l'erreur:

error[E0515]: cannot return value referencing local variable `parent`
  --> src/main.rs:19:9
   |
17 |         let child = Child { parent: &parent };
   |                                     ------- `parent` is borrowed here
18 | 
19 |         Combined { parent, child }
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0505]: cannot move out of `parent` because it is borrowed
  --> src/main.rs:19:20
   |
14 | impl<'a> Combined<'a> {
   |      -- lifetime `'a` defined here
...
17 |         let child = Child { parent: &parent };
   |                                     ------- borrow of `parent` occurs here
18 | 
19 |         Combined { parent, child }
   |         -----------^^^^^^---------
   |         |          |
   |         |          move out of `parent` occurs here
   |         returning this value requires that `parent` is borrowed for `'a`

Pour bien comprendre cette erreur, vous devez penser à la façon dont les valeurs sont représentées en mémoire et à ce qui se passe lorsque vous déplacez ces valeurs. Annotons Combined::newavec quelques adresses mémoire hypothétiques qui montrent où se trouvent les valeurs:

let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42 
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000

Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?

Que devrait-il arriver child? Si la valeur était simplement déplacée comme parent c'était le cas, cela ferait référence à la mémoire qui n'est plus garantie d'avoir une valeur valide. Tout autre morceau de code est autorisé à stocker des valeurs à l'adresse mémoire 0x1000. Accéder à cette mémoire en supposant qu'il s'agit d'un entier peut entraîner des plantages et / ou des bogues de sécurité, et est l'une des principales catégories d'erreurs que Rust empêche.

C'est exactement le problème que les vies empêchent. Une durée de vie est un peu de métadonnées qui vous permet, ainsi qu'au compilateur, de savoir combien de temps une valeur sera valide à son emplacement de mémoire actuel . C'est une distinction importante, car c'est une erreur courante que font les nouveaux arrivants de Rust. La durée de vie de la rouille n'est pas la période de temps entre la création d'un objet et sa destruction!

Par analogie, pensez-y de cette façon: au cours de la vie d'une personne, elle résidera dans de nombreux endroits différents, chacun avec une adresse distincte. Une vie à Rust se préoccupe de l'adresse où vous résidez actuellement , pas du moment où vous mourrez à l'avenir (bien que la mort change également votre adresse). Chaque fois que vous déménagez, c'est pertinent car votre adresse n'est plus valide.

Il est également important de noter que les durées de vie ne modifient pas votre code; votre code contrôle les durées de vie, vos durées de vie ne contrôlent pas le code. Le dicton lapidaire est "les durées de vie sont descriptives, pas normatives".

Annotons Combined::newavec quelques numéros de ligne que nous utiliserons pour mettre en évidence les durées de vie:

{                                          // 0
    let parent = Parent { count: 42 };     // 1
    let child = Child { parent: &parent }; // 2
                                           // 3
    Combined { parent, child }             // 4
}                                          // 5

La durée de vie concrète de parentest de 1 à 4, inclus (que je représenterai comme [1,4]). La durée de vie concrète de childest [2,4]et la durée de vie concrète de la valeur de retour est [4,5]. Il est possible d'avoir des durées de vie concrètes qui commencent à zéro - qui représenteraient la durée de vie d'un paramètre pour une fonction ou quelque chose qui existait en dehors du bloc.

Notez que la durée de vie de childlui-même est [2,4], mais qu'il se réfère à une valeur avec une durée de vie de [1,4]. C'est très bien tant que la valeur de référence devient invalide avant la valeur de référence. Le problème se produit lorsque nous essayons de revenir childdu bloc. Cela "prolongerait" la durée de vie au-delà de sa longueur naturelle.

Cette nouvelle connaissance devrait expliquer les deux premiers exemples. Le troisième nécessite d'examiner la mise en œuvre de Parent::child. Il y a de fortes chances que cela ressemble à ceci:

impl Parent {
    fn child(&self) -> Child { /* ... */ }
}

Cela utilise l' élision à vie pour éviter d'écrire explicitement paramètres de durée de vie génériques . Il équivaut à:

impl Parent {
    fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}

Dans les deux cas, la méthode indique qu'une Childstructure sera retournée qui a été paramétrée avec la durée de vie concrète de self . Autrement dit, l' Childinstance contient une référence à celui Parentqui l'a créée et ne peut donc pas vivre plus longtemps que cette Parentinstance.

Cela nous permet également de reconnaître que quelque chose ne va vraiment pas avec notre fonction de création:

fn make_combined<'a>() -> Combined<'a> { /* ... */ }

Bien que vous soyez plus susceptible de voir cela écrit sous une forme différente:

impl<'a> Combined<'a> {
    fn new() -> Combined<'a> { /* ... */ }
}

Dans les deux cas, aucun paramètre de durée de vie n'est fourni via un argument. Cela signifie que la durée de vieCombined sera paramétrée avec n'est limitée par rien - elle peut être ce que l'appelant veut qu'il soit. Cela n'a pas de sens, car l'appelant pourrait spécifier la 'staticdurée de vie et il n'y a aucun moyen de remplir cette condition.

Comment je le répare?

La solution la plus simple et la plus recommandée consiste à ne pas tenter de regrouper ces éléments dans la même structure. En faisant cela, l'imbrication de votre structure imitera la durée de vie de votre code. Placez ensemble les types qui possèdent des données dans une structure, puis fournissez des méthodes qui vous permettent d'obtenir des références ou des objets contenant des références selon vos besoins.

Il existe un cas particulier où le suivi à vie est trop zélé: lorsque vous avez quelque chose placé sur le tas. Cela se produit lorsque vous utilisez un Box<T> , par exemple. Dans ce cas, la structure déplacée contient un pointeur dans le tas. La valeur pointée restera stable, mais l'adresse du pointeur lui-même se déplacera. En pratique, cela n'a pas d'importance, car vous suivez toujours le pointeur.

La caisse de location (NON PLUS MAINTENUE OU SOUTENUE) ou la caisse owning_ref sont des moyens de représenter ce cas, mais elles nécessitent que l'adresse de base ne bouge jamais . Cela exclut les vecteurs mutants, ce qui peut provoquer une réallocation et un déplacement des valeurs allouées en tas.

Exemples de problèmes résolus avec la location:

Dans d'autres cas, vous souhaiterez peut-être passer à un certain type de comptage de références, comme en utilisant Rcou Arc.

Plus d'information

Après être passé parentdans la structure, pourquoi le compilateur n'est-il pas en mesure d'obtenir une nouvelle référence parentet de l'affecter à childla structure?

Bien qu'il soit théoriquement possible de le faire, cela entraînerait une grande quantité de complexité et de surcharge. Chaque fois que l'objet est déplacé, le compilateur devra insérer du code pour "corriger" la référence. Cela signifierait que la copie d'une structure n'est plus une opération très bon marché qui ne fait que déplacer quelques bits. Cela pourrait même signifier qu'un code comme celui-ci coûte cher, selon la qualité d'un optimiseur hypothétique:

let a = Object::new();
let b = a;
let c = b;

Au lieu de forcer cela à chaque mouvement, le programmeur choisit quand cela se produira en créant des méthodes qui prendront les références appropriées uniquement lorsque vous les appellerez.

Un type avec une référence à lui-même

Il y a un cas spécifique où vous pouvez créer un type avec une référence à lui-même. Vous devez cependant utiliser quelque chose comme Optionpour le faire en deux étapes:

#[derive(Debug)]
struct WhatAboutThis<'a> {
    name: String,
    nickname: Option<&'a str>,
}

fn main() {
    let mut tricky = WhatAboutThis {
        name: "Annabelle".to_string(),
        nickname: None,
    };
    tricky.nickname = Some(&tricky.name[..4]);

    println!("{:?}", tricky);
}

Cela fonctionne, dans un certain sens, mais la valeur créée est très limitée - elle ne peut jamais être déplacée. Notamment, cela signifie qu'il ne peut pas être renvoyé d'une fonction ou transmis par valeur à quoi que ce soit. Une fonction constructeur présente le même problème avec les durées de vie que ci-dessus:

fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }

Et alors Pin?

Pin, stabilisé dans Rust 1.33, a ceci dans la documentation du module :

Un excellent exemple d'un tel scénario serait la construction de structures auto-référentielles, car le déplacement d'un objet avec des pointeurs vers lui-même les invalidera, ce qui pourrait provoquer un comportement indéfini.

Il est important de noter que «autoréférentiel» ne signifie pas nécessairement utiliser une référence . En effet, l' exemple d'une structure auto-référentielle dit précisément (je souligne):

Nous ne pouvons pas en informer le compilateur avec une référence normale, car ce modèle ne peut pas être décrit avec les règles d'emprunt habituelles. Au lieu de cela, nous utilisons un pointeur brut , bien qu'il soit connu pour ne pas être nul, car nous savons qu'il pointe vers la chaîne.

La possibilité d'utiliser un pointeur brut pour ce comportement existe depuis Rust 1.0. En effet, la possession-ref et la location utilisent des pointeurs bruts sous le capot.

La seule chose qui Pinajoute à la table est une façon courante de déclarer qu'une valeur donnée est garantie de ne pas bouger.

Voir également:

Shepmaster
la source
1
Est-ce que quelque chose comme ça ( is.gd/wl2IAt ) est considéré comme idiomatique? C'est-à-dire, pour exposer les données via des méthodes au lieu des données brutes.
Peter Hall
2
@PeterHall bien sûr, cela signifie simplement que Combinedpossède le Childqui possède le Parent. Cela peut ou non avoir du sens selon les types réels que vous avez. Le renvoi de références à vos propres données internes est assez typique.
Shepmaster
Quelle est la solution au problème du tas?
derekdreery
@derekdreery peut-être pourriez-vous développer votre commentaire? Pourquoi le paragraphe entier parle-t-il de la caisse owning_ref insuffisante?
Shepmaster
1
@FynnBecker, il est toujours impossible de stocker une référence et une valeur dans cette référence. Pinest surtout un moyen de connaître la sécurité d'une structure contenant un pointeur auto-référentiel . La possibilité d'utiliser un pointeur brut dans le même but existe depuis Rust 1.0.
Shepmaster
4

Un problème légèrement différent qui provoque des messages de compilateur très similaires est la dépendance de la durée de vie des objets, plutôt que de stocker une référence explicite. Un exemple de cela est la bibliothèque ssh2 . Lorsque vous développez quelque chose de plus grand qu'un projet de test, il est tentant d'essayer de mettre côte à côte le Sessionet Channelobtenu à partir de cette session, en cachant les détails de l'implémentation à l'utilisateur. Cependant, notez que la Channeldéfinition a la 'sessdurée de vie dans son annotation de type, alors Sessionque non.

Cela provoque des erreurs de compilation similaires liées aux durées de vie.

Une façon de le résoudre de manière très simple consiste à déclarer l' Sessionextérieur dans l'appelant, puis à annoter la référence dans la structure avec une durée de vie, similaire à la réponse dans ce post du forum de l'utilisateur de Rust parlant du même problème tout en encapsulant SFTP . Cela n'aura pas l'air élégant et peut ne pas toujours s'appliquer - parce que maintenant vous avez deux entités à traiter, plutôt qu'une que vous vouliez!

Il s'avère que la caisse de location ou la caisse owning_ref de l'autre réponse sont également les solutions à ce problème. Considérons le owning_ref, qui a l'objet spécial à cette fin précise: OwningHandle. Pour éviter le déplacement de l'objet sous-jacent, nous l'allouons sur le tas à l'aide de a Box, ce qui nous donne la solution possible suivante:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

Le résultat de ce code est que nous ne pouvons plus l'utiliser Session, mais il est stocké avec celui Channelque nous utiliserons. Parce que l' OwningHandleobjet fait référence à Box, auquel fait référence Channel, lors de son stockage dans une structure, nous le nommons comme tel. REMARQUE: c'est juste ma compréhension. Je soupçonne que cela n'est peut-être pas correct, car cela semble être assez proche d'une discussion sur l' OwningHandleinsécurité .

Un détail curieux ici est que Sessionlogiquement a une relation similaire avec TcpStreamas Channelto to Session, mais sa propriété n'est pas prise et il n'y a pas d'annotations de type autour de cela. Au lieu de cela, c'est à l'utilisateur de s'en occuper, comme le dit la documentation de la méthode de prise de contact :

Cette session ne prend pas possession du socket fourni, il est recommandé de s'assurer que le socket persiste pendant toute la durée de vie de cette session pour s'assurer que la communication est correctement effectuée.

Il est également fortement recommandé que le flux fourni ne soit pas utilisé simultanément ailleurs pendant la durée de cette session car il peut interférer avec le protocole.

Donc, avec l' TcpStreamutilisation, il appartient entièrement au programmeur d'assurer l'exactitude du code. Avec le OwningHandle, l'attention sur l'endroit où se produit la «magie dangereuse» est attirée à l'aide du unsafe {}bloc.

Une discussion plus approfondie et plus approfondie de ce problème se trouve dans ce fil de discussion du forum de l'utilisateur de Rust - qui comprend un exemple différent et sa solution utilisant la caisse de location, qui ne contient pas de blocs dangereux.

Andrew Y
la source